diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2023-11-23 08:20:36 +1100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-23 08:20:36 +1100 | 
| commit | c49b973120c7fd755923a2b88afd794c7d320d6e (patch) | |
| tree | 5fcef6de1a85568eee3c67470f73b790b294dda8 | |
| parent | fa141929a39e546f3f3ca6bcbc7bd72c64e575c8 (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>
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{  | 
