From 6c0e7c402dec4d9eb560fdd07a3fe55a6845c034 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 1 May 2024 12:17:27 +0200 Subject: [PATCH] fix(setup): decode complex config strings (#7854) * fix(setup): decode complex config strings * decode complex types before string lists * lint * fix hasher env docs * hasher defaults * cleanup * cleanup --------- Co-authored-by: Livio Spring --- cmd/setup/config.go | 19 +-- cmd/setup/config_test.go | 245 +++++++++++++++++++++++++++++++++++++++ cmd/start/config.go | 10 +- 3 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 cmd/setup/config_test.go diff --git a/cmd/setup/config.go b/cmd/setup/config.go index f5547d21c..bb96e9912 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -12,13 +12,14 @@ import ( "github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" - "github.com/zitadel/zitadel/internal/api/authz" + internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/hook" "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/handlers" @@ -29,7 +30,7 @@ import ( type Config struct { Database database.Config SystemDefaults systemdefaults.SystemDefaults - InternalAuthZ authz.Config + InternalAuthZ internal_authz.Config ExternalDomain string ExternalPort uint16 ExternalSecure bool @@ -46,7 +47,7 @@ type Config struct { Login login.Config WebAuthNName string Telemetry *handlers.TelemetryPusherConfig - SystemAPIUsers map[string]*authz.SystemAPIUser + SystemAPIUsers map[string]*internal_authz.SystemAPIUser } type InitProjections struct { @@ -60,16 +61,18 @@ func MustNewConfig(v *viper.Viper) *Config { config := new(Config) err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( + hooks.SliceTypeStringDecode[*domain.CustomMessageText], + hooks.SliceTypeStringDecode[internal_authz.RoleMapping], + hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.MapHTTPHeaderStringDecode, + database.DecodeHook, + actions.HTTPConfigDecodeHook, + hook.EnumHookFunc(internal_authz.MemberTypeString), hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - database.DecodeHook, - hook.EnumHookFunc(authz.MemberTypeString), - actions.HTTPConfigDecodeHook, - hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], - hooks.SliceTypeStringDecode[authz.RoleMapping], )), ) logging.OnError(err).Fatal("unable to read default config") diff --git a/cmd/setup/config_test.go b/cmd/setup/config_test.go new file mode 100644 index 000000000..8cf241cd6 --- /dev/null +++ b/cmd/setup/config_test.go @@ -0,0 +1,245 @@ +package setup + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/muhlemmer/gu" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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 func(*testing.T, *Config) + }{{ + name: "features ok", + args: args{yaml: ` +DefaultInstance: + Features: + LoginDefaultOrg: true + LegacyIntrospection: true + TriggerIntrospectionProjections: true + UserSchema: true +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{ + LoginDefaultOrg: gu.Ptr(true), + LegacyIntrospection: gu.Ptr(true), + TriggerIntrospectionProjections: gu.Ptr(true), + UserSchema: gu.Ptr(true), + }) + }, + }, { + name: "system api users ok", + args: args{yaml: ` +SystemAPIUsers: +- superuser: + Memberships: + - MemberType: System + - MemberType: Organization + - MemberType: IAM +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.SystemAPIUsers, map[string]*authz.SystemAPIUser{ + "superuser": { + Memberships: authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + }, { + MemberType: authz.MemberTypeOrganization, + }, { + 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", + }}) + }, + }, { + name: "roles ok", + args: args{yaml: ` +InternalAuthZ: + RolePermissionMappings: + - Role: IAM_OWNER + Permissions: + - iam.write + - Role: ORG_OWNER + Permissions: + - org.write + - org.read +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.InternalAuthZ, authz.Config{ + RolePermissionMappings: []authz.RoleMapping{ + {Role: "IAM_OWNER", Permissions: []string{"iam.write"}}, + {Role: "ORG_OWNER", Permissions: []string{"org.write", "org.read"}}, + }, + }) + }, + }, { + name: "roles string ok", + args: args{yaml: ` +InternalAuthZ: + RolePermissionMappings: > + [{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write", "org.read"]}] +Log: + Level: info +Actions: + HTTP: + DenyList: [] +`}, + want: func(t *testing.T, config *Config) { + assert.Equal(t, config.InternalAuthZ, authz.Config{ + RolePermissionMappings: []authz.RoleMapping{ + {Role: "IAM_OWNER", Permissions: []string{"iam.write"}}, + {Role: "ORG_OWNER", Permissions: []string{"org.write", "org.read"}}, + }, + }) + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + require.NoError(t, v.ReadConfig(strings.NewReader(tt.args.yaml))) + got := MustNewConfig(v) + tt.want(t, got) + }) + } +} diff --git a/cmd/start/config.go b/cmd/start/config.go index 5a4d3516a..c96386521 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -85,19 +85,19 @@ 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, + database.DecodeHook, + actions.HTTPConfigDecodeHook, + hook.EnumHookFunc(internal_authz.MemberTypeString), + hooks.MapTypeStringDecode[domain.Feature, any], + hooks.SliceTypeStringDecode[*command.SetQuota], hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - database.DecodeHook, - actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read config")