diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index e310ebbeab..f8f6274fa2 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1,7 +1,7 @@ Log: Level: info # ZITADEL_LOG_LEVEL Formatter: - Format: text # ZITADEL_LOG_LEVEL + Format: text # ZITADEL_LOG_FORMATTER_FORMAT # Exposes metrics on /debug/metrics Metrics: @@ -29,7 +29,7 @@ Telemetry: # As long as Enabled is true, ZITADEL tries to send usage data to the configured Telemetry.Endpoints. # Data is projected by ZITADEL even if Enabled is false. # This means that switching this to true makes ZITADEL try to send past data. - Enabled: false + Enabled: false # ZITADEL_TELEMETRY_ENABLED # Push telemetry data to all these endpoints at least once using an HTTP POST request. # If one endpoint returns an unsuccessful response code or times out, # ZITADEL retries to push the data point to all configured endpoints until it succeeds. @@ -40,7 +40,9 @@ Telemetry: Endpoints: - https://httpbin.org/post # These headers are sent with every request to the configured endpoints. - Headers: + # Configure headers by environment variable using a JSON string with header values as arrays, like this: + # ZITADEL_TELEMETRY_HEADERS='{"header1": ["value1"], "header2": ["value2", "value3"]}' + Headers: # ZITADEL_TELEMETRY_HEADERS # single-value: "single-value" # multi-value: # - "multi-value-1" @@ -85,7 +87,7 @@ HTTP2HostHeader: ":authority" # ZITADEL_HTTP2HOSTHEADER # Header name of HTTP1 calls from which the instance will be matched HTTP1HostHeader: "host" # ZITADEL_HTTP1HOSTHEADER -WebAuthNName: ZITADEL # ZITADEL_WEBAUTHN_NAME +WebAuthNName: ZITADEL # ZITADEL_WEBAUTHNNAME Database: # ZITADEL manages three database connection pools. @@ -170,7 +172,7 @@ Machine: Enabled: true # ZITADEL_MACHINE_IDENTIFICATION_WEBHOOK_ENABLED Url: "http://metadata.google.internal/computeMetadata/v1/instance/id" # ZITADEL_MACHINE_IDENTIFICATION_WEBHOOK_URL Headers: - "Metadata-Flavor": "Google" # ZITADEL_MACHINE_IDENTIFICATION_WEBHOOK_HEADERS + "Metadata-Flavor": "Google" # # AWS EC2 IMDSv1 Configuration: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html # Webhook: @@ -205,7 +207,7 @@ Projections: # Time interval between scheduled projections RequeueEvery: 60s # ZITADEL_PROJECTIONS_REQUEUEEVERY # Time between retried database statements resulting from projected events - RetryFailedAfter: 1s # ZITADEL_PROJECTIONS_RETRYFAILED + RetryFailedAfter: 1s # ZITADEL_PROJECTIONS_RETRYFAILEDAFTER # Retried execution number of database statements resulting from projected events MaxFailureCount: 5 # ZITADEL_PROJECTIONS_MAXFAILURECOUNT # Limit of returned events per query @@ -378,28 +380,28 @@ Console: EncryptionKeys: DomainVerification: EncryptionKeyID: "domainVerificationKey" # ZITADEL_ENCRYPTIONKEYS_DOMAINVERIFICATION_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_DOMAINVERIFICATION_DECRYPTIONKEYIDS (comma separated list) IDPConfig: EncryptionKeyID: "idpConfigKey" # ZITADEL_ENCRYPTIONKEYS_IDPCONFIG_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_IDPCONFIG_DECRYPTIONKEYIDS (comma separated list) OIDC: EncryptionKeyID: "oidcKey" # ZITADEL_ENCRYPTIONKEYS_OIDC_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_OIDC_DECRYPTIONKEYIDS (comma separated list) SAML: EncryptionKeyID: "samlKey" # ZITADEL_ENCRYPTIONKEYS_SAML_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_SAML_DECRYPTIONKEYIDS (comma separated list) OTP: EncryptionKeyID: "otpKey" # ZITADEL_ENCRYPTIONKEYS_OTP_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_OTP_DECRYPTIONKEYIDS (comma separated list) SMS: EncryptionKeyID: "smsKey" # ZITADEL_ENCRYPTIONKEYS_SMS_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_SMS_DECRYPTIONKEYIDS (comma separated list) SMTP: EncryptionKeyID: "smtpKey" # ZITADEL_ENCRYPTIONKEYS_SMTP_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_SMTP_DECRYPTIONKEYIDS (comma separated list) User: EncryptionKeyID: "userKey" # ZITADEL_ENCRYPTIONKEYS_USER_ENCRYPTIONKEYID - DecryptionKeyIDs: + DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_USER_DECRYPTIONKEYIDS (comma separated list) CSRFCookieKeyID: "csrfCookieKey" # ZITADEL_ENCRYPTIONKEYS_CSRFCOOKIEKEYID UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID @@ -426,6 +428,8 @@ SystemAPIUsers: # - superuser2: # # If no memberships are specified, the user has a membership of type System with the role "SYSTEM_OWNER" # KeyData: # or you can directly embed it as base64 encoded value +# Configure the SystemAPIUsers by environment variable using JSON notation: +# ZITADEL_SYSTEMAPIUSERS='{"systemuser":{"Path":"/path/to/superuser/key.pem"},"systemuser2":{"KeyData":""}}' #TODO: remove as soon as possible SystemDefaults: @@ -457,13 +461,13 @@ SystemDefaults: # Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS # Hasher: - # Algorithm: "scrypt" - # Cost: 15 + # Algorithm: "scrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM + # Cost: 15 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST # Hasher: - # Algorithm: "pbkdf2" - # Rounds: 290000 - # Hash: "sha256" # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" + # Algorithm: "pbkdf2" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM + # Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ROUNDS + # Hash: "sha256" # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_HASH # Verifiers enable the possibility of verifying # passwords that are previously hashed using another @@ -509,7 +513,7 @@ SystemDefaults: Actions: HTTP: # Wildcard sub domains are currently unsupported - DenyList: + DenyList: # ZITADEL_ACTIONS_HTTP_DENYLIST (comma separated list) - localhost - "127.0.0.1" @@ -727,6 +731,9 @@ DefaultInstance: From: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM FromName: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROMNAME ReplyToAddress: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_REPLYTOADDRESS + # Configure the MessageTexts by environment variable using JSON notation: + # ZITADEL_DEFAULTINSTANCE_MESSAGETEXTS='[{"messageTextType": "InitCode", "title": "My custom title"},{"messageTextType": "PasswordReset", "greeting": "Hi there!"}]' + # Beware that if you configure the MessageTexts by environment variable, all the default MessageTexts are lost. MessageTexts: - MessageTextType: InitCode Language: de @@ -857,7 +864,9 @@ DefaultInstance: # "actions.all.runs.seconds" # The sum of all actions run durations in seconds - Items: + # Configure the Items by environment variable using JSON notation: + # ZITADEL_DEFAULTINSTANCE_QUOTAS_ITEMS='[{"unit": "requests.all.authenticated", "notifications": [{"percent": 100}]}]' + Items: # ZITADEL_DEFAULTINSTANCE_QUOTAS_ITEMS # - Unit: "requests.all.authenticated" # # From defines the starting time from which the current quota period is calculated. # # This is relevant for querying the current usage. @@ -884,6 +893,9 @@ DefaultInstance: AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION InternalAuthZ: + # Configure the RolePermissionMappings by environment variable using JSON notation: + # ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.read", "iam.write"]}]' + # Beware that if you configure the RolePermissionMappings by environment variable, all the default RolePermissionMappings are lost. RolePermissionMappings: - Role: "SYSTEM_OWNER" Permissions: diff --git a/cmd/hooks/complex.go b/cmd/hooks/complex.go new file mode 100644 index 0000000000..73e8567d5e --- /dev/null +++ b/cmd/hooks/complex.go @@ -0,0 +1,35 @@ +package hooks + +import ( + "encoding/json" + "net/http" + "reflect" +) + +func SliceTypeStringDecode[T any](from, to reflect.Value) (any, error) { + into := make([]T, 0) + return complexTypeStringDecodeHook(from, to, into) +} + +func MapTypeStringDecode[K ~string | ~int, V any](from, to reflect.Value) (any, error) { + into := make(map[K]V, 0) + return complexTypeStringDecodeHook(from, to, into) +} + +func MapHTTPHeaderStringDecode(from, to reflect.Value) (any, error) { + into := http.Header{} + return complexTypeStringDecodeHook(from, to, into) +} + +func complexTypeStringDecodeHook(from, to reflect.Value, out any) (any, error) { + fromInterface := from.Interface() + if to.Type() != reflect.TypeOf(out) { + return fromInterface, nil + } + data, ok := fromInterface.(string) + if !ok { + return fromInterface, nil + } + err := json.Unmarshal([]byte(data), &out) + return out, err +} diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 6e0bd080b6..4213cd59ea 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -10,7 +10,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/encryption" - "github.com/zitadel/zitadel/cmd/systemapi" + "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/oidc" @@ -47,7 +47,7 @@ type Config struct { Login login.Config WebAuthNName string Telemetry *handlers.TelemetryPusherConfig - SystemAPIUsers systemapi.Users + SystemAPIUsers map[string]*authz.SystemAPIUser } type InitProjections struct { @@ -70,6 +70,7 @@ func MustNewConfig(v *viper.Viper) *Config { hook.EnumHookFunc(domain.FeatureString), hook.EnumHookFunc(authz.MemberTypeString), actions.HTTPConfigDecodeHook, + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], )), ) logging.OnError(err).Fatal("unable to read default config") @@ -127,7 +128,6 @@ func MustNewSteps(v *viper.Viper) *Steps { mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), hook.EnumHookFunc(domain.FeatureString), - systemapi.UsersDecodeHook, )), ) logging.OnError(err).Fatal("unable to read steps") diff --git a/cmd/start/config.go b/cmd/start/config.go index 709ecaf91b..70c970de42 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/encryption" - "github.com/zitadel/zitadel/cmd/systemapi" + "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" internal_authz "github.com/zitadel/zitadel/internal/api/authz" @@ -61,7 +61,7 @@ type Config struct { EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup AuditLogRetention time.Duration - SystemAPIUsers systemapi.Users + SystemAPIUsers map[string]*internal_authz.SystemAPIUser CustomerPortal string Machine *id.Config Actions *actions.Config @@ -84,6 +84,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( + hooks.SliceTypeStringDecode[*domain.CustomMessageText], + hooks.SliceTypeStringDecode[*command.SetQuota], + hooks.SliceTypeStringDecode[internal_authz.RoleMapping], + hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.MapTypeStringDecode[domain.Feature, any], + hooks.MapHTTPHeaderStringDecode, hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), @@ -91,7 +97,6 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToSliceHookFunc(","), database.DecodeHook, actions.HTTPConfigDecodeHook, - systemapi.UsersDecodeHook, hook.EnumHookFunc(domain.FeatureString), hook.EnumHookFunc(internal_authz.MemberTypeString), )), diff --git a/cmd/start/config_test.go b/cmd/start/config_test.go index cfbf877ab5..0e8f105a23 100644 --- a/cmd/start/config_test.go +++ b/cmd/start/config_test.go @@ -1,44 +1,106 @@ package start import ( - "reflect" + "encoding/base64" + "fmt" + "net" + "net/http" "strings" "testing" "github.com/spf13/viper" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" ) func TestMustNewConfig(t *testing.T) { + encodedKey := "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + decodedKey, err := base64.StdEncoding.DecodeString(encodedKey) + if err != nil { + t.Fatal(err) + } type args struct { yaml string } tests := []struct { name string args args - want *Config + want func(*testing.T, *Config) }{{ + name: "actions deny list ok", + args: args{ + yaml: ` +Actions: + HTTP: + DenyList: + - localhost + - 127.0.0.1 + - foobar +Log: + Level: info +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.Actions.HTTP.DenyList, []actions.AddressChecker{ + &actions.DomainChecker{Domain: "localhost"}, + &actions.IPChecker{IP: net.ParseIP("127.0.0.1")}, + &actions.DomainChecker{Domain: "foobar"}}) + }, + }, { + name: "actions deny list string ok", + args: args{ + yaml: ` +Actions: + HTTP: + DenyList: localhost,127.0.0.1,foobar +Log: + Level: info +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.Actions.HTTP.DenyList, []actions.AddressChecker{ + &actions.DomainChecker{Domain: "localhost"}, + &actions.IPChecker{IP: net.ParseIP("127.0.0.1")}, + &actions.DomainChecker{Domain: "foobar"}}) + }, + }, { name: "features ok", args: args{yaml: ` DefaultInstance: Features: - FeatureLoginDefaultOrg: true +Log: + Level: info +Actions: + HTTP: + DenyList: [] `}, - want: &Config{ - DefaultInstance: command.InstanceSetup{ - Features: map[domain.Feature]any{ - domain.FeatureLoginDefaultOrg: true, - }, - }, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.DefaultInstance.Features, map[domain.Feature]any{ + domain.FeatureLoginDefaultOrg: true, + }) }, }, { - name: "membership types ok", + name: "features string ok", + args: args{yaml: ` +DefaultInstance: + Features: > + [{"featureLoginDefaultOrg": true}] +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.DefaultInstance.Features, map[domain.Feature]any{ + domain.FeatureLoginDefaultOrg: true, + }) + }, + }, { + name: "system api users ok", args: args{yaml: ` SystemAPIUsers: - superuser: @@ -46,9 +108,14 @@ SystemAPIUsers: - MemberType: System - MemberType: Organization - MemberType: IAM +Log: + Level: info +Actions: + HTTP: + DenyList: [] `}, - want: &Config{ - SystemAPIUsers: map[string]*authz.SystemAPIUser{ + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.SystemAPIUsers, map[string]*authz.SystemAPIUser{ "superuser": { Memberships: authz.Memberships{{ MemberType: authz.MemberTypeSystem, @@ -58,27 +125,121 @@ SystemAPIUsers: MemberType: authz.MemberTypeIAM, }}, }, - }, + }) + }, + }, { + name: "system api users string ok", + args: args{yaml: fmt.Sprintf(` +SystemAPIUsers: > + {"systemuser": {"path": "/path/to/superuser/key.pem"}, "systemuser2": {"keyData": "%s"}} +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`, encodedKey)}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.SystemAPIUsers, map[string]*authz.SystemAPIUser{ + "systemuser": { + Path: "/path/to/superuser/key.pem", + }, + "systemuser2": { + KeyData: decodedKey, + }, + }) + }, + }, { + name: "headers ok", + args: args{yaml: ` +Telemetry: + Headers: + single-value: single-value + multi-value: + - multi-value1 + - multi-value2 +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.Telemetry.Headers, http.Header{ + "single-value": []string{"single-value"}, + "multi-value": []string{"multi-value1", "multi-value2"}, + }) + }, + }, { + name: "headers string ok", + args: args{yaml: ` +Telemetry: + Headers: > + {"single-value": "single-value", "multi-value": ["multi-value1", "multi-value2"]} +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.Telemetry.Headers, http.Header{ + "single-value": []string{"single-value"}, + "multi-value": []string{"multi-value1", "multi-value2"}, + }) + }, + }, { + name: "message texts ok", + args: args{yaml: ` +DefaultInstance: + MessageTexts: + - MessageTextType: InitCode + Title: foo + - MessageTextType: PasswordReset + Greeting: bar +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.DefaultInstance.MessageTexts, []*domain.CustomMessageText{{ + MessageTextType: "InitCode", + Title: "foo", + }, { + MessageTextType: "PasswordReset", + Greeting: "bar", + }}) + }, + }, { + name: "message texts string ok", + args: args{yaml: ` +DefaultInstance: + MessageTexts: > + [{"messageTextType": "InitCode", "title": "foo"}, {"messageTextType": "PasswordReset", "greeting": "bar"}] +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.DefaultInstance.MessageTexts, []*domain.CustomMessageText{{ + MessageTextType: "InitCode", + Title: "foo", + }, { + MessageTextType: "PasswordReset", + Greeting: "bar", + }}) }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := viper.New() v.SetConfigType("yaml") - err := v.ReadConfig(strings.NewReader(`Log: - Level: info -Actions: - HTTP: - DenyList: [] -` + tt.args.yaml)) - require.NoError(t, err) - tt.want.Log = &logging.Config{Level: "info"} - tt.want.Actions = &actions.Config{HTTP: actions.HTTPConfig{DenyList: []actions.AddressChecker{}}} - require.NoError(t, tt.want.Log.SetLogger()) + require.NoError(t, v.ReadConfig(strings.NewReader(tt.args.yaml))) got := MustNewConfig(v) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("MustNewConfig() = %v, want %v", got, tt.want) - } + tt.want(t, got) }) } } diff --git a/cmd/systemapi/user.go b/cmd/systemapi/user.go deleted file mode 100644 index 9393cecebe..0000000000 --- a/cmd/systemapi/user.go +++ /dev/null @@ -1,27 +0,0 @@ -package systemapi - -import ( - "encoding/json" - "reflect" - - "github.com/zitadel/zitadel/internal/api/authz" -) - -type Users map[string]*authz.SystemAPIUser - -func UsersDecodeHook(from, to reflect.Value) (any, error) { - if to.Type() != reflect.TypeOf(Users{}) { - return from.Interface(), nil - } - - data, ok := from.Interface().(string) - if !ok { - return from.Interface(), nil - } - users := make(Users) - err := json.Unmarshal([]byte(data), &users) - if err != nil { - return nil, err - } - return users, nil -} diff --git a/internal/actions/http_module_config.go b/internal/actions/http_module_config.go index 29045168aa..06ee4c255f 100644 --- a/internal/actions/http_module_config.go +++ b/internal/actions/http_module_config.go @@ -3,6 +3,7 @@ package actions import ( "net" "reflect" + "strings" "github.com/mitchellh/mapstructure" "github.com/zitadel/zitadel/internal/zerrors" @@ -41,12 +42,18 @@ func HTTPConfigDecodeHook(from, to reflect.Value) (interface{}, error) { } c := HTTPConfig{ - DenyList: make([]AddressChecker, len(config.DenyList)), + DenyList: make([]AddressChecker, 0), } - for i, entry := range config.DenyList { - if c.DenyList[i], err = parseDenyListEntry(entry); err != nil { - return nil, err + for _, unsplit := range config.DenyList { + for _, split := range strings.Split(unsplit, ",") { + parsed, parseErr := parseDenyListEntry(split) + if parseErr != nil { + return nil, parseErr + } + if parsed != nil { + c.DenyList = append(c.DenyList, parsed) + } } }