summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2023-11-23 08:20:36 +1100
committerGitHub <noreply@github.com>2023-11-23 08:20:36 +1100
commitc49b973120c7fd755923a2b88afd794c7d320d6e (patch)
tree5fcef6de1a85568eee3c67470f73b790b294dda8
parentfa141929a39e546f3f3ca6bcbc7bd72c64e575c8 (diff)
fix(configuration): illogical refresh interval default (#6319)
When using the file provider with watch enabled, the refresh interval should just be set to always default as the cost is minimal. Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
-rw-r--r--cmd/authelia-scripts/cmd/gen.go2
-rw-r--r--docs/content/en/configuration/first-factor/introduction.md11
-rw-r--r--docs/data/configkeys.json10
-rw-r--r--docs/static/schemas/latest/json-schema/configuration.json22
-rw-r--r--docs/static/schemas/v4.38/json-schema/configuration.json22
-rw-r--r--internal/configuration/decode_hooks.go153
-rw-r--r--internal/configuration/decode_hooks_test.go277
-rw-r--r--internal/configuration/provider.go1
-rw-r--r--internal/configuration/provider_test.go58
-rw-r--r--internal/configuration/schema/authentication.go6
-rw-r--r--internal/configuration/schema/const.go11
-rw-r--r--internal/configuration/schema/keys.go2
-rw-r--r--internal/configuration/schema/types.go71
-rw-r--r--internal/configuration/test_resources/config.durations.yml1
-rw-r--r--internal/configuration/test_resources/config.no-refresh.yml172
-rw-r--r--internal/configuration/test_resources/config.yml1
-rw-r--r--internal/configuration/test_resources/config_alt.yml1
-rw-r--r--internal/configuration/validator/authentication.go11
-rw-r--r--internal/configuration/validator/authentication_test.go29
-rw-r--r--internal/handlers/handler_authz_authn.go28
-rw-r--r--internal/handlers/handler_authz_builder.go18
-rw-r--r--internal/handlers/handler_authz_builder_test.go14
-rw-r--r--internal/handlers/handler_authz_test.go32
-rw-r--r--internal/handlers/handler_authz_types.go4
-rw-r--r--internal/handlers/handler_firstfactor.go25
-rw-r--r--internal/mocks/authelia_ctx.go1
26 files changed, 830 insertions, 153 deletions
diff --git a/cmd/authelia-scripts/cmd/gen.go b/cmd/authelia-scripts/cmd/gen.go
index d4d02788a..c82d0dc2c 100644
--- a/cmd/authelia-scripts/cmd/gen.go
+++ b/cmd/authelia-scripts/cmd/gen.go
@@ -7,5 +7,5 @@
package cmd
const (
- versionSwaggerUI = "5.10.0"
+ versionSwaggerUI = "5.10.1"
)
diff --git a/docs/content/en/configuration/first-factor/introduction.md b/docs/content/en/configuration/first-factor/introduction.md
index 889c51d6e..660953b8d 100644
--- a/docs/content/en/configuration/first-factor/introduction.md
+++ b/docs/content/en/configuration/first-factor/introduction.md
@@ -41,8 +41,15 @@ This section describes the individual configuration options.
{{< confkey type="string,integer" syntax="duration" default="5 minutes" required="no">}}
-This setting controls the interval at which details are refreshed from the backend. Particularly useful for
-[LDAP](#ldap).
+_**Note:** when using the [File Provider](#file) this value has a default value of `always` as the cost in this
+scenario is basically not measurable, users can however override this setting by setting an explicit value._
+
+This setting controls the interval at which details are refreshed from the backend. The details refreshed in order of
+importance are the groups, email address, and display name. This is particularly useful for the [File Provider](#file)
+when [watch](file.md#watch) is enabled or generally with the [LDAP Provider](#ldap).
+
+In addition to the duration values this option accepts `always` and `disable` as values; where `always` will always
+refresh this value, and `disable` will never refresh the profile.
### password_reset
diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json
index 488bb51bc..e9ef6f49d 100644
--- a/docs/data/configkeys.json
+++ b/docs/data/configkeys.json
@@ -145,6 +145,16 @@
"env": "AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"
},
{
+ "path": "authentication_backend.refresh_interval",
+ "secret": false,
+ "env": "AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"
+ },
+ {
+ "path": "authentication_backend.refresh_interval",
+ "secret": false,
+ "env": "AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"
+ },
+ {
"path": "authentication_backend.file.path",
"secret": false,
"env": "AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"
diff --git a/docs/static/schemas/latest/json-schema/configuration.json b/docs/static/schemas/latest/json-schema/configuration.json
index 0ca18a2b2..389d98781 100644
--- a/docs/static/schemas/latest/json-schema/configuration.json
+++ b/docs/static/schemas/latest/json-schema/configuration.json
@@ -329,7 +329,7 @@
"description": "Allows configuration of the password reset behaviour"
},
"refresh_interval": {
- "type": "string",
+ "$ref": "#/$defs/RefreshIntervalDuration",
"title": "Refresh Interval",
"description": "How frequently the user details are refreshed from the backend"
},
@@ -2252,6 +2252,26 @@
"type": "object",
"description": "PrivacyPolicy is the privacy policy configuration."
},
+ "RefreshIntervalDuration": {
+ "oneOf": [
+ {
+ "type": "string",
+ "enum": [
+ "always",
+ "never"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$"
+ },
+ {
+ "type": "integer",
+ "description": "The duration in seconds"
+ }
+ ],
+ "default": "5 minutes"
+ },
"Regulation": {
"properties": {
"max_retries": {
diff --git a/docs/static/schemas/v4.38/json-schema/configuration.json b/docs/static/schemas/v4.38/json-schema/configuration.json
index 0ca18a2b2..389d98781 100644
--- a/docs/static/schemas/v4.38/json-schema/configuration.json
+++ b/docs/static/schemas/v4.38/json-schema/configuration.json
@@ -329,7 +329,7 @@
"description": "Allows configuration of the password reset behaviour"
},
"refresh_interval": {
- "type": "string",
+ "$ref": "#/$defs/RefreshIntervalDuration",
"title": "Refresh Interval",
"description": "How frequently the user details are refreshed from the backend"
},
@@ -2252,6 +2252,26 @@
"type": "object",
"description": "PrivacyPolicy is the privacy policy configuration."
},
+ "RefreshIntervalDuration": {
+ "oneOf": [
+ {
+ "type": "string",
+ "enum": [
+ "always",
+ "never"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$"
+ },
+ {
+ "type": "integer",
+ "description": "The duration in seconds"
+ }
+ ],
+ "default": "5 minutes"
+ },
"Regulation": {
"properties": {
"max_retries": {
diff --git a/internal/configuration/decode_hooks.go b/internal/configuration/decode_hooks.go
index 2a6c5ff93..745e27d9d 100644
--- a/internal/configuration/decode_hooks.go
+++ b/internal/configuration/decode_hooks.go
@@ -113,9 +113,117 @@ func StringToURLHookFunc() mapstructure.DecodeHookFuncType {
}
}
+func DecodeTimeDuration(f, expectedType reflect.Type, prefixType string, data any) (result time.Duration, err error) {
+ e := reflect.TypeOf(time.Duration(0))
+
+ switch {
+ case f.Kind() == reflect.String:
+ dataStr := data.(string)
+
+ if result, err = utils.ParseDurationString(dataStr); err != nil {
+ return time.Duration(0), fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType, err)
+ }
+ case f.Kind() == reflect.Int:
+ seconds := data.(int)
+
+ result = time.Second * time.Duration(seconds)
+ case f.Kind() == reflect.Int8:
+ seconds := data.(int8)
+
+ result = time.Second * time.Duration(seconds)
+ case f.Kind() == reflect.Int16:
+ seconds := data.(int16)
+
+ result = time.Second * time.Duration(seconds)
+ case f.Kind() == reflect.Int32:
+ seconds := data.(int32)
+
+ result = time.Second * time.Duration(seconds)
+ case f.Kind() == reflect.Float64:
+ fseconds := data.(float64)
+
+ if fseconds > durationMax.Seconds() {
+ result = durationMax
+ } else {
+ seconds, _ := strconv.Atoi(fmt.Sprintf("%.0f", fseconds))
+
+ result = time.Second * time.Duration(seconds)
+ }
+ case f == e:
+ result = data.(time.Duration)
+ case f.Kind() == reflect.Int64:
+ seconds := data.(int64)
+
+ result = time.Second * time.Duration(seconds)
+ }
+
+ return result, nil
+}
+
+// ToRefreshIntervalDurationHookFunc converts string and integer types to a schema.RefreshIntervalDuration.
+func ToRefreshIntervalDurationHookFunc() mapstructure.DecodeHookFuncType {
+ return func(f reflect.Type, t reflect.Type, data any) (value any, err error) {
+ var ptr bool
+
+ switch f.Kind() {
+ case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float64:
+ // We only allow string and integer from kinds to match.
+ break
+ default:
+ return data, nil
+ }
+
+ prefixType := ""
+
+ if t.Kind() == reflect.Ptr {
+ ptr = true
+ prefixType = "*"
+ }
+
+ expectedType := reflect.TypeOf(schema.RefreshIntervalDuration{})
+
+ if ptr && t.Elem() != expectedType {
+ return data, nil
+ } else if !ptr && t != expectedType {
+ return data, nil
+ }
+
+ var (
+ result schema.RefreshIntervalDuration
+ decoded bool
+ )
+
+ if f.Kind() == reflect.String {
+ dataStr, ok := data.(string)
+ if ok {
+ switch dataStr {
+ case schema.ProfileRefreshAlways:
+ result, decoded = schema.NewRefreshIntervalDurationAlways(), true
+ case schema.ProfileRefreshDisabled:
+ result, decoded = schema.NewRefreshIntervalDurationNever(), true
+ }
+ }
+ }
+
+ if !decoded {
+ var resultv time.Duration
+
+ if resultv, err = DecodeTimeDuration(f, expectedType, prefixType, data); err != nil {
+ return nil, err
+ }
+
+ result = schema.NewRefreshIntervalDuration(resultv)
+ }
+
+ if ptr {
+ return &result, nil
+ }
+
+ return result, nil
+ }
+}
+
// ToTimeDurationHookFunc converts string and integer types to a time.Duration.
-//
-//nolint:gocyclo // Function is necessarily complex though flows well due to switch statement usage.
func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType {
return func(f reflect.Type, t reflect.Type, data any) (value any, err error) {
var (
@@ -146,45 +254,8 @@ func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType {
var result time.Duration
- switch {
- case f.Kind() == reflect.String:
- dataStr := data.(string)
-
- if result, err = utils.ParseDurationString(dataStr); err != nil {
- return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType, err)
- }
- case f.Kind() == reflect.Int:
- seconds := data.(int)
-
- result = time.Second * time.Duration(seconds)
- case f.Kind() == reflect.Int8:
- seconds := data.(int8)
-
- result = time.Second * time.Duration(seconds)
- case f.Kind() == reflect.Int16:
- seconds := data.(int16)
-
- result = time.Second * time.Duration(seconds)
- case f.Kind() == reflect.Int32:
- seconds := data.(int32)
-
- result = time.Second * time.Duration(seconds)
- case f.Kind() == reflect.Float64:
- fseconds := data.(float64)
-
- if fseconds > durationMax.Seconds() {
- result = durationMax
- } else {
- seconds, _ := strconv.Atoi(fmt.Sprintf("%.0f", fseconds))
-
- result = time.Second * time.Duration(seconds)
- }
- case f == expectedType:
- result = data.(time.Duration)
- case f.Kind() == reflect.Int64:
- seconds := data.(int64)
-
- result = time.Second * time.Duration(seconds)
+ if result, err = DecodeTimeDuration(f, expectedType, prefixType, data); err != nil {
+ return nil, err
}
if ptr {
diff --git a/internal/configuration/decode_hooks_test.go b/internal/configuration/decode_hooks_test.go
index 2cdaf96ee..dc879d0ea 100644
--- a/internal/configuration/decode_hooks_test.go
+++ b/internal/configuration/decode_hooks_test.go
@@ -559,6 +559,277 @@ func TestToTimeDurationHookFuncPointer(t *testing.T) {
}
}
+func TestToRefreshIntervalDurationHookFunc(t *testing.T) {
+ testCases := []struct {
+ desc string
+ have any
+ want any
+ err string
+ decode bool
+ }{
+ {
+ desc: "ShouldDecodeFourtyFiveSeconds",
+ have: "45s",
+ want: schema.NewRefreshIntervalDuration(time.Second * 45),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeOneMinute",
+ have: "1m",
+ want: schema.NewRefreshIntervalDuration(time.Minute),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeTwoHours",
+ have: "2h",
+ want: schema.NewRefreshIntervalDuration(time.Hour * 2),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeThreeDays",
+ have: "3d",
+ want: schema.NewRefreshIntervalDuration(time.Hour * 24 * 3),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeFourWeeks",
+ have: "4w",
+ want: schema.NewRefreshIntervalDuration(time.Hour * 24 * 7 * 4),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeFiveMonths",
+ have: "5M",
+ want: schema.NewRefreshIntervalDuration(time.Hour * 24 * 30 * 5),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeSixYears",
+ have: "6y",
+ want: schema.NewRefreshIntervalDuration(time.Hour * 24 * 365 * 6),
+ decode: true,
+ },
+ {
+ desc: "ShouldNotDecodeInvalidString",
+ have: "abc",
+ want: schema.RefreshIntervalDuration{},
+ err: "could not decode 'abc' to a schema.RefreshIntervalDuration: could not parse 'abc' as a duration",
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeIntToSeconds",
+ have: 60,
+ want: schema.NewRefreshIntervalDuration(time.Second * 60),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeInt8ToSeconds",
+ have: int8(90),
+ want: schema.NewRefreshIntervalDuration(time.Second * 90),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeInt16ToSeconds",
+ have: int16(90),
+ want: schema.NewRefreshIntervalDuration(time.Second * 90),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeInt32ToSeconds",
+ have: int32(90),
+ want: schema.NewRefreshIntervalDuration(time.Second * 90),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeFloat64ToSeconds",
+ have: float64(90),
+ want: schema.NewRefreshIntervalDuration(time.Second * 90),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeFloat64ToSeconds",
+ have: math.MaxFloat64,
+ want: schema.NewRefreshIntervalDuration(time.Duration(math.MaxInt64)),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeInt64ToSeconds",
+ have: int64(120),
+ want: schema.NewRefreshIntervalDuration(time.Second * 120),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeTimeDuration",
+ have: time.Second * 30,
+ want: schema.NewRefreshIntervalDuration(time.Second * 30),
+ decode: true,
+ },
+ {
+ desc: "ShouldNotDecodeToString",
+ have: int64(30),
+ want: "",
+ decode: false,
+ },
+ {
+ desc: "ShouldDecodeFromIntZero",
+ have: 0,
+ want: schema.NewRefreshIntervalDuration(time.Duration(0)),
+ decode: true,
+ },
+ {
+ desc: "ShouldSkipParsingBoolean",
+ have: true,
+ want: schema.RefreshIntervalDuration{},
+ decode: false,
+ },
+ {
+ desc: "ShouldNotDecodeFromBool",
+ have: true,
+ want: true,
+ },
+ }
+
+ hook := configuration.ToRefreshIntervalDurationHookFunc()
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ result, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.want), tc.have)
+ switch {
+ case !tc.decode:
+ assert.NoError(t, err)
+ assert.Equal(t, tc.have, result)
+ case tc.err == "":
+ assert.NoError(t, err)
+ require.Equal(t, tc.want, result)
+ default:
+ assert.EqualError(t, err, tc.err)
+ assert.Nil(t, result)
+ }
+ })
+ }
+}
+
+func TestTestToRefreshIntervalDurationHookFuncPointer(t *testing.T) {
+ testCases := []struct {
+ desc string
+ have any
+ want any
+ err string
+ decode bool
+ }{
+ {
+ desc: "ShouldDecodeFourtyFiveSeconds",
+ have: "45s",
+ want: testRefreshIntervalDurationPtr(time.Second * 45),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeOneMinute",
+ have: "1m",
+ want: testRefreshIntervalDurationPtr(time.Minute),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeTwoHours",
+ have: "2h",
+ want: testRefreshIntervalDurationPtr(time.Hour * 2),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeThreeDays",
+ have: "3d",
+ want: testRefreshIntervalDurationPtr(time.Hour * 24 * 3),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeFourWeeks",
+ have: "4w",
+ want: testRefreshIntervalDurationPtr(time.Hour * 24 * 7 * 4),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeFiveMonths",
+ have: "5M",
+ want: testRefreshIntervalDurationPtr(time.Hour * 24 * 30 * 5),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeSixYears",
+ have: "6y",
+ want: testRefreshIntervalDurationPtr(time.Hour * 24 * 365 * 6),
+ decode: true,
+ },
+ {
+ desc: "ShouldNotDecodeInvalidString",
+ have: "abc",
+ want: testRefreshIntervalDurationPtr(time.Duration(0)),
+ err: "could not decode 'abc' to a *schema.RefreshIntervalDuration: could not parse 'abc' as a duration",
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeIntToSeconds",
+ have: 60,
+ want: testRefreshIntervalDurationPtr(time.Second * 60),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeInt32ToSeconds",
+ have: int32(90),
+ want: testRefreshIntervalDurationPtr(time.Second * 90),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeInt64ToSeconds",
+ have: int64(120),
+ want: testRefreshIntervalDurationPtr(time.Second * 120),
+ decode: true,
+ },
+ {
+ desc: "ShouldDecodeTimeDuration",
+ have: time.Second * 30,
+ want: testRefreshIntervalDurationPtr(time.Second * 30),
+ decode: true,
+ },
+ {
+ desc: "ShouldNotDecodeToString",
+ have: int64(30),
+ want: &testString,
+ decode: false,
+ },
+ {
+ desc: "ShouldDecodeFromIntZero",
+ have: 0,
+ want: testRefreshIntervalDurationPtr(time.Duration(0)),
+ decode: true,
+ },
+ {
+ desc: "ShouldNotDecodeFromBool",
+ have: true,
+ want: &testTrue,
+ decode: false,
+ },
+ }
+
+ hook := configuration.ToRefreshIntervalDurationHookFunc()
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ result, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.want), tc.have)
+ switch {
+ case !tc.decode:
+ assert.NoError(t, err)
+ assert.Equal(t, tc.have, result)
+ case tc.err == "":
+ assert.NoError(t, err)
+ require.Equal(t, tc.want, result)
+ default:
+ assert.EqualError(t, err, tc.err)
+ assert.Nil(t, result)
+ }
+ })
+ }
+}
+
func TestStringToRegexpFunc(t *testing.T) {
testCases := []struct {
desc string
@@ -1801,6 +2072,12 @@ func testTimeDurationPtr(t time.Duration) *time.Duration {
return &t
}
+func testRefreshIntervalDurationPtr(t time.Duration) *schema.RefreshIntervalDuration {
+ x := schema.NewRefreshIntervalDuration(t)
+
+ return &x
+}
+
var (
testTrue = true
testZero int32
diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go
index b6dcaffff..01bfba5cf 100644
--- a/internal/configuration/provider.go
+++ b/internal/configuration/provider.go
@@ -69,6 +69,7 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o any)
StringToTLSVersionHookFunc(),
StringToPasswordDigestHookFunc(),
ToTimeDurationHookFunc(),
+ ToRefreshIntervalDurationHookFunc(),
),
Metadata: nil,
Result: o,
diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go
index 771e47316..b0714baaf 100644
--- a/internal/configuration/provider_test.go
+++ b/internal/configuration/provider_test.go
@@ -86,6 +86,24 @@ func TestShouldHaveNotifier(t *testing.T) {
assert.NotNil(t, config.Notifier)
}
+func TestShouldConfigureRefreshIntervalDisable(t *testing.T) {
+ testSetEnv(t, "SESSION_SECRET", "abc")
+ testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
+ testSetEnv(t, "JWT_SECRET", "abc")
+ testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
+
+ val := schema.NewStructValidator()
+ _, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
+
+ assert.NoError(t, err)
+ assert.Len(t, val.Errors(), 0)
+ assert.Len(t, val.Warnings(), 0)
+
+ require.NotNil(t, config.AuthenticationBackend.RefreshInterval)
+ assert.True(t, config.AuthenticationBackend.RefreshInterval.Never())
+ assert.False(t, config.AuthenticationBackend.RefreshInterval.Always())
+}
+
func TestShouldParseLargeIntegerDurations(t *testing.T) {
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.durations.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@@ -96,6 +114,11 @@ func TestShouldParseLargeIntegerDurations(t *testing.T) {
assert.Equal(t, durationMax, config.Regulation.FindTime)
assert.Equal(t, time.Second*1000, config.Regulation.BanTime)
+
+ require.NotNil(t, config.AuthenticationBackend.RefreshInterval)
+ assert.Equal(t, false, config.AuthenticationBackend.RefreshInterval.Always())
+ assert.Equal(t, false, config.AuthenticationBackend.RefreshInterval.Never())
+ assert.Equal(t, time.Minute*5, config.AuthenticationBackend.RefreshInterval.Value())
}
func TestShouldValidateConfigurationWithEnv(t *testing.T) {
@@ -671,6 +694,41 @@ func TestShouldDecodeSMTPSenderWithName(t *testing.T) {
assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMe)
}
+func TestShouldConfigureRefreshIntervalAlways(t *testing.T) {
+ val := schema.NewStructValidator()
+ keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_alt.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
+
+ assert.NoError(t, err)
+
+ validator.ValidateKeys(keys, DefaultEnvPrefix, val)
+
+ assert.Len(t, val.Errors(), 0)
+ assert.Len(t, val.Warnings(), 0)
+
+ require.NotNil(t, config.AuthenticationBackend.RefreshInterval)
+ assert.False(t, config.AuthenticationBackend.RefreshInterval.Never())
+ assert.True(t, config.AuthenticationBackend.RefreshInterval.Always())
+}
+
+func TestShouldConfigureRefreshIntervalDefault(t *testing.T) {
+ val := schema.NewStructValidator()
+ keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.no-refresh.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
+
+ assert.NoError(t, err)
+
+ validator.ValidateKeys(keys, DefaultEnvPrefix, val)
+
+ assert.Len(t, val.Errors(), 0)
+ assert.Len(t, val.Warnings(), 0)
+
+ validator.ValidateAuthenticationBackend(&config.AuthenticationBackend, val)
+
+ require.NotNil(t, config.AuthenticationBackend.RefreshInterval)
+ assert.False(t, config.AuthenticationBackend.RefreshInterval.Always())
+ assert.False(t, config.AuthenticationBackend.RefreshInterval.Never())
+ assert.Equal(t, time.Minute*5, config.AuthenticationBackend.RefreshInterval.Value())
+}
+
func TestShouldParseRegex(t *testing.T) {
val := schema.NewStructValidator()
keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_domain_regex.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go
index 6232ae640..56c1f8f39 100644
--- a/internal/configuration/schema/authentication.go
+++ b/internal/configuration/schema/authentication.go
@@ -10,7 +10,7 @@ import (
type AuthenticationBackend struct {
PasswordReset AuthenticationBackendPasswordReset `koanf:"password_reset" json:"password_reset" jsonschema:"title=Password Reset" jsonschema_description:"Allows configuration of the password reset behaviour"`
- RefreshInterval string `koanf:"refresh_interval" json:"refresh_interval" jsonschema:"title=Refresh Interval" jsonschema_description:"How frequently the user details are refreshed from the backend"`
+ RefreshInterval RefreshIntervalDuration `koanf:"refresh_interval" json:"refresh_interval" jsonschema:"default=5 minutes,title=Refresh Interval" jsonschema_description:"How frequently the user details are refreshed from the backend"`
// The file authentication backend configuration.
File *AuthenticationBackendFile `koanf:"file" json:"file" jsonschema:"title=File Backend" jsonschema_description:"The file authentication backend configuration"`
@@ -141,6 +141,10 @@ type AuthenticationBackendLDAPAttributes struct {
GroupName string `koanf:"group_name" json:"group_name" jsonschema:"title=Attribute: Group Name" jsonschema_description:"The directory server attribute which contains the group name for all groups"`
}
+var DefaultAuthenticationBackendConfig = AuthenticationBackend{
+ RefreshInterval: NewRefreshIntervalDuration(time.Minute * 5),
+}
+
// DefaultPasswordConfig represents the default configuration related to Argon2id hashing.
var DefaultPasswordConfig = AuthenticationBackendFilePassword{
Algorithm: argon2,
diff --git a/internal/configuration/schema/const.go b/internal/configuration/schema/const.go
index 2ffde0e2c..52c92d591 100644
--- a/internal/configuration/schema/const.go
+++ b/internal/configuration/schema/const.go
@@ -44,18 +44,15 @@ const (
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
var ErrTLSVersionNotSupported = errors.New("supplied tls version isn't supported")
-// ProfileRefreshDisabled represents a Value for refresh_interval that disables the check entirely.
-const ProfileRefreshDisabled = "disable"
-
const (
// ProfileRefreshAlways represents a value for refresh_interval that's the same as 0ms.
ProfileRefreshAlways = "always"
- // RefreshIntervalDefault represents the default value of refresh_interval.
- RefreshIntervalDefault = "5m"
+ // ProfileRefreshDisabled represents a Value for refresh_interval that disables the check entirely.
+ ProfileRefreshDisabled = "disable"
- // RefreshIntervalAlways represents the duration value refresh interval should have if set to always.
- RefreshIntervalAlways = 0 * time.Millisecond
+ // RefreshIntervalDefault represents the default value of refresh_interval.
+ RefreshIntervalDefault = time.Minute * 5
)
const (
diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go
index d01e7615f..a71168091 100644
--- a/internal/configuration/schema/keys.go
+++ b/internal/configuration/schema/keys.go
@@ -115,6 +115,8 @@ var Keys = []string{
"authentication_backend.password_reset.disable",
"authentication_backend.password_reset.custom_url",
"authentication_backend.refresh_interval",
+ "authentication_backend.refresh_interval",
+ "authentication_backend.refresh_interval",
"authentication_backend.file.path",
"authentication_backend.file.watch",
"authentication_backend.file.password.algorithm",
diff --git a/internal/configuration/schema/types.go b/internal/configuration/schema/types.go
index 7c4969a97..7f0497c51 100644
--- a/internal/configuration/schema/types.go
+++ b/internal/configuration/schema/types.go
@@ -19,7 +19,7 @@ import (
"github.com/go-crypt/crypt/algorithm"
"github.com/go-crypt/crypt/algorithm/plaintext"
"github.com/valyala/fasthttp"
- yaml "gopkg.in/yaml.v3"
+ "gopkg.in/yaml.v3"
)
var cdecoder algorithm.DecoderRegister
@@ -403,6 +403,75 @@ func (c *X509CertificateChain) Validate() (err error) {
return nil
}
+// NewRefreshIntervalDuration returns a RefreshIntervalDuration given a time.Duration.
+func NewRefreshIntervalDuration(value time.Duration) RefreshIntervalDuration {
+ return RefreshIntervalDuration{value: value, valid: true}
+}
+
+// NewRefreshIntervalDurationAlways returns a RefreshIntervalDuration with an always value.
+func NewRefreshIntervalDurationAlways() RefreshIntervalDuration {
+ return RefreshIntervalDuration{valid: true, always: true}
+}
+
+// NewRefreshIntervalDurationNever returns a RefreshIntervalDuration with a never value.
+func NewRefreshIntervalDurationNever() RefreshIntervalDuration {
+ return RefreshIntervalDuration{valid: true, never: true}
+}
+
+// RefreshIntervalDuration is a special time.Duration for the refresh interval.
+type RefreshIntervalDuration struct {
+ value time.Duration
+ valid bool
+ always bool
+ never bool
+}
+
+// Valid returns true if the value was correctly newed up.
+func (d RefreshIntervalDuration) Valid() bool {
+ return d.valid
+}
+
+// Update returns true if the session could require updates.
+func (d RefreshIntervalDuration) Update() bool {
+ return !d.never && !d.always
+}
+
+// Always returns true if the interval is always.
+func (d RefreshIntervalDuration) Always() bool {
+ return d.always
+}
+
+// Never returns true if the interval is never.
+func (d RefreshIntervalDuration) Never() bool {
+ return d.never
+}
+
+// Value returns the time.Duration.
+func (d RefreshIntervalDuration) Value() time.Duration {
+ return d.value
+}
+
+// JSONSchema provides the json-schema formatting.
+func (RefreshIntervalDuration) JSONSchema() *jsonschema.Schema {
+ return &jsonschema.Schema{
+ Default: "5 minutes",
+ OneOf: []*jsonschema.Schema{
+ {
+ Type: jsonschema.TypeString,
+ Enum: []any{"always", "never"},
+ },
+ {
+ Type: jsonschema.TypeString,
+ Pattern: `^\d+\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\s*\d+\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$`,
+ },
+ {
+ Type: jsonschema.TypeInteger,
+ Description: "The duration in seconds",
+ },
+ },
+ }
+}
+
type AccessControlRuleNetworks []string
func (AccessControlRuleNetworks) JSONSchema() *jsonschema.Schema {
diff --git a/internal/configuration/test_resources/config.durations.yml b/internal/configuration/test_resources/config.durations.yml
index e6c3fe89e..f7c32ec5f 100644
--- a/internal/configuration/test_resources/config.durations.yml
+++ b/internal/configuration/test_resources/config.durations.yml
@@ -34,6 +34,7 @@ duo_api:
integration_key: 'ABCDEF'
authentication_backend:
+ refresh_interval: '5m'
ldap:
address: 'ldap://127.0.0.1'
tls:
diff --git a/internal/configuration/test_resources/config.no-refresh.yml b/internal/configuration/test_resources/config.no-refresh.yml
new file mode 100644
index 000000000..467b46351
--- /dev/null
+++ b/internal/configuration/test_resources/config.no-refresh.yml
@@ -0,0 +1,172 @@
+---
+default_redirection_url: 'https://home.example.com:8080/'
+
+server:
+ address: 'tcp://127.0.0.1:9091'
+ endpoints:
+ authz:
+ forward-auth:
+ implementation: 'ForwardAuth'
+ authn_strategies:
+ - name: 'HeaderProxyAuthorization'
+ - name: 'CookieSession'
+ ext-authz:
+ implementation: 'ExtAuthz'
+ authn_strategies:
+ - name: 'HeaderProxyAuthorization'
+ - name: 'CookieSession'
+ auth-request:
+ implementation: 'AuthRequest'
+ authn_strategies:
+ - name: 'HeaderAuthRequestProxyAuthorization'
+ - name: 'CookieSession'
+ legacy:
+ implementation: 'Legacy'
+
+log:
+ level: 'debug'
+
+totp:
+ issuer: 'authelia.com'
+
+duo_api:
+ hostname: 'api-123456789.example.com'
+ integration_key: 'ABCDEF'
+
+authentication_backend:
+ ldap:
+ address: 'ldap://127.0.0.1'
+ tls:
+ private_key: |
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEpAIBAAKCAQEA6z1LOg1ZCqb0lytXWZ+MRBpMHEXOoTOLYgfZXt1IYyE3Z758
+ cyalk0NYQhY5cZDsXPYWPvAHiPMUxutWkoxFwby56S+AbIMa3/Is+ILrHRJs8Exn
+ ZkpyrYFxPX12app2kErdmAkHSx0Z5/kuXiz96PHs8S8/ZbyZolLHzdfLtSzjvRm5
+ Zue5iFzsf19NJz5CIBfv8g5lRwtE8wNJoRSpn1xq7fqfuA0weDNFPzjlNWRLy6aa
+ rK7qJexRkmkCs4sLgyl+9NODYJpvmN8E1yhyC27E0joI6rBFVW7Ihv+cSPCdDzGp
+ EWe81x3AeqAa3mjVqkiq4u4Z2i8JDgBaPboqJwIDAQABAoIBAAFdLZ58jVOefDSU
+ L8F5R1rtvBs93GDa56f926jNJ6pLewLC+/2+757W+SAI+PRLntM7Kg3bXm/Q2QH+
+ Q1Y+MflZmspbWCdI61L5GIGoYKyeers59i+FpvySj5GHtLQRiTZ0+Kv1AXHSDWBm
+ 9XneUOqU3IbZe0ifu1RRno72/VtjkGXbW8Mkkw+ohyGbIeTx/0/JQ6sSNZTT3Vk7
+ 8i4IXptq3HSF0/vqZuah8rShoeNq72pD1YLM9YPdL5by1QkDLnqATDiCpLBTCaNV
+ I8sqYEun+HYbQzBj8ZACG2JVZpEEidONWQHw5BPWO95DSZYrVnEkuCqeH+u5vYt7
+ CHuJ3AECgYEA+W3v5z+j91w1VPHS0VB3SCDMouycAMIUnJPAbt+0LPP0scUFsBGE
+ hPAKddC54pmMZRQ2KIwBKiyWfCrJ8Xz8Yogn7fJgmwTHidJBr2WQpIEkNGlK3Dzi
+ jXL2sh0yC7sHvn0DqiQ79l/e7yRbSnv2wrTJEczOOH2haD7/tBRyCYECgYEA8W+q
+ E9YyGvEltnPFaOxofNZ8LHVcZSsQI5b6fc0iE7fjxFqeXPXEwGSOTwqQLQRiHn9b
+ CfPmIG4Vhyq0otVmlPvUnfBZ2OK+tl5X2/mQFO3ROMdvpi0KYa994uqfJdSTaqLn
+ jjoKFB906UFHnDQDLZUNiV1WwnkTglgLc+xrd6cCgYEAqqthyv6NyBTM3Tm2gcio
+ Ra9Dtntl51LlXZnvwy3IkDXBCd6BHM9vuLKyxZiziGx+Vy90O1xI872cnot8sINQ
+ Am+dur/tAEVN72zxyv0Y8qb2yfH96iKy9gxi5s75TnOEQgAygLnYWaWR2lorKRUX
+ bHTdXBOiS58S0UzCFEslGIECgYBqkO4SKWYeTDhoKvuEj2yjRYyzlu28XeCWxOo1
+ otiauX0YSyNBRt2cSgYiTzhKFng0m+QUJYp63/wymB/5C5Zmxi0XtWIDADpLhqLj
+ HmmBQ2Mo26alQ5YkffBju0mZyhVzaQop1eZi8WuKFV1FThPlB7hc3E0SM5zv2Grd
+ tQnOWwKBgQC40yZY0PcjuILhy+sIc0Wvh7LUA7taSdTye149kRvbvsCDN7Jh75lM
+ USjhLXY0Nld2zBm9r8wMb81mXH29uvD+tDqqsICvyuKlA/tyzXR+QTr7dCVKVwu0
+ 1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw==
+ -----END RSA PRIVATE KEY-----
+ base_dn: 'dc=example,dc=com'
+ additional_users_dn: 'ou=users'
+ users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))'
+ additional_groups_dn: 'ou=groups'
+ groups_filter: '(&(member={dn})(objectClass=groupOfNames))'
+ user: 'cn=admin,dc=example,dc=com'
+ attributes:
+ username: 'uid'
+ group_name: 'cn'
+ mail: 'mail'
+
+access_control:
+ default_policy: 'deny'
+
+ rules:
+ # Rules applied to everyone
+ - domain: 'public.example.com'
+ policy: 'bypass'
+
+ - domain: 'secure.example.com'
+ policy: 'one_factor'
+ # Network based rule, if not provided any network matches.
+ networks:
+ - '192.168.1.0/24'
+ - domain: 'secure.example.com'
+ policy: 'two_factor'
+
+ - domain: ['singlefactor.example.com', 'onefactor.example.com']
+ policy: 'one_factor'
+
+ # Rules applied to 'admins' group
+ - domain: 'mx2.mail.example.com'
+ subject: 'group:admins'
+ policy: 'deny'
+ - domain: '*.example.com'
+ subject: 'group:admins'
+ policy: 'two_factor'
+
+ # Rules applied to 'dev' group
+ - domain: 'dev.example.com'
+ resources:
+ - '^/groups/dev/.*$'
+ subject: 'group:dev'
+ policy: 'two_factor'
+
+ # Rules applied to user 'john'
+ - domain: 'dev.example.com'
+ resources:
+ - '^/users/john/.*$'
+ subject: 'user:john'
+ policy: 'two_factor'
+
+ # Rules applied to 'dev' group and user 'john'
+ - domain: 'dev.example.com'
+ resources:
+ - '^/deny-all.*$'
+ subject: ['group:dev', 'user:john']
+ policy: 'deny'
+
+ # Rules applied to user 'harry'
+ - domain: 'dev.example.com'
+ resources:
+ - '^/users/harry/.*$'
+ subject: 'user:harry'
+ policy: 'two_factor'
+
+ # Rules applied to user 'bob'
+ - domain: '*.mail.example.com'
+ subject: 'user:bob'
+ policy: 'two_factor'
+ - domain: 'dev.example.com'
+ resources:
+ - '^/users/bob/.*$'
+ subject: 'user:bob'
+ policy: 'two_factor'
+
+session:
+ name: 'authelia_session'
+ expiration: '1h' # 1 hour
+ inactivity: '5m' # 5 minutes
+ domain: 'example.com'
+ redis:
+ host: '127.0.0.1'
+ port: 6379
+ high_availability:
+ sentinel_name: 'test'
+
+regulation:
+ max_retries: 3
+ find_time: '2m'
+ ban_time: '5m'
+
+storage:
+ mysql:
+ address: 'tcp://127.0.0.1:3306'
+ database: 'authelia'
+ username: 'authelia'
+
+notifier:
+ smtp:
+ address: 'smtp://127.0.0.1:1025'
+ username: 'test'
+ sender: 'admin@example.com'
+ disable_require_tls: true
+...
diff --git a/internal/configuration/test_resources/config.yml b/internal/configuration/test_resources/config.yml
index 467b46351..ee996f594 100644
--- a/internal/configuration/test_resources/config.yml
+++ b/internal/configuration/test_resources/config.yml
@@ -34,6 +34,7 @@ duo_api:
integration_key: 'ABCDEF'
authentication_backend:
+ refresh_interval: 'disable'
ldap:
address: 'ldap://127.0.0.1'
tls:
diff --git a/internal/configuration/test_resources/config_alt.yml b/internal/configuration/test_resources/config_alt.yml
index 6595e4bbf..1d4813527 100644
--- a/internal/configuration/test_resources/config_alt.yml
+++ b/internal/configuration/test_resources/config_alt.yml
@@ -15,6 +15,7 @@ duo_api:
integration_key: 'ABCDEF'
authentication_backend:
+ refresh_interval: 'always'
ldap:
address: 'ldap://127.0.0.1'
base_dn: 'dc=example,dc=com'
diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go
index 8740e9e6d..b46caea2c 100644
--- a/internal/configuration/validator/authentication.go
+++ b/internal/configuration/validator/authentication.go
@@ -20,12 +20,11 @@ func ValidateAuthenticationBackend(config *schema.AuthenticationBackend, validat
validator.Push(fmt.Errorf(errFmtAuthBackendNotConfigured))
}
- if config.RefreshInterval == "" {
- config.RefreshInterval = schema.RefreshIntervalDefault
- } else {
- _, err := utils.ParseDurationString(config.RefreshInterval)
- if err != nil && config.RefreshInterval != schema.ProfileRefreshDisabled && config.RefreshInterval != schema.ProfileRefreshAlways {
- validator.Push(fmt.Errorf(errFmtAuthBackendRefreshInterval, config.RefreshInterval, err))
+ if !config.RefreshInterval.Valid() {
+ if config.File != nil && config.File.Watch {
+ config.RefreshInterval = schema.NewRefreshIntervalDurationAlways()
+ } else {
+ config.RefreshInterval = schema.NewRefreshIntervalDuration(schema.RefreshIntervalDefault)
}
}
diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go
index f06a53af1..1248de0cd 100644
--- a/internal/configuration/validator/authentication_test.go
+++ b/internal/configuration/validator/authentication_test.go
@@ -65,6 +65,19 @@ func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfigura
suite.Len(suite.validator.Errors(), 0)
}
+func (suite *FileBasedAuthenticationBackend) TestShouldValidateWatchDefaultResetInterval() {
+ suite.config.File.Watch = true
+
+ ValidateAuthenticationBackend(&suite.config, suite.validator)
+
+ suite.Len(suite.validator.Warnings(), 0)
+ suite.Len(suite.validator.Errors(), 0)
+
+ suite.True(suite.config.RefreshInterval.Valid())
+ suite.True(suite.config.RefreshInterval.Always())
+ suite.False(suite.config.RefreshInterval.Never())
+}
+
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() {
suite.config.File.Path = ""
@@ -751,17 +764,6 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAt
suite.Len(suite.validator.Errors(), 0)
}
-func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() {
- suite.config.RefreshInterval = "blah"
-
- ValidateAuthenticationBackend(&suite.config, suite.validator)
-
- suite.Len(suite.validator.Warnings(), 0)
- suite.Require().Len(suite.validator.Errors(), 1)
-
- suite.EqualError(suite.validator.Errors()[0], "authentication_backend: option 'refresh_interval' is configured to 'blah' but it must be either in duration common syntax or one of 'disable', or 'always': could not parse 'blah' as a duration")
-}
-
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultImplementation() {
ValidateAuthenticationBackend(&suite.config, suite.validator)
@@ -820,7 +822,10 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval
suite.Len(suite.validator.Warnings(), 0)
suite.Len(suite.validator.Errors(), 0)
- suite.Equal("5m", suite.config.RefreshInterval)
+ suite.Require().NotNil(suite.config.RefreshInterval)
+ suite.False(suite.config.RefreshInterval.Always())
+ suite.False(suite.config.RefreshInterval.Never())
+ suite.Equal(time.Minute*5, suite.config.RefreshInterval.Value())
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
diff --git a/internal/handlers/handler_authz_authn.go b/internal/handlers/handler_authz_authn.go
index ef13c9bd6..853e98a5c 100644
--- a/internal/handlers/handler_authz_authn.go
+++ b/internal/handlers/handler_authz_authn.go
@@ -20,14 +20,9 @@ import (
)
// NewCookieSessionAuthnStrategy creates a new CookieSessionAuthnStrategy.
-func NewCookieSessionAuthnStrategy(refreshInterval time.Duration) *CookieSessionAuthnStrategy {
- if refreshInterval < time.Second*0 {
- return &CookieSessionAuthnStrategy{}
- }
-
+func NewCookieSessionAuthnStrategy(refresh schema.RefreshIntervalDuration) *CookieSessionAuthnStrategy {
return &CookieSessionAuthnStrategy{
- refreshEnabled: true,
- refreshInterval: refreshInterval,
+ refresh: refresh,
}
}
@@ -75,8 +70,7 @@ func NewHeaderLegacyAuthnStrategy() *HeaderLegacyAuthnStrategy {
// CookieSessionAuthnStrategy is a session cookie AuthnStrategy.
type CookieSessionAuthnStrategy struct {
- refreshEnabled bool
- refreshInterval time.Duration
+ refresh schema.RefreshIntervalDuration
}
// Get returns the Authn information for this AuthnStrategy.
@@ -107,7 +101,7 @@ func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider
}
}
- if invalid := handleVerifyGETAuthnCookieValidate(ctx, provider, &userSession, s.refreshEnabled, s.refreshInterval); invalid {
+ if invalid := handleVerifyGETAuthnCookieValidate(ctx, provider, &userSession, s.refresh); invalid {
if err = ctx.DestroySession(); err != nil {
ctx.Logger.WithError(err).Errorf("Unable to destroy user session")
}
@@ -308,7 +302,7 @@ func (s *HeaderLegacyAuthnStrategy) HandleUnauthorized(ctx *middlewares.Authelia
handleAuthzUnauthorizedAuthorizationBasic(ctx, authn)
}
-func handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, profileRefreshEnabled bool, profileRefreshInterval time.Duration) (invalid bool) {
+func handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, refresh schema.RefreshIntervalDuration) (invalid bool) {
isAnonymous := userSession.Username == ""
if isAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
@@ -323,7 +317,7 @@ func handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider *
return true
}
- if invalid = handleVerifyGETAuthnCookieValidateUpdate(ctx, userSession, isAnonymous, profileRefreshEnabled, profileRefreshInterval); invalid {
+ if invalid = handleVerifyGETAuthnCookieValidateRefresh(ctx, userSession, isAnonymous, refresh); invalid {
return true
}
@@ -350,14 +344,14 @@ func handleVerifyGETAuthnCookieValidateInactivity(ctx *middlewares.AutheliaCtx,
return time.Unix(userSession.LastActivity, 0).Add(provider.Config.Inactivity).Before(ctx.Clock.Now())
}
-func handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isAnonymous, enabled bool, interval time.Duration) (invalid bool) {
- if !enabled || isAnonymous {
+func handleVerifyGETAuthnCookieValidateRefresh(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isAnonymous bool, refresh schema.RefreshIntervalDuration) (invalid bool) {
+ if refresh.Never() || isAnonymous {
return false
}
ctx.Logger.WithField("username", userSession.Username).Trace("Checking if we need check the authentication backend for an updated profile for user")
- if interval != schema.RefreshIntervalAlways && userSession.RefreshTTL.After(ctx.Clock.Now()) {
+ if !refresh.Always() && userSession.RefreshTTL.After(ctx.Clock.Now()) {
return false
}
@@ -387,8 +381,8 @@ func handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, user
diffEmails, diffGroups = utils.IsStringSlicesDifferent(userSession.Emails, details.Emails), utils.IsStringSlicesDifferent(userSession.Groups, details.Groups)
diffDisplayName = userSession.DisplayName != details.DisplayName
- if interval != schema.RefreshIntervalAlways {
- userSession.RefreshTTL = ctx.Clock.Now().Add(interval)
+ if !refresh.Always() {
+ userSession.RefreshTTL = ctx.Clock.Now().Add(refresh.Value())
}
if !diffEmails && !diffGroups && !diffDisplayName {
diff --git a/internal/handlers/handler_authz_builder.go b/internal/handlers/handler_authz_builder.go
index 365966ee9..43be45c0d 100644
--- a/internal/handlers/handler_authz_builder.go
+++ b/internal/handlers/handler_authz_builder.go
@@ -1,18 +1,15 @@
package handlers
import (
- "time"
-
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/configuration/schema"
- "github.com/authelia/authelia/v4/internal/utils"
)
// NewAuthzBuilder creates a new AuthzBuilder.
func NewAuthzBuilder() *AuthzBuilder {
return &AuthzBuilder{
- config: AuthzConfig{RefreshInterval: time.Second * -1},
+ config: AuthzConfig{RefreshInterval: schema.NewRefreshIntervalDurationAlways()},
}
}
@@ -62,19 +59,8 @@ func (b *AuthzBuilder) WithConfig(config *schema.Configuration) *AuthzBuilder {
return b
}
- var refreshInterval time.Duration
-
- switch config.AuthenticationBackend.RefreshInterval {
- case schema.ProfileRefreshDisabled:
- refreshInterval = time.Second * -1
- case schema.ProfileRefreshAlways:
- refreshInterval = time.Second * 0
- default:
- refreshInterval, _ = utils.ParseDurationString(config.AuthenticationBackend.RefreshInterval)
- }
-
b.config = AuthzConfig{
- RefreshInterval: refreshInterval,
+ RefreshInterval: config.AuthenticationBackend.RefreshInterval,
}
return b
diff --git a/internal/handlers/handler_authz_builder_test.go b/internal/handlers/handler_authz_builder_test.go
index e7b145d36..a2d6609e0 100644
--- a/internal/handlers/handler_authz_builder_test.go
+++ b/internal/handlers/handler_authz_builder_test.go
@@ -14,31 +14,31 @@ func TestAuthzBuilder_WithConfig(t *testing.T) {
builder.WithConfig(&schema.Configuration{
AuthenticationBackend: schema.AuthenticationBackend{
- RefreshInterval: "always",
+ RefreshInterval: schema.NewRefreshIntervalDurationAlways(),
},
})
- assert.Equal(t, time.Second*0, builder.config.RefreshInterval)
+ assert.Equal(t, schema.NewRefreshIntervalDurationAlways(), builder.config.RefreshInterval)
builder.WithConfig(&schema.Configuration{
AuthenticationBackend: schema.AuthenticationBackend{
- RefreshInterval: "disable",
+ RefreshInterval: schema.NewRefreshIntervalDurationNever(),
},
})
- assert.Equal(t, time.Second*-1, builder.config.RefreshInterval)
+ assert.Equal(t, schema.NewRefreshIntervalDurationNever(), builder.config.RefreshInterval)
builder.WithConfig(&schema.Configuration{
AuthenticationBackend: schema.AuthenticationBackend{
- RefreshInterval: "1m",
+ RefreshInterval: schema.NewRefreshIntervalDuration(time.Minute),
},
})
- assert.Equal(t, time.Minute, builder.config.RefreshInterval)
+ assert.Equal(t, schema.NewRefreshIntervalDuration(time.Minute), builder.config.RefreshInterval)
builder.WithConfig(nil)
- assert.Equal(t, time.Minute, builder.config.RefreshInterval)
+ assert.Equal(t, schema.NewRefreshIntervalDuration(time.Minute), builder.config.RefreshInterval)
}
func TestAuthzBuilder_WithEndpointConfig(t *testing.T) {
diff --git a/internal/handlers/handler_authz_test.go b/internal/handlers/handler_authz_test.go
index bd32a0228..19d670400 100644
--- a/internal/handlers/handler_authz_test.go
+++ b/internal/handlers/handler_authz_test.go
@@ -279,14 +279,16 @@ func (s *AuthzSuite) TestShouldNotFailOnMissingEmail() {
s.T().Skip()
}
- authz := s.Builder().Build()
-
mock := mocks.NewMockAutheliaCtx(s.T())
defer mock.Close()
+ mock.Ctx.Clock = &mock.Clock
+
mock.Clock.Set(time.Now())
+ authz := s.Builder().WithConfig(&mock.Ctx.Configuration).Build()
+
s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock)
targetURI := s.RequireParseRequestURI("https://bypass.example.com")
@@ -675,7 +677,7 @@ func (s *AuthzSuite) TestShouldDestroySessionWhenInactiveForTooLong() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
@@ -725,7 +727,7 @@ func (s *AuthzSuite) TestShouldNotDestroySessionWhenInactiveForTooLongRememberMe
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
@@ -775,7 +777,7 @@ func (s *AuthzSuite) TestShouldNotDestroySessionWhenNotInactiveForTooLong() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
@@ -826,7 +828,7 @@ func (s *AuthzSuite) TestShouldUpdateInactivityTimestampEvenWhenHittingForbidden
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
@@ -877,7 +879,7 @@ func (s *AuthzSuite) TestShouldNotRefreshUserDetailsFromBackendWhenRefreshDisabl
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(-1 * time.Second),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationNever()),
)
authz := builder.Build()
@@ -900,7 +902,7 @@ func (s *AuthzSuite) TestShouldNotRefreshUserDetailsFromBackendWhenRefreshDisabl
mock.Clock.Set(time.Now())
mock.Ctx.Clock = &mock.Clock
- mock.Ctx.Configuration.AuthenticationBackend.RefreshInterval = schema.ProfileRefreshDisabled
+ mock.Ctx.Configuration.AuthenticationBackend.RefreshInterval = schema.NewRefreshIntervalDurationNever()
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock)
@@ -970,7 +972,7 @@ func (s *AuthzSuite) TestShouldDestroySessionWhenUserDoesNotExist() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(5 * time.Minute),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(5 * time.Minute)),
)
authz := builder.Build()
@@ -1058,7 +1060,7 @@ func (s *AuthzSuite) TestShouldUpdateRemovedUserGroupsFromBackendAndDeny() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(5 * time.Minute),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(5 * time.Minute)),
)
authz := builder.Build()
@@ -1144,7 +1146,7 @@ func (s *AuthzSuite) TestShouldUpdateAddedUserGroupsFromBackendAndDeny() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(5 * time.Minute),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(5 * time.Minute)),
)
authz := builder.Build()
@@ -1229,7 +1231,7 @@ func (s *AuthzSuite) TestShouldCheckValidSessionUsernameHeaderAndReturn200() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
@@ -1282,7 +1284,7 @@ func (s *AuthzSuite) TestShouldCheckInvalidSessionUsernameHeaderAndReturn401AndD
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(5 * time.Minute)),
)
authz := builder.Build()
@@ -1353,7 +1355,7 @@ func (s *AuthzSuite) TestShouldNotRedirectRequestsForBypassACLWhenInactiveForToo
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
@@ -1431,7 +1433,7 @@ func (s *AuthzSuite) TestShouldFailToParsePortalURL() {
builder := s.Builder()
builder = builder.WithStrategies(
- NewCookieSessionAuthnStrategy(testInactivity),
+ NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDuration(testInactivity)),
)
authz := builder.Build()
diff --git a/internal/handlers/handler_authz_types.go b/internal/handlers/handler_authz_types.go
index ca2bfa7a1..6a2f5d370 100644
--- a/internal/handlers/handler_authz_types.go
+++ b/internal/handlers/handler_authz_types.go
@@ -2,10 +2,10 @@ package handlers
import (
"net/url"
- "time"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
)
@@ -75,7 +75,7 @@ type Authn struct {
// AuthzConfig represents the configuration elements of the Authz type.
type AuthzConfig struct {
- RefreshInterval time.Duration
+ RefreshInterval schema.RefreshIntervalDuration
// StatusCodeBadRequest is sent for configuration issues prior to performing authorization checks. It's set by the
// builder.
diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go
index e196f8b80..15882eed6 100644
--- a/internal/handlers/handler_firstfactor.go
+++ b/internal/handlers/handler_firstfactor.go
@@ -4,10 +4,8 @@ import (
"errors"
"time"
- "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation"
- "github.com/authelia/authelia/v4/internal/utils"
)
// FirstFactorPOST is the handler performing the first factory.
@@ -138,8 +136,8 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
userSession.SetOneFactor(ctx.Clock.Now(), userDetails, keepMeLoggedIn)
- if refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend); refresh {
- userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval)
+ if ctx.Configuration.AuthenticationBackend.RefreshInterval.Update() {
+ userSession.RefreshTTL = ctx.Clock.Now().Add(ctx.Configuration.AuthenticationBackend.RefreshInterval.Value())
}
if err = ctx.SaveSession(userSession); err != nil {
@@ -159,22 +157,3 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
}
}
}
-
-func getProfileRefreshSettings(cfg schema.AuthenticationBackend) (refresh bool, refreshInterval time.Duration) {
- if cfg.LDAP != nil {
- if cfg.RefreshInterval == schema.ProfileRefreshDisabled {
- refresh = false
- refreshInterval = 0
- } else {
- refresh = true
-
- if cfg.RefreshInterval != schema.ProfileRefreshAlways {
- refreshInterval, _ = utils.ParseDurationString(cfg.RefreshInterval)
- } else {
- refreshInterval = schema.RefreshIntervalAlways
- }
- }
- }
-
- return refresh, refreshInterval
-}
diff --git a/internal/mocks/authelia_ctx.go b/internal/mocks/authelia_ctx.go
index 54a775e08..d61a2add6 100644
--- a/internal/mocks/authelia_ctx.go
+++ b/internal/mocks/authelia_ctx.go
@@ -74,6 +74,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
},
}
+ config.AuthenticationBackend.RefreshInterval = schema.NewRefreshIntervalDuration(schema.RefreshIntervalDefault)
config.AccessControl = schema.AccessControl{
DefaultPolicy: "deny",
Rules: []schema.AccessControlRule{