feat(configuration): automatically map old keys (#3199)
This performs automatic remapping of deprecated configuration keys in most situations.pull/3614/head
parent
ab1d0c51d3
commit
d2f1e5d36d
|
@ -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.
|
||||
|
|
|
@ -18,7 +18,6 @@ aliases:
|
|||
|
||||
```yaml
|
||||
authentication_backend:
|
||||
disable_reset_password: false
|
||||
file:
|
||||
path: /config/users.yml
|
||||
password:
|
||||
|
|
|
@ -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" >}}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,14 +33,18 @@ server:
|
|||
host: 0.0.0.0
|
||||
```
|
||||
|
||||
## Policy
|
||||
|
||||
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+.
|
||||
|
||||
## Migrations
|
||||
|
||||
### 4.36.0
|
||||
|
||||
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
|
||||
|
||||
The options deprecated in version [4.30.0](#4300) have been fully removed as per our deprecation policy and warnings
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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{}) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -56,12 +56,12 @@ type AuthenticationBackendConfiguration struct {
|
|||
|
||||
PasswordReset PasswordResetAuthenticationBackendConfiguration `koanf:"password_reset"`
|
||||
|
||||
DisableResetPassword bool `koanf:"disable_reset_password"`
|
||||
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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue