summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2022-06-28 13:15:50 +1000
committerGitHub <noreply@github.com>2022-06-28 13:15:50 +1000
commitd2f1e5d36de9e271d0c668a7323b9e876812e6a2 (patch)
tree794d9b0b7ca2651a559ded58491a0aa1453a169c
parentab1d0c51d31e423f3caf4da1e02f3cc863c2cbd9 (diff)
feat(configuration): automatically map old keys (#3199)
This performs automatic remapping of deprecated configuration keys in most situations.
-rw-r--r--config.template.yml5
-rw-r--r--docs/content/en/configuration/first-factor/file.md1
-rw-r--r--docs/content/en/configuration/first-factor/introduction.md8
-rw-r--r--docs/content/en/configuration/first-factor/ldap.md3
-rw-r--r--docs/content/en/configuration/prologue/migration.md22
-rw-r--r--docs/content/en/overview/security/measures.md2
-rw-r--r--internal/authentication/ldap_user_provider.go2
-rw-r--r--internal/configuration/config.template.yml5
-rw-r--r--internal/configuration/deprecation.go116
-rw-r--r--internal/configuration/koanf_util.go130
-rw-r--r--internal/configuration/koanf_util_test.go75
-rw-r--r--internal/configuration/provider.go21
-rw-r--r--internal/configuration/provider_test.go10
-rw-r--r--internal/configuration/schema/authentication.go4
-rw-r--r--internal/configuration/template.go3
-rw-r--r--internal/configuration/validator/authentication.go4
-rw-r--r--internal/configuration/validator/authentication_test.go12
-rw-r--r--internal/model/const.go10
-rw-r--r--internal/model/semver.go120
-rw-r--r--internal/model/semver_test.go157
-rw-r--r--internal/server/handlers.go4
21 files changed, 669 insertions, 45 deletions
diff --git a/config.template.yml b/config.template.yml
index 9f9c28a08..e865f05ed 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -216,11 +216,10 @@ ntp:
##
## The available providers are: `file`, `ldap`. You must use only one of these providers.
authentication_backend:
- ## Disable both the HTML element and the API for reset password functionality.
- disable_reset_password: false
-
## Password Reset Options.
password_reset:
+ ## Disable both the HTML element and the API for reset password functionality.
+ disable: false
## External reset password url that redirects the user to an external reset portal. This disables the internal reset
## functionality.
diff --git a/docs/content/en/configuration/first-factor/file.md b/docs/content/en/configuration/first-factor/file.md
index c419fdd29..72a5f8278 100644
--- a/docs/content/en/configuration/first-factor/file.md
+++ b/docs/content/en/configuration/first-factor/file.md
@@ -18,7 +18,6 @@ aliases:
```yaml
authentication_backend:
- disable_reset_password: false
file:
path: /config/users.yml
password:
diff --git a/docs/content/en/configuration/first-factor/introduction.md b/docs/content/en/configuration/first-factor/introduction.md
index 2800d3c9c..b77975628 100644
--- a/docs/content/en/configuration/first-factor/introduction.md
+++ b/docs/content/en/configuration/first-factor/introduction.md
@@ -26,8 +26,8 @@ There are two ways to integrate *Authelia* with an authentication backend:
```yaml
authentication_backend:
refresh_interval: 5m
- disable_reset_password: false
password_reset:
+ disable: false
custom_url: ""
```
@@ -40,14 +40,14 @@ authentication_backend:
This setting controls the interval at which details are refreshed from the backend. Particularly useful for
[LDAP](#ldap).
-### disable_reset_password
+### password_reset
+
+#### disable
{{< confkey type="boolean" default="false" required="no" >}}
This setting controls if users can reset their password from the web frontend or not.
-### password_reset
-
#### custom_url
{{< confkey type="string" required="no" >}}
diff --git a/docs/content/en/configuration/first-factor/ldap.md b/docs/content/en/configuration/first-factor/ldap.md
index bca733623..3c06b5200 100644
--- a/docs/content/en/configuration/first-factor/ldap.md
+++ b/docs/content/en/configuration/first-factor/ldap.md
@@ -196,8 +196,7 @@ referrals to be followed when performing write operations.
server and utilizing a service account.*
Permits binding to the server without a password. For this option to be enabled both the [password](#password)
-configuration option must be blank and [disable_reset_password](introduction.md#disable_reset_password) must be
-disabled.
+configuration option must be blank and the [password_reset disable](introduction.md#disable) option must be `true`.
### user
diff --git a/docs/content/en/configuration/prologue/migration.md b/docs/content/en/configuration/prologue/migration.md
index 242a01f51..80b14cb97 100644
--- a/docs/content/en/configuration/prologue/migration.md
+++ b/docs/content/en/configuration/prologue/migration.md
@@ -14,8 +14,12 @@ aliases:
- /docs/configuration/migration.html
---
-This section documents changes in the configuration which may require manual migration by the administrator. Typically
-this only occurs when a configuration key is renamed or moved to a more appropriate location.
+This section discusses the change to the configuration over time. Since v4.36.0 the migration process is automatically
+performed where possible in memory (the file is unchanged). The automatic process generates warnings and the automatic
+migrations are disabled in major version bumps.
+
+If you're running a version prior to v4.36.0 this it may require manual migration by the administrator. Typically this
+only occurs when a configuration key is renamed or moved to a more appropriate location.
## Format
@@ -29,13 +33,17 @@ server:
host: 0.0.0.0
```
-## Policy
+## Migrations
-Our deprecation policy for configuration keys is 3 minor versions. For example if a configuration option is deprecated
-in version 4.30.0, it will remain as a warning for 4.30.x, 4.31.x, and 4.32.x; then it will become a fatal error in
-4.33.0+.
+### 4.36.0
-## Migrations
+Automatic mapping was introduced in this version.
+
+The following changes occurred in 4.30.0:
+
+| Previous Key | New Key |
+|:---------------------------------------------:|:---------------------------------------------:|
+| authentication_backend.disable_reset_password | authentication_backend.password_reset.disable |
### 4.33.0
diff --git a/docs/content/en/overview/security/measures.md b/docs/content/en/overview/security/measures.md
index 45af7a3a8..368bdfeb6 100644
--- a/docs/content/en/overview/security/measures.md
+++ b/docs/content/en/overview/security/measures.md
@@ -224,7 +224,7 @@ To configure mutual TLS, please refer to [this document](../../configuration/mis
### Reset Password
It's possible to disable the reset password functionality and is an optional adjustment to consider for anyone wanting
-to increase security. See the [configuration](../../configuration/first-factor/introduction.md#disable_reset_password)
+to increase security. See the [configuration](../../configuration/first-factor/introduction.md#disable)
for more information.
### Session security
diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go
index dd84461be..0c4b2ed9f 100644
--- a/internal/authentication/ldap_user_provider.go
+++ b/internal/authentication/ldap_user_provider.go
@@ -43,7 +43,7 @@ type LDAPUserProvider struct {
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
func NewLDAPUserProvider(config schema.AuthenticationBackendConfiguration, certPool *x509.CertPool) (provider *LDAPUserProvider) {
- provider = newLDAPUserProvider(*config.LDAP, config.DisableResetPassword, certPool, nil)
+ provider = newLDAPUserProvider(*config.LDAP, config.PasswordReset.Disable, certPool, nil)
return provider
}
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index 9f9c28a08..e865f05ed 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -216,11 +216,10 @@ ntp:
##
## The available providers are: `file`, `ldap`. You must use only one of these providers.
authentication_backend:
- ## Disable both the HTML element and the API for reset password functionality.
- disable_reset_password: false
-
## Password Reset Options.
password_reset:
+ ## Disable both the HTML element and the API for reset password functionality.
+ disable: false
## External reset password url that redirects the user to an external reset portal. This disables the internal reset
## functionality.
diff --git a/internal/configuration/deprecation.go b/internal/configuration/deprecation.go
new file mode 100644
index 000000000..f0850bf74
--- /dev/null
+++ b/internal/configuration/deprecation.go
@@ -0,0 +1,116 @@
+package configuration
+
+import (
+ "github.com/authelia/authelia/v4/internal/model"
+)
+
+// Deprecation represents a deprecated configuration key.
+type Deprecation struct {
+ Version model.SemanticVersion
+ Key string
+ NewKey string
+ AutoMap bool
+ MapFunc func(value interface{}) interface{}
+ ErrText string
+}
+
+var deprecations = map[string]Deprecation{
+ "logs_level": {
+ Version: model.SemanticVersion{Major: 4, Minor: 7},
+ Key: "logs_level",
+ NewKey: "log.level",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "logs_file": {
+ Version: model.SemanticVersion{Major: 4, Minor: 7},
+ Key: "logs_file",
+ NewKey: "log.file_path",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "authentication_backend.ldap.skip_verify": {
+ Version: model.SemanticVersion{Major: 4, Minor: 25},
+ Key: "authentication_backend.ldap.skip_verify",
+ NewKey: "authentication_backend.ldap.tls.skip_verify",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "authentication_backend.ldap.minimum_tls_version": {
+ Version: model.SemanticVersion{Major: 4, Minor: 25},
+ Key: "authentication_backend.ldap.minimum_tls_version",
+ NewKey: "authentication_backend.ldap.tls.minimum_version",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "notifier.smtp.disable_verify_cert": {
+ Version: model.SemanticVersion{Major: 4, Minor: 25},
+ Key: "notifier.smtp.disable_verify_cert",
+ NewKey: "notifier.smtp.tls.skip_verify",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "notifier.smtp.trusted_cert": {
+ Version: model.SemanticVersion{Major: 4, Minor: 25},
+ Key: "notifier.smtp.trusted_cert",
+ NewKey: "certificates_directory",
+ AutoMap: false,
+ MapFunc: nil,
+ },
+ "host": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "logs_file",
+ NewKey: "server.host",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "port": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "port",
+ NewKey: "server.port",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "tls_key": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "tls_key",
+ NewKey: "server.tls.key",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "tls_cert": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "tls_cert",
+ NewKey: "server.tls.certificate",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "log_level": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "log_level",
+ NewKey: "log.level",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "log_file_path": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "log_file_path",
+ NewKey: "log.file_path",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "log_format": {
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ Key: "log_format",
+ NewKey: "log.format",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+ "authentication_backend.disable_reset_password": {
+ Version: model.SemanticVersion{Major: 4, Minor: 36},
+ Key: "authentication_backend.disable_reset_password",
+ NewKey: "authentication_backend.password_reset.disable",
+ AutoMap: true,
+ MapFunc: nil,
+ },
+}
diff --git a/internal/configuration/koanf_util.go b/internal/configuration/koanf_util.go
index d9e816f0e..589b4b05e 100644
--- a/internal/configuration/koanf_util.go
+++ b/internal/configuration/koanf_util.go
@@ -2,13 +2,16 @@ package configuration
import (
"fmt"
+ "strings"
"github.com/knadh/koanf"
+ "github.com/knadh/koanf/providers/confmap"
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
-func getAllKoanfKeys(ko *koanf.Koanf) (keys []string) {
+func koanfGetKeys(ko *koanf.Koanf) (keys []string) {
keys = ko.Keys()
for key, value := range ko.All() {
@@ -34,3 +37,128 @@ func getAllKoanfKeys(ko *koanf.Koanf) (keys []string) {
return keys
}
+
+func koanfRemapKeys(val *schema.StructValidator, ko *koanf.Koanf, ds map[string]Deprecation) (final *koanf.Koanf, err error) {
+ keys := ko.All()
+
+ keys = koanfRemapKeysStandard(keys, val, ds)
+ keys = koanfRemapKeysMapped(keys, val, ds)
+
+ final = koanf.New(".")
+
+ if err = final.Load(confmap.Provider(keys, "."), nil); err != nil {
+ return nil, err
+ }
+
+ return final, nil
+}
+
+func koanfRemapKeysStandard(keys map[string]interface{}, val *schema.StructValidator, ds map[string]Deprecation) (keysFinal map[string]interface{}) {
+ var (
+ ok bool
+ d Deprecation
+ key string
+ value interface{}
+ )
+
+ keysFinal = make(map[string]interface{})
+
+ for key, value = range keys {
+ if d, ok = ds[key]; ok {
+ if !d.AutoMap {
+ val.Push(fmt.Errorf("invalid configuration key '%s' was replaced by '%s'", d.Key, d.NewKey))
+
+ keysFinal[key] = value
+
+ continue
+ } else {
+ val.PushWarning(fmt.Errorf("configuration key '%s' is deprecated in %s and has been replaced by '%s': "+
+ "this has been automatically mapped for you but you will need to adjust your configuration to remove this message", d.Key, d.Version.String(), d.NewKey))
+ }
+
+ if !mapHasKey(d.NewKey, keys) && !mapHasKey(d.NewKey, keysFinal) {
+ if d.MapFunc != nil {
+ keysFinal[d.NewKey] = d.MapFunc(value)
+ } else {
+ keysFinal[d.NewKey] = value
+ }
+ }
+
+ continue
+ }
+
+ keysFinal[key] = value
+ }
+
+ return keysFinal
+}
+
+func koanfRemapKeysMapped(keys map[string]interface{}, val *schema.StructValidator, ds map[string]Deprecation) (keysFinal map[string]interface{}) {
+ var (
+ key string
+ value interface{}
+ slc, slcFinal []interface{}
+ ok bool
+ m map[string]interface{}
+ d Deprecation
+ )
+
+ keysFinal = make(map[string]interface{})
+
+ for key, value = range keys {
+ if slc, ok = value.([]interface{}); !ok {
+ keysFinal[key] = value
+
+ continue
+ }
+
+ slcFinal = make([]interface{}, len(slc))
+
+ for i, item := range slc {
+ if m, ok = item.(map[string]interface{}); !ok {
+ slcFinal[i] = item
+
+ continue
+ }
+
+ itemFinal := make(map[string]interface{})
+
+ for subkey, element := range m {
+ prefix := fmt.Sprintf("%s[].", key)
+
+ fullKey := prefix + subkey
+
+ if d, ok = ds[fullKey]; ok {
+ if !d.AutoMap {
+ val.Push(fmt.Errorf("invalid configuration key '%s' was replaced by '%s'", d.Key, d.NewKey))
+
+ itemFinal[subkey] = element
+
+ continue
+ } else {
+ val.PushWarning(fmt.Errorf("configuration key '%s' is deprecated in %s and has been replaced by '%s': "+
+ "this has been automatically mapped for you but you will need to adjust your configuration to remove this message", d.Key, d.Version.String(), d.NewKey))
+ }
+
+ newkey := strings.Replace(d.NewKey, prefix, "", 1)
+
+ if !mapHasKey(newkey, m) && !mapHasKey(newkey, itemFinal) {
+ if d.MapFunc != nil {
+ itemFinal[newkey] = d.MapFunc(element)
+ } else {
+ itemFinal[newkey] = element
+ }
+ }
+ } else {
+ itemFinal[subkey] = element
+ }
+ }
+
+ slcFinal[i] = itemFinal
+ }
+
+ keysFinal[key] = slcFinal
+ }
+
+ return keysFinal
+}
diff --git a/internal/configuration/koanf_util_test.go b/internal/configuration/koanf_util_test.go
new file mode 100644
index 000000000..fb7038818
--- /dev/null
+++ b/internal/configuration/koanf_util_test.go
@@ -0,0 +1,75 @@
+package configuration
+
+import (
+ "testing"
+
+ "github.com/knadh/koanf"
+ "github.com/knadh/koanf/parsers/yaml"
+ "github.com/knadh/koanf/providers/rawbytes"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
+ "github.com/authelia/authelia/v4/internal/model"
+)
+
+type testDeprecationsConf struct {
+ SubItems []testDeprecationsConfSubItem `koanf:"subitems"`
+
+ ANonSubItemString string `koanf:"a_non_subitem_string"`
+ ANonSubItemInt int `koanf:"a_non_subitem_int"`
+ ANonSubItemBool bool `koanf:"a_non_subitem_bool"`
+}
+
+type testDeprecationsConfSubItem struct {
+ AString string `koanf:"a_string"`
+ AnInt int `koanf:"an_int"`
+ ABool bool `koanf:"a_bool"`
+}
+
+func TestSubItemRemap(t *testing.T) {
+ ds := map[string]Deprecation{
+ "astring": {
+ Key: "astring",
+ NewKey: "a_non_subitem_string",
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ AutoMap: true,
+ },
+ "subitems[].astring": {
+ Key: "subitems[].astring",
+ NewKey: "subitems[].a_string",
+ Version: model.SemanticVersion{Major: 4, Minor: 30},
+ AutoMap: true,
+ },
+ }
+
+ val := schema.NewStructValidator()
+
+ ko := koanf.New(".")
+
+ configYAML := []byte(`
+astring: test
+subitems:
+- astring: example
+- an_int: 1
+`)
+
+ require.NoError(t, ko.Load(rawbytes.Provider(configYAML), yaml.Parser()))
+
+ final, err := koanfRemapKeys(val, ko, ds)
+ require.NoError(t, err)
+
+ conf := &testDeprecationsConf{}
+
+ require.NoError(t, final.Unmarshal("", conf))
+
+ assert.Equal(t, "test", conf.ANonSubItemString)
+ assert.Equal(t, 0, conf.ANonSubItemInt)
+ assert.False(t, conf.ANonSubItemBool)
+
+ require.Len(t, conf.SubItems, 2)
+ assert.Equal(t, "example", conf.SubItems[0].AString)
+ assert.Equal(t, 0, conf.SubItems[0].AnInt)
+ assert.Equal(t, "", conf.SubItems[1].AString)
+ assert.Equal(t, 1, conf.SubItems[1].AnInt)
+}
diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go
index b64bb8bc1..111e5c041 100644
--- a/internal/configuration/provider.go
+++ b/internal/configuration/provider.go
@@ -29,14 +29,27 @@ func LoadAdvanced(val *schema.StructValidator, path string, result interface{},
StrictMerge: false,
})
- err = loadSources(ko, val, sources...)
- if err != nil {
+ if err = loadSources(ko, val, sources...); err != nil {
return ko.Keys(), err
}
- unmarshal(ko, val, path, result)
+ var final *koanf.Koanf
- return getAllKoanfKeys(ko), nil
+ if final, err = koanfRemapKeys(val, ko, deprecations); err != nil {
+ return koanfGetKeys(ko), err
+ }
+
+ unmarshal(final, val, path, result)
+
+ return koanfGetKeys(final), nil
+}
+
+func mapHasKey(k string, m map[string]interface{}) bool {
+ if _, ok := m[k]; ok {
+ return true
+ }
+
+ return false
}
func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o interface{}) {
diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go
index d3fd9cb5a..3f7aa7b45 100644
--- a/internal/configuration/provider_test.go
+++ b/internal/configuration/provider_test.go
@@ -228,17 +228,19 @@ func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
val := schema.NewStructValidator()
- keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config_bad_keys.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
+ keys, c, err := Load(val, NewDefaultSources([]string{"./test_resources/config_bad_keys.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
- require.Len(t, val.Errors(), 2)
- assert.Len(t, val.Warnings(), 0)
+ require.Len(t, val.Errors(), 1)
+ require.Len(t, val.Warnings(), 1)
assert.EqualError(t, val.Errors()[0], "configuration key not expected: loggy_file")
- assert.EqualError(t, val.Errors()[1], "invalid configuration key 'logs_level' was replaced by 'log.level'")
+ assert.EqualError(t, val.Warnings()[0], "configuration key 'logs_level' is deprecated in 4.7.0 and has been replaced by 'log.level': this has been automatically mapped for you but you will need to adjust your configuration to remove this message")
+
+ assert.Equal(t, "debug", c.Log.Level)
}
func TestShouldRaiseErrOnInvalidNotifierSMTPSender(t *testing.T) {
diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go
index a87536e39..895cc7711 100644
--- a/internal/configuration/schema/authentication.go
+++ b/internal/configuration/schema/authentication.go
@@ -56,12 +56,12 @@ type AuthenticationBackendConfiguration struct {
PasswordReset PasswordResetAuthenticationBackendConfiguration `koanf:"password_reset"`
- DisableResetPassword bool `koanf:"disable_reset_password"`
- RefreshInterval string `koanf:"refresh_interval"`
+ RefreshInterval string `koanf:"refresh_interval"`
}
// PasswordResetAuthenticationBackendConfiguration represents the configuration related to password reset functionality.
type PasswordResetAuthenticationBackendConfiguration struct {
+ Disable bool `koanf:"disable"`
CustomURL url.URL `koanf:"custom_url"`
}
diff --git a/internal/configuration/template.go b/internal/configuration/template.go
index e777f4d33..667b1cf0a 100644
--- a/internal/configuration/template.go
+++ b/internal/configuration/template.go
@@ -15,8 +15,7 @@ func EnsureConfigurationExists(path string) (created bool, err error) {
_, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
- err := os.WriteFile(path, template, 0600)
- if err != nil {
+ if err = os.WriteFile(path, template, 0600); err != nil {
return false, fmt.Errorf(errFmtGenerateConfiguration, err)
}
diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go
index f65f8f04e..59ea0518a 100644
--- a/internal/configuration/validator/authentication.go
+++ b/internal/configuration/validator/authentication.go
@@ -37,7 +37,7 @@ func ValidateAuthenticationBackend(config *schema.AuthenticationBackendConfigura
if config.PasswordReset.CustomURL.String() != "" {
switch config.PasswordReset.CustomURL.Scheme {
case schemeHTTP, schemeHTTPS:
- config.DisableResetPassword = false
+ config.PasswordReset.Disable = false
default:
validator.Push(fmt.Errorf(errFmtAuthBackendPasswordResetCustomURLScheme, config.PasswordReset.CustomURL.String(), config.PasswordReset.CustomURL.Scheme))
}
@@ -197,7 +197,7 @@ func validateLDAPRequiredParameters(config *schema.AuthenticationBackendConfigur
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendUnauthenticatedBindWithPassword))
}
- if !config.DisableResetPassword {
+ if !config.PasswordReset.Disable {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendUnauthenticatedBindWithResetEnabled))
}
} else {
diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go
index a575a231c..9df81a38b 100644
--- a/internal/configuration/validator/authentication_test.go
+++ b/internal/configuration/validator/authentication_test.go
@@ -233,9 +233,9 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfigura
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenResetURLIsInvalid() {
suite.config.PasswordReset.CustomURL = url.URL{Scheme: "ldap", Host: "google.com"}
- suite.config.DisableResetPassword = true
+ suite.config.PasswordReset.Disable = true
- suite.Assert().True(suite.config.DisableResetPassword)
+ suite.Assert().True(suite.config.PasswordReset.Disable)
ValidateAuthenticationBackend(&suite.config, suite.validator)
@@ -244,7 +244,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenResetURLIsI
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: password_reset: option 'custom_url' is configured to 'ldap://google.com' which has the scheme 'ldap' but the scheme must be either 'http' or 'https'")
- suite.Assert().True(suite.config.DisableResetPassword)
+ suite.Assert().True(suite.config.PasswordReset.Disable)
}
func (suite *FileBasedAuthenticationBackend) TestShouldNotRaiseErrorWhenResetURLIsValid() {
@@ -258,16 +258,16 @@ func (suite *FileBasedAuthenticationBackend) TestShouldNotRaiseErrorWhenResetURL
func (suite *FileBasedAuthenticationBackend) TestShouldConfigureDisableResetPasswordWhenCustomURL() {
suite.config.PasswordReset.CustomURL = url.URL{Scheme: "https", Host: "google.com"}
- suite.config.DisableResetPassword = true
+ suite.config.PasswordReset.Disable = true
- suite.Assert().True(suite.config.DisableResetPassword)
+ suite.Assert().True(suite.config.PasswordReset.Disable)
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
- suite.Assert().False(suite.config.DisableResetPassword)
+ suite.Assert().False(suite.config.PasswordReset.Disable)
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() {
diff --git a/internal/model/const.go b/internal/model/const.go
index efdf6ef38..3c0b13a64 100644
--- a/internal/model/const.go
+++ b/internal/model/const.go
@@ -1,5 +1,9 @@
package model
+import (
+ "regexp"
+)
+
const (
errFmtValueNil = "cannot value model type '%T' with value nil to driver.Value"
errFmtScanNil = "cannot scan model type '%T' from value nil: type doesn't support nil values"
@@ -17,3 +21,9 @@ const (
// SecondFactorMethodDuo method using Duo application to receive push notifications.
SecondFactorMethodDuo = "mobile_push"
)
+
+var reSemanticVersion = regexp.MustCompile(`^v?(?P<Major>\d+)\.(?P<Minor>\d+)\.(?P<Patch>\d+)(\-(?P<PreRelease>[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*))?(\+(?P<Metadata>[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*))?$`)
+
+const (
+ semverRegexpGroupPreRelease = "PreRelease"
+)
diff --git a/internal/model/semver.go b/internal/model/semver.go
new file mode 100644
index 000000000..4e724c837
--- /dev/null
+++ b/internal/model/semver.go
@@ -0,0 +1,120 @@
+package model
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// NewSemanticVersion creates a SemanticVersion from a string.
+func NewSemanticVersion(input string) (version *SemanticVersion, err error) {
+ if !reSemanticVersion.MatchString(input) {
+ return nil, fmt.Errorf("the input '%s' failed to match the semantic version pattern", input)
+ }
+
+ version = &SemanticVersion{}
+
+ submatch := reSemanticVersion.FindStringSubmatch(input)
+
+ for i, name := range reSemanticVersion.SubexpNames() {
+ switch name {
+ case "Major":
+ version.Major, _ = strconv.Atoi(submatch[i])
+ case "Minor":
+ version.Minor, _ = strconv.Atoi(submatch[i])
+ case "Patch":
+ version.Patch, _ = strconv.Atoi(submatch[i])
+ case semverRegexpGroupPreRelease, "Metadata":
+ if submatch[i] == "" {
+ continue
+ }
+
+ val := strings.Split(submatch[i], ".")
+
+ if name == semverRegexpGroupPreRelease {
+ version.PreRelease = val
+ } else {
+ version.Metadata = val
+ }
+ }
+ }
+
+ return version, nil
+}
+
+// SemanticVersion represents a semantic 2.0 version.
+type SemanticVersion struct {
+ Major int
+ Minor int
+ Patch int
+ PreRelease []string
+ Metadata []string
+}
+
+// String is a function to provide a nice representation of a SemanticVersion.
+func (v SemanticVersion) String() (value string) {
+ builder := strings.Builder{}
+
+ builder.WriteString(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch))
+
+ if len(v.PreRelease) != 0 {
+ builder.WriteString("-")
+ builder.WriteString(strings.Join(v.PreRelease, "."))
+ }
+
+ if len(v.Metadata) != 0 {
+ builder.WriteString("+")
+ builder.WriteString(strings.Join(v.Metadata, "."))
+ }
+
+ return builder.String()
+}
+
+// Equal returns true if this SemanticVersion is equal to the provided SemanticVersion.
+func (v SemanticVersion) Equal(version SemanticVersion) (equals bool) {
+ return v.Major == version.Major && v.Minor == version.Minor && v.Patch == version.Patch
+}
+
+// GreaterThan returns true if this SemanticVersion is greater than the provided SemanticVersion.
+func (v SemanticVersion) GreaterThan(version SemanticVersion) (gt bool) {
+ if v.Major > version.Major {
+ return true
+ }
+
+ if v.Major == version.Major && v.Minor > version.Minor {
+ return true
+ }
+
+ if v.Major == version.Major && v.Minor == version.Minor && v.Patch > version.Patch {
+ return true
+ }
+
+ return false
+}
+
+// LessThan returns true if this SemanticVersion is less than the provided SemanticVersion.
+func (v SemanticVersion) LessThan(version SemanticVersion) (gt bool) {
+ if v.Major < version.Major {
+ return true
+ }
+
+ if v.Major == version.Major && v.Minor < version.Minor {
+ return true
+ }
+
+ if v.Major == version.Major && v.Minor == version.Minor && v.Patch < version.Patch {
+ return true
+ }
+
+ return false
+}
+
+// GreaterThanOrEqual returns true if this SemanticVersion is greater than or equal to the provided SemanticVersion.
+func (v SemanticVersion) GreaterThanOrEqual(version SemanticVersion) (ge bool) {
+ return v.Equal(version) || v.GreaterThan(version)
+}
+
+// LessThanOrEqual returns true if this SemanticVersion is less than or equal to the provided SemanticVersion.
+func (v SemanticVersion) LessThanOrEqual(version SemanticVersion) (ge bool) {
+ return v.Equal(version) || v.LessThan(version)
+}
diff --git a/internal/model/semver_test.go b/internal/model/semver_test.go
new file mode 100644
index 000000000..e2548329b
--- /dev/null
+++ b/internal/model/semver_test.go
@@ -0,0 +1,157 @@
+package model
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewSemanticVersion(t *testing.T) {
+ testCases := []struct {
+ desc string
+ have string
+ expected *SemanticVersion
+ err string
+ }{
+ {
+ desc: "ShouldParseStandardSemVer",
+ have: "4.30.0",
+ expected: &SemanticVersion{Major: 4, Minor: 30, Patch: 0},
+ },
+ {
+ desc: "ShouldParseSemVerWithPre",
+ have: "4.30.0-alpha1",
+ expected: &SemanticVersion{Major: 4, Minor: 30, Patch: 0, PreRelease: []string{"alpha1"}},
+ },
+ {
+ desc: "ShouldParseSemVerWithMeta",
+ have: "4.30.0+build4",
+ expected: &SemanticVersion{Major: 4, Minor: 30, Patch: 0, Metadata: []string{"build4"}},
+ },
+ {
+ desc: "ShouldParseSemVerWithPreAndMeta",
+ have: "4.30.0-alpha1+build4",
+ expected: &SemanticVersion{Major: 4, Minor: 30, Patch: 0, PreRelease: []string{"alpha1"}, Metadata: []string{"build4"}},
+ },
+ {
+ desc: "ShouldParseSemVerWithPreAndMetaMulti",
+ have: "4.30.0-alpha1.test+build4.new",
+ expected: &SemanticVersion{Major: 4, Minor: 30, Patch: 0, PreRelease: []string{"alpha1", "test"}, Metadata: []string{"build4", "new"}},
+ },
+ {
+ desc: "ShouldNotParseInvalidVersion",
+ have: "1.2",
+ expected: nil,
+ err: "the input '1.2' failed to match the semantic version pattern",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ version, err := NewSemanticVersion(tc.have)
+
+ if tc.err == "" {
+ assert.Nil(t, err)
+ require.NotNil(t, version)
+ assert.Equal(t, tc.expected, version)
+ assert.Equal(t, tc.have, version.String())
+ } else {
+ assert.Nil(t, version)
+ require.NotNil(t, err)
+ assert.EqualError(t, err, tc.err)
+ }
+ })
+ }
+}
+
+func TestSemanticVersionComparisons(t *testing.T) {
+ testCases := []struct {
+ desc string
+
+ haveFirst, haveSecond SemanticVersion
+
+ expectedEQ, expectedGT, expectedGE, expectedLT, expectedLE bool
+ }{
+ {
+ desc: "ShouldCompareVersionLessThanMajor",
+ haveFirst: SemanticVersion{Major: 4, Minor: 30, Patch: 0},
+ haveSecond: SemanticVersion{Major: 5, Minor: 3, Patch: 0},
+ expectedEQ: false,
+ expectedGT: false,
+ expectedGE: false,
+ expectedLT: true,
+ expectedLE: true,
+ },
+ {
+ desc: "ShouldCompareVersionLessThanMinor",
+ haveFirst: SemanticVersion{Major: 4, Minor: 30, Patch: 0},
+ haveSecond: SemanticVersion{Major: 4, Minor: 31, Patch: 0},
+ expectedEQ: false,
+ expectedGT: false,
+ expectedGE: false,
+ expectedLT: true,
+ expectedLE: true,
+ },
+ {
+ desc: "ShouldCompareVersionLessThanPatch",
+ haveFirst: SemanticVersion{Major: 4, Minor: 31, Patch: 0},
+ haveSecond: SemanticVersion{Major: 4, Minor: 31, Patch: 9},
+ expectedEQ: false,
+ expectedGT: false,
+ expectedGE: false,
+ expectedLT: true,
+ expectedLE: true,
+ },
+ {
+ desc: "ShouldCompareVersionEqual",
+ haveFirst: SemanticVersion{Major: 4, Minor: 31, Patch: 0},
+ haveSecond: SemanticVersion{Major: 4, Minor: 31, Patch: 0},
+ expectedEQ: true,
+ expectedGT: false,
+ expectedGE: true,
+ expectedLT: false,
+ expectedLE: true,
+ },
+ {
+ desc: "ShouldCompareVersionGreaterThanMajor",
+ haveFirst: SemanticVersion{Major: 5, Minor: 0, Patch: 0},
+ haveSecond: SemanticVersion{Major: 4, Minor: 30, Patch: 0},
+ expectedEQ: false,
+ expectedGT: true,
+ expectedGE: true,
+ expectedLT: false,
+ expectedLE: false,
+ },
+ {
+ desc: "ShouldCompareVersionGreaterThanMinor",
+ haveFirst: SemanticVersion{Major: 4, Minor: 31, Patch: 0},
+ haveSecond: SemanticVersion{Major: 4, Minor: 30, Patch: 0},
+ expectedEQ: false,
+ expectedGT: true,
+ expectedGE: true,
+ expectedLT: false,
+ expectedLE: false,
+ },
+ {
+ desc: "ShouldCompareVersionGreaterThanPatch",
+ haveFirst: SemanticVersion{Major: 4, Minor: 31, Patch: 5},
+ haveSecond: SemanticVersion{Major: 4, Minor: 31, Patch: 0},
+ expectedEQ: false,
+ expectedGT: true,
+ expectedGE: true,
+ expectedLT: false,
+ expectedLE: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ assert.Equal(t, tc.expectedEQ, tc.haveFirst.Equal(tc.haveSecond))
+ assert.Equal(t, tc.expectedGT, tc.haveFirst.GreaterThan(tc.haveSecond))
+ assert.Equal(t, tc.expectedGE, tc.haveFirst.GreaterThanOrEqual(tc.haveSecond))
+ assert.Equal(t, tc.expectedLT, tc.haveFirst.LessThan(tc.haveSecond))
+ assert.Equal(t, tc.expectedLE, tc.haveFirst.LessThanOrEqual(tc.haveSecond))
+ })
+ }
+}
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index a7f5798c9..389743d39 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -93,7 +93,7 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
func handleRouter(config schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
rememberMe := strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled)
- resetPassword := strconv.FormatBool(!config.AuthenticationBackend.DisableResetPassword)
+ resetPassword := strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable)
resetPasswordCustomURL := config.AuthenticationBackend.PasswordReset.CustomURL.String()
@@ -175,7 +175,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.POST("/api/logout", middlewareAPI(handlers.LogoutPOST))
// Only register endpoints if forgot password is not disabled.
- if !config.AuthenticationBackend.DisableResetPassword &&
+ if !config.AuthenticationBackend.PasswordReset.Disable &&
config.AuthenticationBackend.PasswordReset.CustomURL.String() == "" {
// Password reset related endpoints.
r.POST("/api/reset-password/identity/start", middlewareAPI(handlers.ResetPasswordIdentityStart))