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 <livio.a@gmail.com>
This commit is contained in:
Elio Bischof 2024-05-01 12:17:27 +02:00 committed by GitHub
parent 9950cafafc
commit 6c0e7c402d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 261 additions and 13 deletions

View File

@ -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")

245
cmd/setup/config_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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")