From 7040174f0d49cb71805838092bd5d3b7bc6f7e66 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Mon, 9 Dec 2024 16:42:34 +0100 Subject: [PATCH 1/9] secret management --- go.mod | 2 +- go.sum | 6 ++---- internal/cac/storage/server_storage.go | 10 +++++++++ internal/cac/storage/server_storage_test.go | 24 +++++++++++++++++++++ internal/cac/storage/tenant_storage_test.go | 2 ++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 548483a..bf7a301 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.0 require ( github.com/Masterminds/sprig/v3 v3.2.3 - github.com/cloudentity/acp-client-go v0.0.0-20240618142147-15447bea0396 + github.com/cloudentity/acp-client-go v0.0.0-20241209151610-14608290c460 github.com/corvus-ch/zbase32 v1.0.0 github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b github.com/go-openapi/strfmt v0.22.0 diff --git a/go.sum b/go.sum index 4fb47f7..b527bc1 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/cloudentity/acp-client-go v0.0.0-20240618142147-15447bea0396 h1:nWtlxPLa9os1mp4ASp3R9a+hcQo6hJWv15kYqNXXGyA= -github.com/cloudentity/acp-client-go v0.0.0-20240618142147-15447bea0396/go.mod h1:dTHIsfs5YtDOH2CgeoHFlhfnnU1X+ohn+TIU30WlWQQ= +github.com/cloudentity/acp-client-go v0.0.0-20241209151610-14608290c460 h1:ViagTxoPaC+H0R1QrjnTlXGuqR9PT4VZAI7o8v3c2KU= +github.com/cloudentity/acp-client-go v0.0.0-20241209151610-14608290c460/go.mod h1:dTHIsfs5YtDOH2CgeoHFlhfnnU1X+ohn+TIU30WlWQQ= github.com/corvus-ch/zbase32 v1.0.0 h1:pDV0qZ1g+HYA8P0PbULsgUg/tZue1FIjsZ7r7h4nZeU= github.com/corvus-ch/zbase32 v1.0.0/go.mod h1:A7KLRecF1tysURyoqiJBvMJFmt/ccqkRdDTLjlQeVsU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -55,8 +55,6 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ= -github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= diff --git a/internal/cac/storage/server_storage.go b/internal/cac/storage/server_storage.go index d43b098..702ee15 100644 --- a/internal/cac/storage/server_storage.go +++ b/internal/cac/storage/server_storage.go @@ -150,6 +150,12 @@ func (s *ServerStorage) Write(ctx context.Context, input models.Rfc7396PatchOper return err } + if err = writeFiles(data.Secrets, + filepath.Join(workspacePath, "secrets"), + func(id string, it models.TreeSecret) string { return id }); err != nil { + return err + } + slog.Info("Workspace configuration successfully stored", "workspace", workspace, "path", workspacePath) return nil @@ -257,6 +263,10 @@ func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models return nil, err } + if err = readFilesToMap(server, "secrets", filepath.Join(path, "secrets")); err != nil { + return nil, err + } + if server, err = utils.FilterPatch(server, options.Filters); err != nil { return nil, err } diff --git a/internal/cac/storage/server_storage_test.go b/internal/cac/storage/server_storage_test.go index 05f9323..2ed75d0 100644 --- a/internal/cac/storage/server_storage_test.go +++ b/internal/cac/storage/server_storage_test.go @@ -41,7 +41,9 @@ func TestStorage(t *testing.T) { }, assert: func(t *testing.T, path string, bts []byte) { require.YAMLEq(t, `access_token_ttl: 10m0s +authentication_mechanisms: [] authorization_code_ttl: 0s +scope_claim_formats: [] backchannel_token_delivery_modes_supported: [] backchannel_user_code_parameter_supported: false cookie_max_age: 0s @@ -93,6 +95,7 @@ client_id_issued_at: 0 client_name: Demo Portal client_secret_expires_at: 0 created_at: 0001-01-01T00:00:00.000Z +default_acr_values: [] dpop_bound_access_tokens: false dynamically_registered: false grant_types: [] @@ -232,6 +235,7 @@ identifier_case_insensitive: false mfa_session_ttl: 0s name: Some Pool public_registration_allowed: false +second_factor_threshold: 0 system: false`, string(bts)) }, }, @@ -587,10 +591,30 @@ identifier_case_insensitive: false mfa_session_ttl: 0s name: Some Pool public_registration_allowed: false +second_factor_threshold: 0 system: false`, string(bts)) } }, }, + { + desc: "secrets", + data: &models.TreeServer{ + Secrets: models.TreeSecrets{ + "Some_secret": models.TreeSecret{ + CreatedAt: dateTime, + Secret: "test", + }, + }, + }, + files: []string{ + "workspaces/demo/secrets/Some_secret.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `created_at: 2024-01-23T23:19:30.004+01:00 +id: Some_secret +secret: test`, string(bts)) + }, + }, } for _, tc := range tcs { diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index 5f6001e..447a575 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -60,7 +60,9 @@ id: sms mechanism: sms`, string(bts)) case "workspaces/demo/server.yaml": require.YAMLEq(t, `access_token_ttl: 10m0s +authentication_mechanisms: [] authorization_code_ttl: 0s +scope_claim_formats: [] backchannel_token_delivery_modes_supported: [] backchannel_user_code_parameter_supported: false cookie_max_age: 0s From 6c1155f1d604be7d6bf7b02eaa12b23b61deb6e8 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Mon, 9 Dec 2024 16:46:38 +0100 Subject: [PATCH 2/9] filter secrets from diff --- internal/cac/diff/diff.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cac/diff/diff.go b/internal/cac/diff/diff.go index 4af34f8..11ee60e 100644 --- a/internal/cac/diff/diff.go +++ b/internal/cac/diff/diff.go @@ -57,6 +57,7 @@ var secretFields = []string{ "servers.*jwks", // workspace jwks (when comparing tenant config) "webhooks.*api_key", "mfa_methods.*auth", + "secrets.*secret", } var volatileFields = []string{ From a8710313303736688d57851f3bf00c6286a69020 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 01:54:04 +0200 Subject: [PATCH 3/9] first iteration --- .gitignore | 3 +- cmd/pull.go | 34 +- cmd/push.go | 5 +- go.mod | 18 +- go.sum | 27 +- internal/cac/api/model.go | 78 ++++ internal/cac/api/source.go | 10 +- internal/cac/app.go | 13 +- internal/cac/client/client.go | 39 +- internal/cac/client/client_test.go | 24 +- internal/cac/client/errors.go | 28 ++ internal/cac/client/secrets_client.go | 127 +++++++ internal/cac/client/tenant_client.go | 43 ++- internal/cac/data/server_validator.go | 13 +- internal/cac/data/tenant_validator.go | 9 +- internal/cac/data/validator.go | 6 +- internal/cac/data/validator_test.go | 6 +- internal/cac/diff/diff.go | 26 +- internal/cac/logging/logging.go | 16 +- internal/cac/storage/dry.go | 28 +- internal/cac/storage/multi.go | 24 +- internal/cac/storage/reader.go | 12 +- internal/cac/storage/secrets_test.go | 133 +++++++ internal/cac/storage/server_storage.go | 93 +++-- internal/cac/storage/server_storage_test.go | 58 +-- internal/cac/storage/storage.go | 5 +- internal/cac/storage/tenant_storage.go | 388 +++++++++++--------- internal/cac/storage/tenant_storage_test.go | 12 +- internal/cac/storage/test_utils.go | 52 +++ internal/cac/storage/writer.go | 25 +- internal/cac/templates/functions.go | 4 +- internal/cac/templates/model.go | 5 +- internal/cac/utils/model.go | 8 +- internal/cac/utils/model_test.go | 5 +- 34 files changed, 977 insertions(+), 400 deletions(-) create mode 100644 internal/cac/api/model.go create mode 100644 internal/cac/client/errors.go create mode 100644 internal/cac/client/secrets_client.go create mode 100644 internal/cac/storage/secrets_test.go create mode 100644 internal/cac/storage/test_utils.go diff --git a/.gitignore b/.gitignore index 349f7bb..3633e15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.iml .idea -/cac \ No newline at end of file +/cac +/examples/e2e-local/ \ No newline at end of file diff --git a/cmd/pull.go b/cmd/pull.go index 1123c05..3110c41 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -1,9 +1,12 @@ package cmd import ( - "github.com/cloudentity/acp-client-go/clients/hub/models" + "os" + "github.com/cloudentity/cac/internal/cac" "github.com/cloudentity/cac/internal/cac/api" + "github.com/cloudentity/cac/internal/cac/utils" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/exp/slog" ) @@ -15,7 +18,7 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { var ( app *cac.Application - data models.Rfc7396PatchOperation + data api.PatchInterface err error ) @@ -39,20 +42,43 @@ var ( return err } - if err = app.Storage.Write(cmd.Context(), data, api.WithWorkspace(rootConfig.Workspace)); err != nil { - return err + if pullConfig.Out == "" { + // default + if err = app.Storage.Write(cmd.Context(), data, api.WithWorkspace(rootConfig.Workspace), api.WithSecrets(pullConfig.WithSecrets)); err != nil { + return err + } + } else { + bts, err := utils.ToYaml(data) + + if err != nil { + return errors.Wrap(err, "failed to marshal data to YAML") + } + + if pullConfig.Out == "-" { + if _, err = os.Stdout.Write(bts); err != nil { + return errors.Wrap(err, "failed to write diff result to stdout") + } + } + + if err = os.WriteFile(pullConfig.Out, bts, 0644); err != nil { + return errors.Wrap(err, "failed to write diff result to file") + } } + slog.Info("Configuration pulled", "out", pullConfig.Out) + return nil }, } pullConfig struct { WithSecrets bool Filters []string + Out string } ) func init() { pullCmd.PersistentFlags().BoolVar(&pullConfig.WithSecrets, "with-secrets", false, "Pull secrets") pullCmd.PersistentFlags().StringSliceVar(&pullConfig.Filters, "filter", []string{}, "Pull only selected resources") + pullCmd.PersistentFlags().StringVar(&pullConfig.Out, "out", "", "Pull output. It can be a file or '-' for stdout") } diff --git a/cmd/push.go b/cmd/push.go index 0058931..fb8fce8 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/cloudentity/acp-client-go/clients/hub/models" "github.com/cloudentity/cac/internal/cac" "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/storage" @@ -17,7 +16,7 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { var ( app *cac.Application - data models.Rfc7396PatchOperation + data api.PatchInterface err error ) @@ -34,7 +33,7 @@ var ( } if !pushConfig.NoLocalValidate { - if err = app.Validator.Validate(&data); err != nil { + if err = app.Validator.Validate(data); err != nil { return errors.Wrap(err, "failed to validate configuration") } } diff --git a/go.mod b/go.mod index bf7a301..562a663 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/cloudentity/cac -go 1.22 +go 1.23.0 -toolchain go1.22.0 +toolchain go1.24.3 require ( github.com/Masterminds/sprig/v3 v3.2.3 - github.com/cloudentity/acp-client-go v0.0.0-20241209151610-14608290c460 + github.com/cloudentity/acp-client-go v0.0.0-20250530113034-a2fab50491c3 github.com/corvus-ch/zbase32 v1.0.0 github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b github.com/go-openapi/strfmt v0.22.0 @@ -17,7 +17,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20240119083558-1b970713d09a ) @@ -28,6 +28,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.14.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.22.2 // indirect @@ -69,16 +70,15 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.35.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b527bc1..deface5 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/cloudentity/acp-client-go v0.0.0-20241209151610-14608290c460 h1:ViagTxoPaC+H0R1QrjnTlXGuqR9PT4VZAI7o8v3c2KU= -github.com/cloudentity/acp-client-go v0.0.0-20241209151610-14608290c460/go.mod h1:dTHIsfs5YtDOH2CgeoHFlhfnnU1X+ohn+TIU30WlWQQ= +github.com/cloudentity/acp-client-go v0.0.0-20250530113034-a2fab50491c3 h1:uOLP0y8JkhUF5NHFhi9r8BxblcH/Q2Of35y06GpFaFA= +github.com/cloudentity/acp-client-go v0.0.0-20250530113034-a2fab50491c3/go.mod h1:bDN2WQOAcMuBO9eQc1Le3zgyQ0RdsIcwVg3U+lR9Pgg= github.com/corvus-ch/zbase32 v1.0.0 h1:pDV0qZ1g+HYA8P0PbULsgUg/tZue1FIjsZ7r7h4nZeU= github.com/corvus-ch/zbase32 v1.0.0/go.mod h1:A7KLRecF1tysURyoqiJBvMJFmt/ccqkRdDTLjlQeVsU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -22,6 +22,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b h1:IM96IiRXFcd7l+mU8Sys9pcggoBLbH/dEgzOESrS8F8= github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b/go.mod h1:uDEMZSTQMj7V6Lxdrx4ZwchmHEGdICbjuY+GQd7j9LM= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -150,8 +152,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -175,8 +178,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -189,8 +192,8 @@ golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -200,8 +203,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -212,8 +215,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -234,8 +237,6 @@ gopkg.in/corvus-ch/zbase32.v1 v1.0.0 h1:K4u1NprbDNvKPczKfHLbwdOWHTZ0zfv2ow71H1nR gopkg.in/corvus-ch/zbase32.v1 v1.0.0/go.mod h1:T3oKkPOm4AV/bNXCNFUxRmlE9RUyBz/DSo0nK9U+c0Y= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cac/api/model.go b/internal/cac/api/model.go new file mode 100644 index 0000000..3760982 --- /dev/null +++ b/internal/cac/api/model.go @@ -0,0 +1,78 @@ +package api + +import ( + "github.com/cloudentity/acp-client-go/clients/hub/models" + smodels "github.com/cloudentity/acp-client-go/clients/system/models" + "github.com/imdario/mergo" +) + +type ServerExtensions struct { + Secrets map[string]*smodels.Secret `json:"secrets,omitempty"` +} + +type TenantExtensions struct { + Servers map[string]ServerExtensions `json:"servers,omitempty"` +} + +func (te *TenantExtensions) GetServerExtensions(serverID string) *ServerExtensions { + if te.Servers == nil { + return nil + } + + if ext, ok := te.Servers[serverID]; ok { + return &ext + } + + return nil +} + +type Patch[T any] struct { + Data models.Rfc7396PatchOperation `json:"data,omitempty"` + Ext *T `json:"ext,omitempty"` +} + +type PatchInterface interface { + GetData() models.Rfc7396PatchOperation + GetExtensions() any + Merge(other PatchInterface) error +} + +type ServerPatch Patch[ServerExtensions] + +func (sp *ServerPatch) GetData() models.Rfc7396PatchOperation { + return sp.Data +} +func (tp *ServerPatch) GetExtensions() any { + return tp.Ext +} +func (sp *ServerPatch) Merge(other PatchInterface) error { + if err := mergo.Merge(&sp.Data, other.GetData(), mergo.WithOverride); err != nil { + return err + } + + if err := mergo.Merge(sp.Ext, other.GetExtensions(), mergo.WithOverride); err != nil { + return err + } + + return nil +} + +type TenantPatch Patch[TenantExtensions] + +func (tp *TenantPatch) GetData() models.Rfc7396PatchOperation { + return tp.Data +} +func (tp *TenantPatch) GetExtensions() any { + return tp.Ext +} +func (sp *TenantPatch) Merge(other PatchInterface) error { + if err := mergo.Merge(&sp.Data, other.GetData(), mergo.WithOverride); err != nil { + return err + } + + if err := mergo.Merge(sp.Ext, other.GetExtensions(), mergo.WithOverride); err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/internal/cac/api/source.go b/internal/cac/api/source.go index f291e56..5ddaf38 100644 --- a/internal/cac/api/source.go +++ b/internal/cac/api/source.go @@ -3,7 +3,6 @@ package api import ( "context" "errors" - "github.com/cloudentity/acp-client-go/clients/hub/models" ) type SourceType string @@ -37,17 +36,12 @@ type Options struct { type SourceOpt func(*Options) type Source interface { - Read(ctx context.Context, opts ...SourceOpt) (models.Rfc7396PatchOperation, error) - Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...SourceOpt) error + Read(ctx context.Context, opts ...SourceOpt) (PatchInterface, error) + Write(ctx context.Context, data PatchInterface, opts ...SourceOpt) error String() string } -type Mapper[T any] interface { - FromPatchToModel(patch models.Rfc7396PatchOperation) (*T, error) - FromModelToPatch(*T) (models.Rfc7396PatchOperation, error) -} - func WithSecrets(secrets bool) SourceOpt { return func(o *Options) { o.Secrets = secrets diff --git a/internal/cac/app.go b/internal/cac/app.go index 8c874ed..76942e9 100644 --- a/internal/cac/app.go +++ b/internal/cac/app.go @@ -1,6 +1,8 @@ package cac import ( + "strings" + "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/client" "github.com/cloudentity/cac/internal/cac/config" @@ -8,15 +10,14 @@ import ( "github.com/cloudentity/cac/internal/cac/logging" "github.com/cloudentity/cac/internal/cac/storage" "golang.org/x/exp/slog" - "strings" ) type Application struct { - Config *config.Configuration - RootConfig *config.RootConfiguration - Client api.Source - Storage storage.Storage - Validator data.ValidatorApi + Config *config.Configuration + RootConfig *config.RootConfiguration + Client api.Source + Storage storage.Storage + Validator data.ValidatorApi } func InitApp(configPath string, profile string, tenant bool) (app *Application, err error) { diff --git a/internal/cac/client/client.go b/internal/cac/client/client.go index 2966f64..c116153 100644 --- a/internal/cac/client/client.go +++ b/internal/cac/client/client.go @@ -4,18 +4,21 @@ import ( "context" "crypto/tls" "fmt" + "net/http" + "github.com/cloudentity/acp-client-go" "github.com/cloudentity/acp-client-go/clients/hub/client/workspace_configuration" "github.com/cloudentity/acp-client-go/clients/hub/models" + smodels "github.com/cloudentity/acp-client-go/clients/system/models" "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/utils" "github.com/pkg/errors" "golang.org/x/exp/slog" - "net/http" ) type Client struct { acp *acpclient.Client + sec *SecretsClient } var _ api.Source = &Client{} @@ -43,14 +46,19 @@ func InitClient(config *Configuration) (c *Client, err error) { return &Client{ acp: &acp, + sec: &SecretsClient{ + acp: &acp, + }, }, nil } -func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) { +func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { var ( options = &api.Options{} ok *workspace_configuration.ExportWorkspaceConfigOK + secrets map[string]*smodels.Secret data models.Rfc7396PatchOperation + ext = api.ServerExtensions{} workspace string err error ) @@ -72,6 +80,7 @@ func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc739 WithWithCredentials(&options.Secrets). WithTid(c.acp.Config.TenantID). WithWid(workspace), nil); err != nil { + logErr(err) return nil, err } @@ -79,14 +88,25 @@ func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc739 return nil, errors.Wrap(err, "failed to convert tree server to patch") } + if options.Secrets { + if secrets, err = c.sec.ListAllAsMap(ctx, workspace); err != nil { + return nil, errors.Wrap(err, "failed to list secrets") + } + + ext.Secrets = secrets + } + if data, err = utils.FilterPatch(data, options.Filters); err != nil { return nil, errors.Wrap(err, "failed to filter patch") } - return data, nil + return &api.ServerPatch{ + Data: data, + Ext: &ext, + }, nil } -func (c *Client) Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { +func (c *Client) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { var ( options = &api.Options{} workspace string @@ -104,10 +124,12 @@ func (c *Client) Write(ctx context.Context, data models.Rfc7396PatchOperation, o switch options.Method { case "import": if err = c.Import(ctx, workspace, options.Mode, data); err != nil { + logErr(err) return err } case "patch": if err = c.Patch(ctx, workspace, options.Mode, data); err != nil { + logErr(err) return err } default: @@ -117,7 +139,7 @@ func (c *Client) Write(ctx context.Context, data models.Rfc7396PatchOperation, o return nil } -func (c *Client) Patch(ctx context.Context, workspace string, mode string, data models.Rfc7396PatchOperation) error { +func (c *Client) Patch(ctx context.Context, workspace string, mode string, data api.PatchInterface) error { var ( err error ) @@ -129,20 +151,20 @@ func (c *Client) Patch(ctx context.Context, workspace string, mode string, data WithWid(workspace). WithTid(c.acp.Config.TenantID). WithMode(&mode). - WithPatch(data), nil); err != nil { + WithPatch(data.GetData()), nil); err != nil { return err } return nil } -func (c *Client) Import(ctx context.Context, workspace string, mode string, data models.Rfc7396PatchOperation) error { +func (c *Client) Import(ctx context.Context, workspace string, mode string, data api.PatchInterface) error { var ( err error out *models.TreeServer ) - if out, err = utils.FromPatchToModel[models.TreeServer](data); err != nil { + if out, err = utils.FromPatchToModel[models.TreeServer](data.GetData()); err != nil { return err } @@ -163,6 +185,7 @@ func (c *Client) Import(ctx context.Context, workspace string, mode string, data func (c *Client) Tenant() *TenantClient { return &TenantClient{ acp: c.acp, + sec: c.sec, } } diff --git a/internal/cac/client/client_test.go b/internal/cac/client/client_test.go index 1b6e6d6..be2f1c4 100644 --- a/internal/cac/client/client_test.go +++ b/internal/cac/client/client_test.go @@ -85,9 +85,9 @@ func TestClient(t *testing.T) { require.NoError(t, err) - require.Len(t, data["clients"], 1) - require.Len(t, data["idps"], 1) - require.Equal(t, "demo workspace", data["name"]) + require.Len(t, data.GetData()["clients"], 1) + require.Len(t, data.GetData()["idps"], 1) + require.Equal(t, "demo workspace", data.GetData()["name"]) }) t.Run("client pull configuration and filter", func(t *testing.T) { @@ -114,8 +114,8 @@ func TestClient(t *testing.T) { require.NoError(t, err) - require.Len(t, data["clients"], 1) - require.Nil(t, data["idps"]) + require.Len(t, data.GetData()["clients"], 1) + require.Nil(t, data.GetData()["idps"]) }) t.Run("client pull tenant configuration", func(t *testing.T) { @@ -142,9 +142,9 @@ func TestClient(t *testing.T) { require.NoError(t, err) - require.Len(t, data["servers"], 1) - require.Len(t, data["mfa_methods"], 1) - require.Equal(t, "demo tenant", data["name"]) + require.Len(t, data.GetData()["servers"], 1) + require.Len(t, data.GetData()["mfa_methods"], 1) + require.Equal(t, "demo tenant", data.GetData()["name"]) }) t.Run("client pull tenant configuration with credentials", func(t *testing.T) { @@ -171,10 +171,10 @@ func TestClient(t *testing.T) { require.NoError(t, err) - require.Len(t, data["servers"], 1) - require.Len(t, data["mfa_methods"], 1) - require.Equal(t, "demo tenant", data["name"]) - secret := data["servers"].(map[string]interface{})["server1"].(map[string]interface{})["clients"].(map[string]interface{})["cid1"].(map[string]interface{})["client_secret"] + require.Len(t, data.GetData()["servers"], 1) + require.Len(t, data.GetData()["mfa_methods"], 1) + require.Equal(t, "demo tenant", data.GetData()["name"]) + secret := data.GetData()["servers"].(map[string]interface{})["server1"].(map[string]interface{})["clients"].(map[string]interface{})["cid1"].(map[string]interface{})["client_secret"] require.Equal(t, "secret", secret) }) } diff --git a/internal/cac/client/errors.go b/internal/cac/client/errors.go new file mode 100644 index 0000000..8921fe5 --- /dev/null +++ b/internal/cac/client/errors.go @@ -0,0 +1,28 @@ +package client + +import ( + "reflect" + + "github.com/cloudentity/acp-client-go/clients/hub/client/workspace_configuration" + "github.com/go-openapi/runtime" + "golang.org/x/exp/slog" +) + +func logErr(err error) { + switch e := err.(type) { + case *runtime.APIError: + traceID := "" + resp, ok := e.Response.(runtime.ClientResponse) + if ok { + traceID = resp.GetHeader("X-Trace-ID") + } + slog.Error("Request failed", "code", e.Code, "trace.id", traceID) + case *workspace_configuration.PatchWorkspaceConfigRfc7396UnprocessableEntity: + case *workspace_configuration.PatchWorkspaceConfigRfc6902BadRequest: + case *workspace_configuration.ImportWorkspaceConfigBadRequest: + case *workspace_configuration.ImportWorkspaceConfigUnprocessableEntity: + slog.Error("Request failed", "code", e.Code, "message", e.Payload.Error) + default: + slog.Error("Request failed", "error", reflect.TypeOf(err), "message", err.Error()) + } +} \ No newline at end of file diff --git a/internal/cac/client/secrets_client.go b/internal/cac/client/secrets_client.go new file mode 100644 index 0000000..c329142 --- /dev/null +++ b/internal/cac/client/secrets_client.go @@ -0,0 +1,127 @@ +package client + +import ( + "context" + + acpclient "github.com/cloudentity/acp-client-go" + "github.com/cloudentity/acp-client-go/clients/system/client/secrets" + "github.com/cloudentity/acp-client-go/clients/system/models" + "github.com/imdario/mergo" + "github.com/pkg/errors" +) + +type SecretsClient struct { + acp *acpclient.Client +} + +func (s *SecretsClient) ListAll(ctx context.Context, wid string) ([]*models.Secret, error) { + var ( + ok *secrets.ListSecretsOK + err error + ) + + if ok, err = s.acp.System.Secrets.ListSecrets(secrets.NewListSecretsParamsWithContext(ctx). + WithWid(wid), nil); err != nil { + return nil, err + } + + return ok.Payload.Secrets, nil +} + +func (s *SecretsClient) ListAllAsMap(ctx context.Context, wid string) (map[string]*models.Secret, error) { + var ( + all []*models.Secret + err error + ) + + if all, err = s.ListAll(ctx, wid); err != nil { + return nil, err + } + + secretMap := make(map[string]*models.Secret, len(all)) + + for _, secret := range all { + secretMap[secret.ID] = secret + } + + return secretMap, nil +} + +func (s *SecretsClient) Create(ctx context.Context, wid string, payload models.Secret) (*models.Secret, error) { + var ( + ok *secrets.CreateSecretCreated + err error + ) + + if ok, err = s.acp.System.Secrets.CreateSecret(secrets.NewCreateSecretParamsWithContext(ctx). + WithWid(wid). + WithSecret(&payload), nil); err != nil { + return nil, err + } + + return ok.Payload, nil +} + +func (s *SecretsClient) Update(ctx context.Context, wid string, payload models.Secret) (error) { + var ( + err error + ) + + if _, err = s.acp.System.Secrets.UpdateSecret(secrets.NewUpdateSecretParamsWithContext(ctx). + WithWid(wid). + WithSecret(&payload), nil); err != nil { + return err + } + + return nil +} + +func (s *SecretsClient) UpdateAll(ctx context.Context, wid string, payload []models.Secret) error { + return s.patchAll(ctx, wid, payload, func(dest *models.Secret, source models.Secret) error { + dest = &source + return nil + }) +} +func (s *SecretsClient) PatchAll(ctx context.Context, wid string, payload []models.Secret) error { + return s.patchAll(ctx, wid, payload, func(dest *models.Secret, source models.Secret) error { + return mergo.Merge(dest, source, mergo.WithOverride); + }) +} + +type PatchFunc func (dest *models.Secret, source models.Secret) error + +func (s *SecretsClient) patchAll(ctx context.Context, wid string, payload []models.Secret, patchF PatchFunc) error { + + var ( + existingSecrets []*models.Secret + err error + ) + + if existingSecrets, err = s.ListAll(ctx, wid); err != nil { + return err + } + + existingMap := make(map[string]*models.Secret) + for _, secret := range existingSecrets { + existingMap[secret.ID] = secret + } + + for _, secret := range payload { + if existingSecret, exists := existingMap[secret.ID]; exists { + if err = patchF(existingSecret, secret); err != nil { + return errors.Wrapf(err, "failed to merge secret %s", secret.ID) + } + + if err = s.Update(ctx, wid, *existingSecret); err != nil { + return errors.Wrapf(err, "failed to update secret %s", secret.ID) + } + delete(existingMap, existingSecret.ID) // Remove from map to avoid creating it later + } else { + if _, err = s.Create(ctx, wid, secret); err != nil { + return errors.Wrapf(err, "failed to create secret %s", secret.ID) + } + } + } + + return nil +} \ No newline at end of file diff --git a/internal/cac/client/tenant_client.go b/internal/cac/client/tenant_client.go index 27e85ba..0c77c2d 100644 --- a/internal/cac/client/tenant_client.go +++ b/internal/cac/client/tenant_client.go @@ -7,6 +7,7 @@ import ( acpclient "github.com/cloudentity/acp-client-go" "github.com/cloudentity/acp-client-go/clients/hub/client/tenant_configuration" "github.com/cloudentity/acp-client-go/clients/hub/models" + smodels "github.com/cloudentity/acp-client-go/clients/system/models" "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/utils" "golang.org/x/exp/slog" @@ -14,14 +15,18 @@ import ( type TenantClient struct { acp *acpclient.Client + sec *SecretsClient } -func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) { +func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { var ( ok *tenant_configuration.ExportTenantConfigOK options = &api.Options{} data models.Rfc7396PatchOperation - err error + ext = api.TenantExtensions{ + Servers: make(map[string]api.ServerExtensions), + } + err error ) for _, opt := range opts { @@ -34,21 +39,42 @@ func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (models. WithTid(t.acp.Config.TenantID). WithWithCredentials(&options.Secrets), nil, ); err != nil { + logErr(err) return nil, err } - if data, err = utils.FromModelToPatch[models.TreeTenant](ok.Payload); err != nil { + if data, err = utils.FromModelToPatch(ok.Payload); err != nil { return nil, err } + if options.Secrets { + for id, _ := range ok.Payload.Servers { + var secrets map[string]*smodels.Secret + + slog.Info("Pulling all server secrets", "server", id) + if secrets, err = t.sec.ListAllAsMap(ctx, id); err != nil { + return nil, fmt.Errorf("failed to list secrets for server %s: %w", id, err) + } + + slog.Info("Pulled secrets", "server", id, "count", len(secrets)) + + ext.Servers[id] = api.ServerExtensions{ + Secrets: secrets, + } + } + } + if data, err = utils.FilterPatch(data, options.Filters); err != nil { return nil, err } - return data, nil + return &api.TenantPatch{ + Data: data, + Ext: &ext, + }, nil } -func (t *TenantClient) Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { +func (t *TenantClient) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { var ( options = &api.Options{} err error @@ -60,11 +86,13 @@ func (t *TenantClient) Write(ctx context.Context, data models.Rfc7396PatchOperat switch options.Method { case "import": - if err = t.Import(ctx, options.Mode, data); err != nil { + if err = t.Import(ctx, options.Mode, data.GetData()); err != nil { + logErr(err) return err } case "patch": - if err = t.Patch(ctx, options.Mode, data); err != nil { + if err = t.Patch(ctx, options.Mode, data.GetData()); err != nil { + logErr(err) return err } default: @@ -103,6 +131,7 @@ func (t *TenantClient) Patch(ctx context.Context, mode string, data models.Rfc73 WithMode(&mode). WithPatch(data), nil, ); err != nil { + logErr(err) return err } diff --git a/internal/cac/data/server_validator.go b/internal/cac/data/server_validator.go index 87316d0..3db657b 100644 --- a/internal/cac/data/server_validator.go +++ b/internal/cac/data/server_validator.go @@ -2,6 +2,7 @@ package data import ( "github.com/cloudentity/acp-client-go/clients/hub/models" + "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/utils" "github.com/go-openapi/strfmt" ) @@ -10,12 +11,16 @@ type ServerValidator struct{} var _ ValidatorApi = &ServerValidator{} -func (sv *ServerValidator) Validate(data *models.Rfc7396PatchOperation) error { +func (sv *ServerValidator) Validate(data api.PatchInterface) error { var ( - err error - serv *models.TreeServer + err error + serv *models.TreeServer + sdata = data.GetData() ) - if serv, err = utils.FromPatchToModel[models.TreeServer](*data); err != nil { + + utils.CleanPatch(sdata) + + if serv, err = utils.FromPatchToModel[models.TreeServer](sdata); err != nil { return err } diff --git a/internal/cac/data/tenant_validator.go b/internal/cac/data/tenant_validator.go index 654086c..7502517 100644 --- a/internal/cac/data/tenant_validator.go +++ b/internal/cac/data/tenant_validator.go @@ -2,6 +2,7 @@ package data import ( "github.com/cloudentity/acp-client-go/clients/hub/models" + "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/utils" "github.com/go-openapi/strfmt" ) @@ -10,12 +11,16 @@ type TenantValidator struct{} var _ ValidatorApi = &TenantValidator{} -func (sv *TenantValidator) Validate(data *models.Rfc7396PatchOperation) error { +func (sv *TenantValidator) Validate(data api.PatchInterface) error { var ( err error tenant *models.TreeTenant + tdata = data.GetData() ) - if tenant, err = utils.FromPatchToModel[models.TreeTenant](*data); err != nil { + + utils.CleanPatch(tdata) + + if tenant, err = utils.FromPatchToModel[models.TreeTenant](tdata); err != nil { return err } diff --git a/internal/cac/data/validator.go b/internal/cac/data/validator.go index 9c02b49..b5aa156 100644 --- a/internal/cac/data/validator.go +++ b/internal/cac/data/validator.go @@ -1,7 +1,9 @@ package data -import "github.com/cloudentity/acp-client-go/clients/hub/models" +import ( + "github.com/cloudentity/cac/internal/cac/api" +) type ValidatorApi interface { - Validate(data *models.Rfc7396PatchOperation) error + Validate(data api.PatchInterface) error } diff --git a/internal/cac/data/validator_test.go b/internal/cac/data/validator_test.go index fedec02..6a3261f 100644 --- a/internal/cac/data/validator_test.go +++ b/internal/cac/data/validator_test.go @@ -3,6 +3,7 @@ package data_test import ( "testing" + "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/data" "github.com/stretchr/testify/require" @@ -27,7 +28,10 @@ func TestServerValidator(t *testing.T) { }, } - err := validator.Validate(&patch) + err := validator.Validate(&api.TenantPatch{ + Data: patch, + Ext: &api.TenantExtensions{}, + }) require.NoError(t, err) }) diff --git a/internal/cac/diff/diff.go b/internal/cac/diff/diff.go index 11ee60e..003888f 100644 --- a/internal/cac/diff/diff.go +++ b/internal/cac/diff/diff.go @@ -2,7 +2,6 @@ package diff import ( "context" - "github.com/cloudentity/acp-client-go/clients/hub/models" "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/utils" "github.com/google/go-cmp/cmp" @@ -90,8 +89,8 @@ var filterSecretFields = fieldsFilter(secretFields) func Diff(ctx context.Context, source api.Source, target api.Source, workspace string, opts ...Option) (string, error) { var ( - server1 models.Rfc7396PatchOperation - server2 models.Rfc7396PatchOperation + server1 api.PatchInterface + server2 api.PatchInterface options = &Options{} readOpts []api.SourceOpt err error @@ -124,7 +123,7 @@ func Diff(ctx context.Context, source api.Source, target api.Source, workspace s return Tree(server1, server2, opts...) } -func Tree(source models.Rfc7396PatchOperation, target models.Rfc7396PatchOperation, opts ...Option) (string, error) { +func Tree(source api.PatchInterface, target api.PatchInterface, opts ...Option) (string, error) { var ( options = &Options{} diffOpts = cmp.Options{} @@ -135,22 +134,25 @@ func Tree(source models.Rfc7396PatchOperation, target models.Rfc7396PatchOperati opt(options) } - utils.CleanPatch(source) - utils.CleanPatch(target) + sdata := source.GetData() + tdata := target.GetData() + + utils.CleanPatch(sdata) + utils.CleanPatch(tdata) // marshaling structs to json and back to get proper field names in the comparison - if source, err = utils.NormalizePatch(source); err != nil { + if sdata, err = utils.NormalizePatch(sdata); err != nil { return "", err } - if target, err = utils.NormalizePatch(target); err != nil { + if tdata, err = utils.NormalizePatch(tdata); err != nil { return "", err } if options.PresentAtSource { - for k := range target { - if tm, ok := target[k].(map[string]any); ok { - OnlyPresentKeys(source[k], tm) + for k := range tdata { + if tm, ok := tdata[k].(map[string]any); ok { + OnlyPresentKeys(sdata[k], tm) } } } @@ -163,7 +165,7 @@ func Tree(source models.Rfc7396PatchOperation, target models.Rfc7396PatchOperati diffOpts = append(diffOpts, filterSecretFields) } - var out = cmp.Diff(target, source, diffOpts) + var out = cmp.Diff(tdata, sdata, diffOpts) if options.Color { return colorize(out), nil diff --git a/internal/cac/logging/logging.go b/internal/cac/logging/logging.go index 29069fb..6935c1b 100644 --- a/internal/cac/logging/logging.go +++ b/internal/cac/logging/logging.go @@ -1,10 +1,14 @@ package logging import ( - "golang.org/x/exp/slog" "os" + "strings" + + "golang.org/x/exp/slog" ) +const LevelTrace slog.Level = -8 + var DefaultLoggingConfig = func() *Configuration { return &Configuration{ Level: "info", @@ -33,7 +37,9 @@ func InitLogging(config *Configuration) (err error) { logger *slog.Logger ) - if err = levelRef.UnmarshalText([]byte(config.Level)); err != nil { + if err = levelRef.UnmarshalText([]byte(config.Level)); err != nil && strings.ToUpper(config.Level) == "TRACE" { + levelRef.Set(LevelTrace) + } else if err != nil { return err } @@ -47,7 +53,11 @@ func InitLogging(config *Configuration) (err error) { logger = slog.New(handler) slog.SetDefault(logger) - slog.With("logger", logger).Debug("Initiated logging") + slog.With("level", levelRef.Level()).Debug("Initiated logging") return nil } + +func Trace(msg string, args... any) { + slog.Log(nil, LevelTrace, msg, args...) +} diff --git a/internal/cac/storage/dry.go b/internal/cac/storage/dry.go index 8862d16..e137626 100644 --- a/internal/cac/storage/dry.go +++ b/internal/cac/storage/dry.go @@ -5,8 +5,8 @@ import ( "log/slog" "os" - "github.com/cloudentity/acp-client-go/clients/hub/models" "github.com/cloudentity/cac/internal/cac/api" + "github.com/cloudentity/cac/internal/cac/logging" "github.com/cloudentity/cac/internal/cac/utils" "github.com/pkg/errors" ) @@ -22,8 +22,20 @@ func InitDryStorage(out string, constr Constructor) (*DryStorage, error) { ) if out == "-" { - slog.Debug("Writing to stdout") - delegatedWriter = stdWriter + logging.Trace("Writing to stdout") + delegatedWriter = func(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { + var ( + bts []byte + err error + ) + + if bts, err = utils.ToYaml(data); err != nil { + return err + } + + _, err = os.Stdout.Write(bts) + return err + } } else if out != "" { var ( file *os.File @@ -63,17 +75,17 @@ func InitDryStorage(out string, constr Constructor) (*DryStorage, error) { }, nil } -type WriterFunc func(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error +type WriterFunc func(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error -func (d *DryStorage) Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { +func (d *DryStorage) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { return d.DelegatedWriter(ctx, data, opts...) } -func (d *DryStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) { +func (d *DryStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { panic("read operation is not implemented for dry storage") } -var stdWriter = func(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { +var stdWriter = func(ctx context.Context, data *api.Patch[any], opts ...api.SourceOpt) error { var ( bts []byte err error @@ -87,7 +99,7 @@ var stdWriter = func(ctx context.Context, data models.Rfc7396PatchOperation, opt } var flatFileWriter = func(out string) WriterFunc { - return func(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { + return func(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { var ( bts []byte err error diff --git a/internal/cac/storage/multi.go b/internal/cac/storage/multi.go index 8888c62..64bedaa 100644 --- a/internal/cac/storage/multi.go +++ b/internal/cac/storage/multi.go @@ -3,10 +3,9 @@ package storage import ( "context" "fmt" + "log/slog" - "github.com/cloudentity/acp-client-go/clients/hub/models" "github.com/cloudentity/cac/internal/cac/api" - "github.com/imdario/mergo" "github.com/pkg/errors" ) @@ -50,26 +49,33 @@ var _ Storage = &MultiStorage{} var _ api.Source = &MultiStorage{} // Write for simplicity stores data in first storage only, it is responsibility of the user to move entities to other storages -func (m *MultiStorage) Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { +func (m *MultiStorage) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { + slog.Debug("Writing data to multi storage") return m.Storages[0].Write(ctx, data, opts...) } // Read data from all storages and merge them -func (m *MultiStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) { +func (m *MultiStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { var ( - data = models.Rfc7396PatchOperation{} + data api.PatchInterface err error ) + slog.Debug("Reading data from multi storage") + for i := len(m.Storages) - 1; i >= 0; i-- { - var data2 models.Rfc7396PatchOperation + var data2 api.PatchInterface if data2, err = m.Storages[i].Read(ctx, opts...); err != nil { - return data, errors.Wrap(err, "failed to read data from storage") + return nil, errors.Wrap(err, "failed to read data from storage") } - if err = mergo.Merge(&data, data2, mergo.WithOverride); err != nil { - return data, errors.Wrap(err, "failed to merge data") + if data == nil { + data = data2 + } else { + if err = data.Merge(data2); err != nil { + return nil, errors.Wrap(err, "failed to merge data") + } } } diff --git a/internal/cac/storage/reader.go b/internal/cac/storage/reader.go index 168f5f9..7c02f41 100644 --- a/internal/cac/storage/reader.go +++ b/internal/cac/storage/reader.go @@ -4,10 +4,10 @@ import ( "os" "path/filepath" + "github.com/cloudentity/cac/internal/cac/logging" "github.com/cloudentity/cac/internal/cac/templates" ccyaml "github.com/goccy/go-yaml" "github.com/pkg/errors" - "golang.org/x/exp/slog" ) type ReadFileOpts struct { @@ -30,25 +30,25 @@ func readFile(path string, opts ...ReadFileOpt) (map[string]any, error) { path += ".yaml" } - slog.Debug("reading file", "path", path) + logging.Trace("reading file", "path", path) if bts, err = templates.New(path).Render(); err != nil { if os.IsNotExist(err) { - slog.Debug("file not found", "path", path) + logging.Trace("file not found", "path", path) return out, nil } return out, errors.Wrapf(err, "failed to render template %s", path) } - slog.Debug("read template", "path", path, "data", bts) + logging.Trace("read template", "path", path, "data", bts) if err = ccyaml.Unmarshal(bts, &out); err != nil { return out, errors.Wrapf(err, "failed to unmarshal template %s", path) } - slog.Debug("read yaml", "path", path, "out", out) + logging.Trace("read yaml", "path", path, "out", out) return out, nil } @@ -78,7 +78,7 @@ func readFiles(path string, opts ...ReadFileOpt) (map[string]any, error) { ) if ext != ".yaml" && ext != ".yml" { - slog.Debug("skipping not yaml file", "name", name) + logging.Trace("skipping not yaml file", "name", name) continue } diff --git a/internal/cac/storage/secrets_test.go b/internal/cac/storage/secrets_test.go new file mode 100644 index 0000000..a2a5f7c --- /dev/null +++ b/internal/cac/storage/secrets_test.go @@ -0,0 +1,133 @@ +package storage_test + +import ( + "context" + "os" + "testing" + + "github.com/cloudentity/acp-client-go/clients/hub/models" + smodels "github.com/cloudentity/acp-client-go/clients/system/models" + "github.com/cloudentity/cac/internal/cac/api" + "github.com/cloudentity/cac/internal/cac/logging" + "github.com/cloudentity/cac/internal/cac/storage" + "github.com/cloudentity/cac/internal/cac/utils" + "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/assert/yaml" + "github.com/stretchr/testify/require" +) + + +func TestWritingSecrets(t *testing.T) { + var dateTime, _ = strfmt.ParseDateTime("2024-01-23T23:19:30.004+01:00") + + data := &models.Rfc7396PatchOperation{} + + err := logging.InitLogging(&logging.Configuration{ + Level: "debug", + }) + + require.NoError(t, err) + + st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ + DirPath: []string{t.TempDir(), t.TempDir()}, + }, storage.InitServerStorage) + + require.NoError(t, err) + + err = st.Write(context.Background(), &api.ServerPatch{ + Data: *data, + Ext: &api.ServerExtensions{ + Secrets: map[string]*smodels.Secret{ + "Some_secret": &smodels.Secret{ + ID: "Some_secret", + Value: "test", + CreatedAt: dateTime, + }, + }, + }, + }, api.WithWorkspace("demo"), api.WithSecrets(true)) + + require.NoError(t, err) + + files, err := storage.ListFilesInDirectories(st.Config.DirPath...) + + require.NoError(t, err) + require.ElementsMatch(t, []string{"workspaces/demo/server.yaml", "workspaces/demo/secrets/Some_secret.yaml"}, files) + + bts, err := os.ReadFile(st.Config.DirPath[0] + "/workspaces/demo/secrets/Some_secret.yaml") + require.NoError(t, err) + + var secret smodels.Secret + err = yaml.Unmarshal(bts, &secret) + + require.NoError(t, err) + + require.Equal(t, "Some_secret", secret.ID) + require.Equal(t, "test", secret.Value) + require.Equal(t, dateTime, secret.CreatedAt) +} + + +func TestReadingSecrets(t *testing.T) { + var dateTime, _ = strfmt.ParseDateTime("2024-01-23T23:19:30.004+01:00") + + data := smodels.Secret{ + ID: "Some_secret", + Value: "test", + CreatedAt: dateTime, + } + + tmpDir := t.TempDir() + + yml, err := utils.ToYaml(data) + + require.NoError(t, err) + + os.MkdirAll(tmpDir+"/workspaces/demo/secrets", 0755) + + err = os.WriteFile(tmpDir+"/workspaces/demo/secrets/Some_secret.yaml", yml, 0644) + require.NoError(t, err) + + server := models.TreeServer{ + Name: "demo workspace", + } + + yml, err = utils.ToYaml(server) + require.NoError(t, err) + + err = os.WriteFile(tmpDir+"/workspaces/demo/server.yaml", yml, 0644) + require.NoError(t, err) + + err = logging.InitLogging(&logging.Configuration{ + Level: "debug", + }) + require.NoError(t, err) + + st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ + DirPath: []string{t.TempDir(), tmpDir}, + }, storage.InitServerStorage) + + require.NoError(t, err) + + readData, err := st.Read(context.Background(), api.WithWorkspace("demo"), api.WithSecrets(true)) + + require.NoError(t, err) + + require.NotNil(t, readData) + + files, err := storage.ListFilesInDirectories(st.Config.DirPath...) + require.NoError(t, err) + require.ElementsMatch(t, []string{"workspaces/demo/server.yaml", "workspaces/demo/secrets/Some_secret.yaml"}, files) + + ext, ok := readData.GetExtensions().(*api.ServerExtensions) + require.True(t, ok) + + secrets := ext.Secrets + require.Len(t, secrets, 1) + + secret, ok := secrets["Some_secret"] + require.True(t, ok) + + require.Equal(t, "test", secret.Value) + require.Equal(t, dateTime.String(), secret.CreatedAt.String()) +} \ No newline at end of file diff --git a/internal/cac/storage/server_storage.go b/internal/cac/storage/server_storage.go index 702ee15..86648a4 100644 --- a/internal/cac/storage/server_storage.go +++ b/internal/cac/storage/server_storage.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" + "github.com/cloudentity/acp-client-go/clients/hub/models" smodels "github.com/cloudentity/acp-client-go/clients/system/models" "github.com/cloudentity/cac/internal/cac/api" @@ -11,7 +13,6 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/maps" "golang.org/x/exp/slog" - "path/filepath" ) type Configuration struct { @@ -35,7 +36,7 @@ type ServerStorage struct { var _ Storage = &ServerStorage{} var _ api.Source = &ServerStorage{} -func (s *ServerStorage) Write(ctx context.Context, input models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { +func (s *ServerStorage) Write(ctx context.Context, input api.PatchInterface, opts ...api.SourceOpt) error { var ( workspacePath string workspace string @@ -47,6 +48,8 @@ func (s *ServerStorage) Write(ctx context.Context, input models.Rfc7396PatchOper for _, opt := range opts { opt(options) } + + slog.Debug("write server data", "options", options) if workspace = options.Workspace; workspace == "" { return errors.New("workspace is required to write to server storage") @@ -54,106 +57,122 @@ func (s *ServerStorage) Write(ctx context.Context, input models.Rfc7396PatchOper workspacePath = s.workspacePath(workspace) - if data, err = utils.FromPatchToModel[models.TreeServer](input); err != nil { + if data, err = utils.FromPatchToModel[models.TreeServer](input.GetData()); err != nil { return errors.Wrap(err, "failed to convert patch to tree server") } if err = s.storeServer(workspace, data); err != nil { - return err + return errors.Wrapf(err, "failed to store server data for workspace %s", workspace) } if err = writeFiles(data.Clients, filepath.Join(workspacePath, "clients"), func(id string, it models.TreeClient) string { return it.ClientName }); err != nil { - return err + return errors.Wrapf(err, "failed to write clients for workspace %s", workspace) } if err = writeFiles(data.Idps, filepath.Join(workspacePath, "idps"), func(id string, it models.TreeIDP) string { return it.Name }); err != nil { - return err + return errors.Wrapf(err, "failed to write idps for workspace %s", workspace) } if err = writeFile(data.Claims, filepath.Join(workspacePath, "claims")); err != nil { - return err + return errors.Wrapf(err, "failed to write claims for workspace %s", workspace) } if err = writeFiles(data.CustomApps, filepath.Join(workspacePath, "custom_apps"), func(id string, it models.TreeCustomApp) string { return it.Name }); err != nil { - return err + return errors.Wrapf(err, "failed to write custom apps for workspace %s", workspace) } if err = writeFiles(data.Gateways, filepath.Join(workspacePath, "gateways"), func(id string, it models.TreeGateway) string { return it.Name }); err != nil { - return err + return errors.Wrapf(err, "failed to write gateways for workspace %s", workspace) } if err = writeFile(data.PolicyExecutionPoints, filepath.Join(workspacePath, "policy_execution_points")); err != nil { - return err + return errors.Wrapf(err, "failed to write policy execution points for workspace %s", workspace) } if err = writeFiles(data.Pools, filepath.Join(workspacePath, "pools"), func(id string, it models.TreePool) string { return it.Name }); err != nil { - return err + return errors.Wrapf(err, "failed to write pools for workspace %s", workspace) } if err = writeFile(data.ScopesWithoutService, filepath.Join(workspacePath, "scopes")); err != nil { - return err + return errors.Wrapf(err, "failed to write scopes for workspace %s", workspace) } if err = writeFile(data.ScriptExecutionPoints, filepath.Join(workspacePath, "script_execution_points")); err != nil { - return err + return errors.Wrapf(err, "failed to write script execution points for workspace %s", workspace) } if err = writeFile(data.ServerConsent, filepath.Join(workspacePath, "consent")); err != nil { - return err + return errors.Wrapf(err, "failed to write server consent for workspace %s", workspace) } if len(data.ServersBindings) > 0 { if err = writeFile(map[string]any{ "bindings": maps.Keys(data.ServersBindings), }, filepath.Join(workspacePath, "servers_bindings")); err != nil { - return err + return errors.Wrapf(err, "failed to write server bindings for workspace %s", workspace) } } if err = writeFiles(data.Services, filepath.Join(workspacePath, "services"), func(id string, it models.TreeService) string { return it.Name }); err != nil { - return err + return errors.Wrapf(err, "failed to write services for workspace %s", workspace) } if data.ThemeBinding != nil && data.ThemeBinding.ThemeID != "" { if err = writeFile(data.ThemeBinding, filepath.Join(workspacePath, "theme_binding")); err != nil { - return err + return errors.Wrapf(err, "failed to write theme binding for workspace %s", workspace) } } if err = writeFiles(data.Webhooks, filepath.Join(workspacePath, "webhooks"), func(id string, it models.TreeWebhook) string { return id }); err != nil { - return err + return errors.Wrapf(err, "failed to write webhooks for workspace %s", workspace) } if err = writeFile(data.CibaAuthenticationService, filepath.Join(workspacePath, "ciba")); err != nil { - return err + return errors.Wrapf(err, "failed to write ciba authentication service for workspace %s", workspace) } if err = storeScripts(data.Scripts, filepath.Join(workspacePath, "scripts")); err != nil { - return err + return errors.Wrapf(err, "failed to store scripts for workspace %s", workspace) } if err = StorePolicies(data.Policies, filepath.Join(workspacePath, "policies")); err != nil { - return err + return errors.Wrapf(err, "failed to store policies for workspace %s", workspace) } - if err = writeFiles(data.Secrets, - filepath.Join(workspacePath, "secrets"), - func(id string, it models.TreeSecret) string { return id }); err != nil { - return err + if options.Secrets { + slog.Debug("trying to write secrets", "server", options.Workspace) + } + + if options.Secrets { + ext, ok := input.GetExtensions().(*api.ServerExtensions) + + if !ok { + return errors.New("extensions are required to write secrets") + } + + for _, secret := range ext.Secrets { + secret.Secret = "" // clear the secret to avoid storing encrypted secrets in the storage + } + + if err = writeFiles(ext.Secrets, + filepath.Join(workspacePath, "secrets"), + func(id string, it *smodels.Secret) string { return id }); err != nil { + return errors.Wrapf(err, "failed to write secrets for workspace %s", workspace) + } } slog.Info("Workspace configuration successfully stored", "workspace", workspace, "path", workspacePath) @@ -161,11 +180,12 @@ func (s *ServerStorage) Write(ctx context.Context, input models.Rfc7396PatchOper return nil } -func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) { +func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { var ( path string workspace string server models.Rfc7396PatchOperation + ext = models.Rfc7396PatchOperation{} options = &api.Options{} err error ) @@ -181,7 +201,7 @@ func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models path = s.workspacePath(workspace) if server, err = readFile(filepath.Join(path, "server")); err != nil { - return server, err + return nil, err } if err = readFilesToMap(server, "clients", filepath.Join(path, "clients")); err != nil { @@ -230,7 +250,7 @@ func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models var sb map[string]any if sb, err = readFile(filepath.Join(path, "servers_bindings")); err != nil { - return server, err + return nil, err } if bindings, ok := sb["bindings"].([]any); ok && len(bindings) != 0 { @@ -263,7 +283,7 @@ func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models return nil, err } - if err = readFilesToMap(server, "secrets", filepath.Join(path, "secrets")); err != nil { + if err = readFilesToMap(ext, "secrets", filepath.Join(path, "secrets")); err != nil { return nil, err } @@ -271,7 +291,16 @@ func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models return nil, err } - return server, nil + sext, err := utils.FromPatchToModel[api.ServerExtensions](ext) + + if err != nil { + return nil, errors.Wrap(err, "failed to convert extensions to model") + } + + return &api.ServerPatch{ + Data: server, + Ext: sext, + }, nil } func (s *ServerStorage) String() string { @@ -292,11 +321,11 @@ func (s *ServerStorage) storeServer(workspace string, data *models.TreeServer) e // serialize the server data into system/models to remove the dependencies which are stored in separate files if bts, err = json.Marshal(data); err != nil { - return err + return errors.Wrapf(err, "failed to marshal server data for workspace %s", workspace) } if err = json.Unmarshal(bts, &server); err != nil { - return err + return errors.Wrapf(err, "failed to unmarshal server data for workspace %s into system model", workspace) } server.ID = workspace diff --git a/internal/cac/storage/server_storage_test.go b/internal/cac/storage/server_storage_test.go index 2ed75d0..2137259 100644 --- a/internal/cac/storage/server_storage_test.go +++ b/internal/cac/storage/server_storage_test.go @@ -2,7 +2,6 @@ package storage_test import ( "context" - "io/fs" "os" "path/filepath" "slices" @@ -105,6 +104,9 @@ post_logout_redirect_uris: [] request_uris: [] require_pushed_authorization_requests: false rotated_secrets: [] +saml_allowed_attributes: [] +saml_metadata_updated_at: 0001-01-01T00:00:00.000Z +saml_override_attributes: false scopes: [] system: false tls_client_certificate_bound_access_tokens: false @@ -229,7 +231,8 @@ name: Some Gateway`, string(bts)) "workspaces/demo/pools/Some_Pool.yaml", }, assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `deleted: false + require.YAMLEq(t, `allow_skip_2fa: false +deleted: false id: some-pool identifier_case_insensitive: false mfa_session_ttl: 0s @@ -585,7 +588,8 @@ name: Some IDP static_amr: [] version: 0`, string(bts)) case "workspaces/demo/pools/Some_Pool.yaml": - require.YAMLEq(t, `deleted: false + require.YAMLEq(t, `allow_skip_2fa: false +deleted: false id: some-pool identifier_case_insensitive: false mfa_session_ttl: 0s @@ -596,25 +600,6 @@ system: false`, string(bts)) } }, }, - { - desc: "secrets", - data: &models.TreeServer{ - Secrets: models.TreeSecrets{ - "Some_secret": models.TreeSecret{ - CreatedAt: dateTime, - Secret: "test", - }, - }, - }, - files: []string{ - "workspaces/demo/secrets/Some_secret.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `created_at: 2024-01-23T23:19:30.004+01:00 -id: Some_secret -secret: test`, string(bts)) - }, - }, } for _, tc := range tcs { @@ -634,27 +619,12 @@ secret: test`, string(bts)) patchData, err := utils.FromModelToPatch(tc.data) require.NoError(t, err) - err = st.Write(context.Background(), patchData, api.WithWorkspace("demo")) + err = st.Write(context.Background(), &api.ServerPatch{ + Data: patchData, + }, api.WithWorkspace("demo")) require.NoError(t, err) - var files []string - - for _, dir := range st.Config.DirPath { - err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() { - if path, err = filepath.Rel(dir, path); err != nil { - return err - } - - files = append(files, path) - } - return nil - }) - } + files, err := storage.ListFilesInDirectories(st.Config.DirPath...) require.NoError(t, err) require.ElementsMatch(t, slices.Compact(append(tc.files, "workspaces/demo/server.yaml")), files) @@ -670,7 +640,7 @@ secret: test`, string(bts)) } } - var readServer models.Rfc7396PatchOperation + var readServer api.PatchInterface readServer, err = st.Read(context.Background(), api.WithWorkspace("demo"), api.WithFilters(tc.filters)) @@ -682,7 +652,9 @@ secret: test`, string(bts)) patchData, err = utils.FilterPatch(patchData, tc.filters) require.NoError(t, err) - d, err := diff.Tree(patchData, readServer) + d, err := diff.Tree(&api.ServerPatch{ + Data: patchData, + }, readServer) require.NoError(t, err) require.Empty(t, d) }) diff --git a/internal/cac/storage/storage.go b/internal/cac/storage/storage.go index 9720fba..d5a08fe 100644 --- a/internal/cac/storage/storage.go +++ b/internal/cac/storage/storage.go @@ -2,11 +2,10 @@ package storage import ( "context" - "github.com/cloudentity/acp-client-go/clients/hub/models" "github.com/cloudentity/cac/internal/cac/api" ) type Storage interface { - Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error - Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) + Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error + Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) } diff --git a/internal/cac/storage/tenant_storage.go b/internal/cac/storage/tenant_storage.go index b2ba635..1c505c3 100644 --- a/internal/cac/storage/tenant_storage.go +++ b/internal/cac/storage/tenant_storage.go @@ -1,222 +1,248 @@ package storage import ( - "context" - "github.com/cloudentity/acp-client-go/clients/hub/models" - "github.com/cloudentity/cac/internal/cac/api" - "github.com/cloudentity/cac/internal/cac/utils" - "path/filepath" + "context" + "log/slog" + "path/filepath" + + "github.com/cloudentity/acp-client-go/clients/hub/models" + "github.com/cloudentity/cac/internal/cac/api" + "github.com/cloudentity/cac/internal/cac/utils" + "github.com/pkg/errors" ) func InitTenantStorage(config *Configuration) Storage { - return &TenantStorage{ - Config: config, - ServerStorage: InitServerStorage(config), - } + return &TenantStorage{ + Config: config, + ServerStorage: InitServerStorage(config), + } } type TenantStorage struct { - Config *Configuration - ServerStorage Storage + Config *Configuration + ServerStorage Storage } -func (t *TenantStorage) Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error { - var ( - path = t.Config.DirPath - model *models.TreeTenant - err error - ) - - if model, err = utils.FromPatchToModel[models.TreeTenant](data); err != nil { - return err - } - - if err = writeFiles(model.Pools, - filepath.Join(path, "pools"), - func(id string, it models.TreePool) string { return it.Name }); err != nil { - return err - } - - if err = writeFiles(model.Schemas, - filepath.Join(path, "schemas"), - func(id string, it models.TreeSchema) string { return it.Name }); err != nil { - return err - } - - if err = writeFiles(model.MfaMethods, - filepath.Join(path, "mfa_methods"), - func(id string, it models.TreeMFAMethod) string { return it.Mechanism }); err != nil { - return err - } - - for _, theme := range model.Themes { - var ( - themePath = filepath.Join(path, "themes", normalize(theme.Name)) - themeConfig models.Rfc7396PatchOperation - ) - - if themeConfig, err = utils.FromModelToPatch(&theme); err != nil { - return err +func (t *TenantStorage) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { + var ( + path = t.Config.DirPath + model *models.TreeTenant + err error + ) + + slog.Debug("Writing tenant data", + "path", path, + "data", data.GetData(), + "extensions", data.GetExtensions(), + "workspace", opts, + ) + + if model, err = utils.FromPatchToModel[models.TreeTenant](data.GetData()); err != nil { + return err + } + + if err = writeFiles(model.Pools, + filepath.Join(path, "pools"), + func(id string, it models.TreePool) string { return it.Name }); err != nil { + return err + } + + if err = writeFiles(model.Schemas, + filepath.Join(path, "schemas"), + func(id string, it models.TreeSchema) string { return it.Name }); err != nil { + return err + } + + if err = writeFiles(model.MfaMethods, + filepath.Join(path, "mfa_methods"), + func(id string, it models.TreeMFAMethod) string { return it.Mechanism }); err != nil { + return err + } + + for _, theme := range model.Themes { + var ( + themePath = filepath.Join(path, "themes", normalize(theme.Name)) + themeConfig models.Rfc7396PatchOperation + ) + + if themeConfig, err = utils.FromModelToPatch(&theme); err != nil { + return err + } + + delete(themeConfig, "templates") + + if err = writeFile(themeConfig, filepath.Join(themePath, "theme")); err != nil { + return err + } + + if err = storeTemplates(theme.Templates, filepath.Join(themePath, "templates")); err != nil { + return err + } + } + + for k, server := range model.Servers { + opts = append(opts, api.WithWorkspace(k)) + + var ( + serverData models.Rfc7396PatchOperation + ) + + if serverData, err = utils.FromModelToPatch(&server); err != nil { + return err + } + + ext, ok := data.GetExtensions().(*api.TenantExtensions) + + if !ok { + return errors.New("invalid extensions type, expected *api.TenantExtensions") } - delete(themeConfig, "templates") + if err = t.ServerStorage.Write(ctx, &api.ServerPatch{ + Data: serverData, + Ext: ext.GetServerExtensions(k), + }, opts...); err != nil { + return err + } + } - if err = writeFile(themeConfig, filepath.Join(themePath, "theme")); err != nil { - return err - } + return nil +} - if err = storeTemplates(theme.Templates, filepath.Join(themePath, "templates")); err != nil { - return err - } - } +func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { + var ( + path = t.Config.DirPath + tenant models.Rfc7396PatchOperation + ext = api.TenantExtensions{} + options = &api.Options{} + themeDirs []string + workspaces []string + err error + ) - for k, server := range model.Servers { - opts = append(opts, api.WithWorkspace(k)) - var serverData models.Rfc7396PatchOperation - if serverData, err = utils.FromModelToPatch(&server); err != nil { - return err - } + for _, opt := range opts { + opt(options) + } - if err = t.ServerStorage.Write(ctx, serverData, opts...); err != nil { - return err - } - } + if tenant, err = readFile(filepath.Join(path, "tenant")); err != nil { + return nil, err + } - return nil -} + if err = readFilesToMap(tenant, "pools", filepath.Join(path, "pools")); err != nil { + return nil, err + } -func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc7396PatchOperation, error) { - var ( - path = t.Config.DirPath - tenant models.Rfc7396PatchOperation - options = &api.Options{} - themeDirs []string - workspaces []string - err error - ) - - for _, opt := range opts { - opt(options) - } - - if tenant, err = readFile(filepath.Join(path, "tenant")); err != nil { - return nil, err - } - - if err = readFilesToMap(tenant, "pools", filepath.Join(path, "pools")); err != nil { - return nil, err - } - - if err = readFilesToMap(tenant, "schemas", filepath.Join(path, "schemas")); err != nil { - return nil, err - } - - if err = readFilesToMap(tenant, "mfa_methods", filepath.Join(path, "mfa_methods")); err != nil { - return nil, err - } - - if err = readFilesToMap(tenant, "themes", filepath.Join(path, "themes")); err != nil { - return nil, err - } - - if themeDirs, err = listDirsInPath(filepath.Join(path, "themes")); err != nil { - return nil, err - } - - themes := models.TreeThemes{} - - for _, dir := range themeDirs { - var ( - themeConfig map[string]any - theme *models.TreeTheme - ) - - if themeConfig, err = readFile(filepath.Join(path, "themes", dir, "theme")); err != nil { - return nil, err - } + if err = readFilesToMap(tenant, "schemas", filepath.Join(path, "schemas")); err != nil { + return nil, err + } - if theme, err = utils.FromPatchToModel[models.TreeTheme](themeConfig); err != nil { - return nil, err - } + if err = readFilesToMap(tenant, "mfa_methods", filepath.Join(path, "mfa_methods")); err != nil { + return nil, err + } - var ( - templates *models.TreeTemplates - templatesConfig map[string]any - ) + if err = readFilesToMap(tenant, "themes", filepath.Join(path, "themes")); err != nil { + return nil, err + } - if templatesConfig, err = readFiles(filepath.Join(path, "themes", dir, "templates")); err != nil { - return nil, err - } + if themeDirs, err = listDirsInPath(filepath.Join(path, "themes")); err != nil { + return nil, err + } - if templates, err = utils.FromPatchToModel[models.TreeTemplates](templatesConfig); err != nil { - return nil, err - } + themes := models.TreeThemes{} - theme.Templates = *templates - themes[themeConfig["name"].(string)] = *theme - } + for _, dir := range themeDirs { + var ( + themeConfig map[string]any + theme *models.TreeTheme + ) - if len(themes) > 0 { - tenant["themes"] = themes - } + if themeConfig, err = readFile(filepath.Join(path, "themes", dir, "theme")); err != nil { + return nil, err + } - if workspaces, err = listDirsInPath(filepath.Join(path, "workspaces")); err != nil { - return nil, err - } + if theme, err = utils.FromPatchToModel[models.TreeTheme](themeConfig); err != nil { + return nil, err + } - if len(workspaces) > 0 { - var servers = map[string]any{} + var ( + templates *models.TreeTemplates + templatesConfig map[string]any + ) - for _, workspace := range workspaces { - var workspaceConfig models.Rfc7396PatchOperation + if templatesConfig, err = readFiles(filepath.Join(path, "themes", dir, "templates")); err != nil { + return nil, err + } - opts = append(opts, api.WithWorkspace(workspace), api.WithFilters([]string{})) + if templates, err = utils.FromPatchToModel[models.TreeTemplates](templatesConfig); err != nil { + return nil, err + } - if workspaceConfig, err = t.ServerStorage.Read(ctx, opts...); err != nil { - return nil, err - } + theme.Templates = *templates + themes[themeConfig["name"].(string)] = *theme + } - id := workspaceConfig["id"].(string) - delete(workspaceConfig, "id") - delete(workspaceConfig, "tenant_id") - servers[id] = workspaceConfig - } + if len(themes) > 0 { + tenant["themes"] = themes + } - tenant["servers"] = servers - } + if workspaces, err = listDirsInPath(filepath.Join(path, "workspaces")); err != nil { + return nil, err + } - if tenant, err = utils.FilterPatch(tenant, options.Filters); err != nil { - return nil, err - } + if len(workspaces) > 0 { + var servers = map[string]any{} - return tenant, nil -} + for _, workspace := range workspaces { + var workspaceConfig api.PatchInterface -var _ Storage = &TenantStorage{} + opts = append(opts, api.WithWorkspace(workspace), api.WithFilters([]string{})) -func storeTemplates(templates models.TreeTemplates, path string) error { - for id, template := range templates { - var ( - sc = NewWithID(id, template) - name = normalize(id) - raw Writer[[]byte] - err error - ) - - if raw, err = RawWriter(path); err != nil { - return err - } + if workspaceConfig, err = t.ServerStorage.Read(ctx, opts...); err != nil { + return nil, err + } - if err = raw(name, []byte(sc.Other.Content)); err != nil { - return err - } + data := workspaceConfig.GetData() - sc.Other.Content = createMultilineIncludeTemplate(name, 2) + utils.CleanPatch(data) + } - if err = writeFile(sc, filepath.Join(path, name)); err != nil { - return err - } - } + tenant["servers"] = servers + } + + if tenant, err = utils.FilterPatch(tenant, options.Filters); err != nil { + return nil, err + } - return nil + return &api.TenantPatch{ + Data: tenant, + Ext: &ext, + }, nil +} + +var _ Storage = &TenantStorage{} + +func storeTemplates(templates models.TreeTemplates, path string) error { + for id, template := range templates { + var ( + sc = NewWithID(id, template) + name = normalize(id) + raw Writer[[]byte] + err error + ) + + if raw, err = RawWriter(path); err != nil { + return err + } + + if err = raw(name, []byte(sc.Other.Content)); err != nil { + return err + } + + sc.Other.Content = createMultilineIncludeTemplate(name, 2) + + if err = writeFile(sc, filepath.Join(path, name)); err != nil { + return err + } + } + + return nil } diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index 447a575..a5c485c 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -203,7 +203,10 @@ updated_at: 0001-01-01T00:00:00.000Z patchData, err := utils.FromModelToPatch(tc.data) require.NoError(t, err) - err = st.Write(context.Background(), patchData, api.WithWorkspace("demo")) + err = st.Write(context.Background(), &api.TenantPatch{ + Data: patchData, + Ext: &api.TenantExtensions{}, + }, api.WithWorkspace("demo")) require.NoError(t, err) var files []string @@ -239,7 +242,7 @@ updated_at: 0001-01-01T00:00:00.000Z } } - var readServer models.Rfc7396PatchOperation + var readServer api.PatchInterface readServer, err = st.Read(context.Background(), api.WithWorkspace("demo"), api.WithFilters(tc.filters)) @@ -250,7 +253,10 @@ updated_at: 0001-01-01T00:00:00.000Z patchData, err = utils.FilterPatch(patchData, tc.filters) require.NoError(t, err) - d, err := diff.Tree(patchData, readServer) + d, err := diff.Tree(&api.TenantPatch{ + Data: patchData, + Ext: &api.TenantExtensions{}, + }, readServer) require.NoError(t, err) require.Empty(t, d) }) diff --git a/internal/cac/storage/test_utils.go b/internal/cac/storage/test_utils.go new file mode 100644 index 0000000..b800e28 --- /dev/null +++ b/internal/cac/storage/test_utils.go @@ -0,0 +1,52 @@ +package storage + +import ( + "os" + "path/filepath" +) + +// var files []string + +// for _, dir := range st.Config.DirPath { +// err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { +// if err != nil { +// return err +// } + +// if !info.IsDir() { +// if path, err = filepath.Rel(dir, path); err != nil { +// return err +// } + +// files = append(files, path) +// } +// return nil +// }) +// } + +// write a function that returns a list of all files in the input directories +func ListFilesInDirectories(dirs ...string) ([]string, error) { + var files []string + + for _, dir := range dirs { + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + files = append(files, relPath) + } + return nil + }) + if err != nil { + return nil, err + } + } + + return files, nil +} \ No newline at end of file diff --git a/internal/cac/storage/writer.go b/internal/cac/storage/writer.go index ffc9082..edb9bcb 100644 --- a/internal/cac/storage/writer.go +++ b/internal/cac/storage/writer.go @@ -2,14 +2,15 @@ package storage import ( "fmt" - "github.com/cloudentity/cac/internal/cac/utils" "os" "path/filepath" "reflect" "regexp" "strings" - "golang.org/x/exp/slog" + "github.com/cloudentity/cac/internal/cac/logging" + "github.com/cloudentity/cac/internal/cac/utils" + "github.com/pkg/errors" ) type Writer[T any] func(name string, it T) error @@ -63,16 +64,16 @@ func writeFile[T any](data T, path string) error { ) if reflect.ValueOf(data).IsZero() { - slog.Debug("skipping empty file", "path", path) + logging.Trace("skipping empty file", "path", path) return nil } if writer, err = YAMLWriter[T](parent); err != nil { - return err + return errors.Wrapf(err, "failed to create YAML writer for path %s", parent) } if err = writer(filepath.Base(path), data); err != nil { - return err + return errors.Wrapf(err, "failed to write file %s", path) } return nil @@ -84,7 +85,7 @@ func YAMLWriter[T any](dirPath string) (Writer[T], error) { err error ) if raw, err = RawWriter(dirPath); err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to create raw writer for path %s", dirPath) } return func(name string, it T) error { @@ -96,13 +97,13 @@ func YAMLWriter[T any](dirPath string) (Writer[T], error) { name += ".yaml" if bts, err = utils.ToYaml(it); err != nil { - return err + return errors.Wrapf(err, "failed to marshal %T to yaml", it) } bts = postProcessMultilineTemplates(bts) if err = raw(name, bts); err != nil { - return err + return errors.Wrapf(err, "failed to write yaml file %s", name) } return nil @@ -120,7 +121,7 @@ func RawWriter(dirPath string) (Writer[[]byte], error) { err error ) - slog.Debug("writing file", "path", filepath.Join(dirPath, name), "data", string(bts)) + logging.Trace("writing file", "path", filepath.Join(dirPath, name), "data", string(bts)) if name == "" { return fmt.Errorf("file name cannot be empty") @@ -133,16 +134,16 @@ func RawWriter(dirPath string) (Writer[[]byte], error) { name = normalize(name) if file, err = os.Create(filepath.Join(dirPath, name)); err != nil && !os.IsExist(err) { - return err + return errors.Wrapf(err, "failed to create file %s", filepath.Join(dirPath, name)) } defer file.Close() if _, err = file.Write(bts); err != nil { - return err + return errors.Wrapf(err, "failed to write data to file %s", filepath.Join(dirPath, name)) } - slog.Debug("wrote file", "path", filepath.Join(dirPath, name), "data", string(bts)) + logging.Trace("wrote file", "path", filepath.Join(dirPath, name), "data", string(bts)) return nil }, nil diff --git a/internal/cac/templates/functions.go b/internal/cac/templates/functions.go index 5e74e8c..01bd61e 100644 --- a/internal/cac/templates/functions.go +++ b/internal/cac/templates/functions.go @@ -2,12 +2,12 @@ package templates import ( "fmt" - "log/slog" "os" "path/filepath" "strings" "text/template" + "github.com/cloudentity/cac/internal/cac/logging" zb32 "github.com/corvus-ch/zbase32" "github.com/pkg/errors" @@ -42,7 +42,7 @@ func include(t *Template) func(string) (string, error) { } str = string(bts) - slog.Debug("including file", "path", fp, "data", str) + logging.Trace("including file", "path", fp, "data", str) return str, nil } diff --git a/internal/cac/templates/model.go b/internal/cac/templates/model.go index 957bb85..64545a0 100644 --- a/internal/cac/templates/model.go +++ b/internal/cac/templates/model.go @@ -2,9 +2,10 @@ package templates import ( "bytes" - "golang.org/x/exp/slog" "os" "text/template" + + "github.com/cloudentity/cac/internal/cac/logging" ) type Template struct { @@ -27,7 +28,7 @@ func (t *Template) Render() ([]byte, error) { return nil, err } - slog.Debug("rendering template", "path", t.Path, "data", string(bts)) + logging.Trace("rendering template", "path", t.Path, "data", string(bts)) if tmpl, err = template.New(t.Path).Funcs(functions(t)).Parse(string(bts)); err != nil { return nil, err diff --git a/internal/cac/utils/model.go b/internal/cac/utils/model.go index 6b74f82..2138531 100644 --- a/internal/cac/utils/model.go +++ b/internal/cac/utils/model.go @@ -25,6 +25,10 @@ func FromModelToPatch[T any](data *T) (models.Rfc7396PatchOperation, error) { } func FromPatchToModel[T any](patch models.Rfc7396PatchOperation) (*T, error) { + return FromPatchToModelWithOptions[T](patch, json.RejectUnknownMembers(true)) +} + +func FromPatchToModelWithOptions[T any](patch models.Rfc7396PatchOperation, unOpts... json.Options) (*T, error) { var ( out = new(T) bts []byte @@ -37,7 +41,7 @@ func FromPatchToModel[T any](patch models.Rfc7396PatchOperation) (*T, error) { return out, errors.Wrap(err, "failed to marshal patch to json") } - if err = json.Unmarshal(bts, out, json.RejectUnknownMembers(true)); err != nil { + if err = json.Unmarshal(bts, out, unOpts...); err != nil { return out, errors.Wrapf(err, "failed to unmarshal json to %T", out) } @@ -62,8 +66,8 @@ func NormalizePatch(patch models.Rfc7396PatchOperation) (models.Rfc7396PatchOper return out, nil } -// CleanPatch cleans fields that are available in system model but not available in hub model func CleanPatch(patch models.Rfc7396PatchOperation) { + // clean fields that are available in system model but not available in hub model delete(patch, "id") delete(patch, "tenant_id") } diff --git a/internal/cac/utils/model_test.go b/internal/cac/utils/model_test.go index 27d748e..11f19e7 100644 --- a/internal/cac/utils/model_test.go +++ b/internal/cac/utils/model_test.go @@ -1,11 +1,12 @@ package utils_test import ( + "reflect" + "testing" + "github.com/cloudentity/acp-client-go/clients/hub/models" "github.com/cloudentity/cac/internal/cac/utils" "github.com/stretchr/testify/require" - "reflect" - "testing" ) func TestFilterPatch(t *testing.T) { From 1ce7a1b2b03185c2de30875fa56d070602433e89 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 09:35:30 +0200 Subject: [PATCH 4/9] rename interface --- cmd/pull.go | 4 +- cmd/push.go | 12 +- internal/cac/api/model.go | 24 +- internal/cac/api/source.go | 4 +- internal/cac/client/client.go | 10 +- internal/cac/client/tenant_client.go | 4 +- internal/cac/data/server_validator.go | 2 +- internal/cac/data/tenant_validator.go | 2 +- internal/cac/data/validator.go | 2 +- internal/cac/diff/diff.go | 9 +- internal/cac/storage/dry.go | 12 +- internal/cac/storage/multi.go | 10 +- internal/cac/storage/server_storage.go | 8 +- internal/cac/storage/server_storage_test.go | 882 ++++++++++---------- internal/cac/storage/storage.go | 5 +- internal/cac/storage/tenant_storage.go | 38 +- internal/cac/storage/tenant_storage_test.go | 387 ++++----- 17 files changed, 710 insertions(+), 705 deletions(-) diff --git a/cmd/pull.go b/cmd/pull.go index 3110c41..9aa749d 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -18,7 +18,7 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { var ( app *cac.Application - data api.PatchInterface + data api.Patch err error ) @@ -43,7 +43,7 @@ var ( } if pullConfig.Out == "" { - // default + // default if err = app.Storage.Write(cmd.Context(), data, api.WithWorkspace(rootConfig.Workspace), api.WithSecrets(pullConfig.WithSecrets)); err != nil { return err } diff --git a/cmd/push.go b/cmd/push.go index fb8fce8..6305608 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -16,7 +16,7 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { var ( app *cac.Application - data api.PatchInterface + data api.Patch err error ) @@ -77,11 +77,11 @@ var ( }, } pushConfig struct { - DryRun bool - Out string - Mode string - Method string - Filters []string + DryRun bool + Out string + Mode string + Method string + Filters []string NoLocalValidate bool } ) diff --git a/internal/cac/api/model.go b/internal/cac/api/model.go index 3760982..b40c1f1 100644 --- a/internal/cac/api/model.go +++ b/internal/cac/api/model.go @@ -26,18 +26,20 @@ func (te *TenantExtensions) GetServerExtensions(serverID string) *ServerExtensio return nil } -type Patch[T any] struct { +type Patch interface { + GetData() models.Rfc7396PatchOperation + GetExtensions() any + Merge(other Patch) error +} + +type PatchImpl[T any] struct { Data models.Rfc7396PatchOperation `json:"data,omitempty"` Ext *T `json:"ext,omitempty"` } -type PatchInterface interface { - GetData() models.Rfc7396PatchOperation - GetExtensions() any - Merge(other PatchInterface) error -} +type ServerPatch PatchImpl[ServerExtensions] -type ServerPatch Patch[ServerExtensions] +var _ Patch = &ServerPatch{} func (sp *ServerPatch) GetData() models.Rfc7396PatchOperation { return sp.Data @@ -45,7 +47,7 @@ func (sp *ServerPatch) GetData() models.Rfc7396PatchOperation { func (tp *ServerPatch) GetExtensions() any { return tp.Ext } -func (sp *ServerPatch) Merge(other PatchInterface) error { +func (sp *ServerPatch) Merge(other Patch) error { if err := mergo.Merge(&sp.Data, other.GetData(), mergo.WithOverride); err != nil { return err } @@ -57,7 +59,7 @@ func (sp *ServerPatch) Merge(other PatchInterface) error { return nil } -type TenantPatch Patch[TenantExtensions] +type TenantPatch PatchImpl[TenantExtensions] func (tp *TenantPatch) GetData() models.Rfc7396PatchOperation { return tp.Data @@ -65,7 +67,7 @@ func (tp *TenantPatch) GetData() models.Rfc7396PatchOperation { func (tp *TenantPatch) GetExtensions() any { return tp.Ext } -func (sp *TenantPatch) Merge(other PatchInterface) error { +func (sp *TenantPatch) Merge(other Patch) error { if err := mergo.Merge(&sp.Data, other.GetData(), mergo.WithOverride); err != nil { return err } @@ -75,4 +77,4 @@ func (sp *TenantPatch) Merge(other PatchInterface) error { } return nil -} \ No newline at end of file +} diff --git a/internal/cac/api/source.go b/internal/cac/api/source.go index 5ddaf38..e22e16b 100644 --- a/internal/cac/api/source.go +++ b/internal/cac/api/source.go @@ -36,8 +36,8 @@ type Options struct { type SourceOpt func(*Options) type Source interface { - Read(ctx context.Context, opts ...SourceOpt) (PatchInterface, error) - Write(ctx context.Context, data PatchInterface, opts ...SourceOpt) error + Read(ctx context.Context, opts ...SourceOpt) (Patch, error) + Write(ctx context.Context, data Patch, opts ...SourceOpt) error String() string } diff --git a/internal/cac/client/client.go b/internal/cac/client/client.go index c116153..be79232 100644 --- a/internal/cac/client/client.go +++ b/internal/cac/client/client.go @@ -6,7 +6,7 @@ import ( "fmt" "net/http" - "github.com/cloudentity/acp-client-go" + acpclient "github.com/cloudentity/acp-client-go" "github.com/cloudentity/acp-client-go/clients/hub/client/workspace_configuration" "github.com/cloudentity/acp-client-go/clients/hub/models" smodels "github.com/cloudentity/acp-client-go/clients/system/models" @@ -52,7 +52,7 @@ func InitClient(config *Configuration) (c *Client, err error) { }, nil } -func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { +func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) { var ( options = &api.Options{} ok *workspace_configuration.ExportWorkspaceConfigOK @@ -106,7 +106,7 @@ func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInte }, nil } -func (c *Client) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { +func (c *Client) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( options = &api.Options{} workspace string @@ -139,7 +139,7 @@ func (c *Client) Write(ctx context.Context, data api.PatchInterface, opts ...api return nil } -func (c *Client) Patch(ctx context.Context, workspace string, mode string, data api.PatchInterface) error { +func (c *Client) Patch(ctx context.Context, workspace string, mode string, data api.Patch) error { var ( err error ) @@ -158,7 +158,7 @@ func (c *Client) Patch(ctx context.Context, workspace string, mode string, data return nil } -func (c *Client) Import(ctx context.Context, workspace string, mode string, data api.PatchInterface) error { +func (c *Client) Import(ctx context.Context, workspace string, mode string, data api.Patch) error { var ( err error out *models.TreeServer diff --git a/internal/cac/client/tenant_client.go b/internal/cac/client/tenant_client.go index 0c77c2d..69040a5 100644 --- a/internal/cac/client/tenant_client.go +++ b/internal/cac/client/tenant_client.go @@ -18,7 +18,7 @@ type TenantClient struct { sec *SecretsClient } -func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { +func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) { var ( ok *tenant_configuration.ExportTenantConfigOK options = &api.Options{} @@ -74,7 +74,7 @@ func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pat }, nil } -func (t *TenantClient) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { +func (t *TenantClient) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( options = &api.Options{} err error diff --git a/internal/cac/data/server_validator.go b/internal/cac/data/server_validator.go index 3db657b..be603d5 100644 --- a/internal/cac/data/server_validator.go +++ b/internal/cac/data/server_validator.go @@ -11,7 +11,7 @@ type ServerValidator struct{} var _ ValidatorApi = &ServerValidator{} -func (sv *ServerValidator) Validate(data api.PatchInterface) error { +func (sv *ServerValidator) Validate(data api.Patch) error { var ( err error serv *models.TreeServer diff --git a/internal/cac/data/tenant_validator.go b/internal/cac/data/tenant_validator.go index 7502517..ecbc2c9 100644 --- a/internal/cac/data/tenant_validator.go +++ b/internal/cac/data/tenant_validator.go @@ -11,7 +11,7 @@ type TenantValidator struct{} var _ ValidatorApi = &TenantValidator{} -func (sv *TenantValidator) Validate(data api.PatchInterface) error { +func (sv *TenantValidator) Validate(data api.Patch) error { var ( err error tenant *models.TreeTenant diff --git a/internal/cac/data/validator.go b/internal/cac/data/validator.go index b5aa156..43d0414 100644 --- a/internal/cac/data/validator.go +++ b/internal/cac/data/validator.go @@ -5,5 +5,5 @@ import ( ) type ValidatorApi interface { - Validate(data api.PatchInterface) error + Validate(data api.Patch) error } diff --git a/internal/cac/diff/diff.go b/internal/cac/diff/diff.go index 003888f..8d1bfe2 100644 --- a/internal/cac/diff/diff.go +++ b/internal/cac/diff/diff.go @@ -2,11 +2,12 @@ package diff import ( "context" + "regexp" + "github.com/cloudentity/cac/internal/cac/api" "github.com/cloudentity/cac/internal/cac/utils" "github.com/google/go-cmp/cmp" "golang.org/x/exp/slog" - "regexp" ) type Options struct { @@ -89,8 +90,8 @@ var filterSecretFields = fieldsFilter(secretFields) func Diff(ctx context.Context, source api.Source, target api.Source, workspace string, opts ...Option) (string, error) { var ( - server1 api.PatchInterface - server2 api.PatchInterface + server1 api.Patch + server2 api.Patch options = &Options{} readOpts []api.SourceOpt err error @@ -123,7 +124,7 @@ func Diff(ctx context.Context, source api.Source, target api.Source, workspace s return Tree(server1, server2, opts...) } -func Tree(source api.PatchInterface, target api.PatchInterface, opts ...Option) (string, error) { +func Tree(source api.Patch, target api.Patch, opts ...Option) (string, error) { var ( options = &Options{} diffOpts = cmp.Options{} diff --git a/internal/cac/storage/dry.go b/internal/cac/storage/dry.go index e137626..dc28d07 100644 --- a/internal/cac/storage/dry.go +++ b/internal/cac/storage/dry.go @@ -23,7 +23,7 @@ func InitDryStorage(out string, constr Constructor) (*DryStorage, error) { if out == "-" { logging.Trace("Writing to stdout") - delegatedWriter = func(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { + delegatedWriter = func(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( bts []byte err error @@ -75,17 +75,17 @@ func InitDryStorage(out string, constr Constructor) (*DryStorage, error) { }, nil } -type WriterFunc func(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error +type WriterFunc func(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error -func (d *DryStorage) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { +func (d *DryStorage) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { return d.DelegatedWriter(ctx, data, opts...) } -func (d *DryStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { +func (d *DryStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) { panic("read operation is not implemented for dry storage") } -var stdWriter = func(ctx context.Context, data *api.Patch[any], opts ...api.SourceOpt) error { +var stdWriter = func(ctx context.Context, data *api.PatchImpl[any], opts ...api.SourceOpt) error { var ( bts []byte err error @@ -99,7 +99,7 @@ var stdWriter = func(ctx context.Context, data *api.Patch[any], opts ...api.Sour } var flatFileWriter = func(out string) WriterFunc { - return func(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { + return func(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( bts []byte err error diff --git a/internal/cac/storage/multi.go b/internal/cac/storage/multi.go index 64bedaa..700e568 100644 --- a/internal/cac/storage/multi.go +++ b/internal/cac/storage/multi.go @@ -49,22 +49,22 @@ var _ Storage = &MultiStorage{} var _ api.Source = &MultiStorage{} // Write for simplicity stores data in first storage only, it is responsibility of the user to move entities to other storages -func (m *MultiStorage) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { - slog.Debug("Writing data to multi storage") +func (m *MultiStorage) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { + slog.Debug("Writing data to multi storage") return m.Storages[0].Write(ctx, data, opts...) } // Read data from all storages and merge them -func (m *MultiStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { +func (m *MultiStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) { var ( - data api.PatchInterface + data api.Patch err error ) slog.Debug("Reading data from multi storage") for i := len(m.Storages) - 1; i >= 0; i-- { - var data2 api.PatchInterface + var data2 api.Patch if data2, err = m.Storages[i].Read(ctx, opts...); err != nil { return nil, errors.Wrap(err, "failed to read data from storage") diff --git a/internal/cac/storage/server_storage.go b/internal/cac/storage/server_storage.go index 86648a4..2a9a8b0 100644 --- a/internal/cac/storage/server_storage.go +++ b/internal/cac/storage/server_storage.go @@ -36,7 +36,7 @@ type ServerStorage struct { var _ Storage = &ServerStorage{} var _ api.Source = &ServerStorage{} -func (s *ServerStorage) Write(ctx context.Context, input api.PatchInterface, opts ...api.SourceOpt) error { +func (s *ServerStorage) Write(ctx context.Context, input api.Patch, opts ...api.SourceOpt) error { var ( workspacePath string workspace string @@ -48,7 +48,7 @@ func (s *ServerStorage) Write(ctx context.Context, input api.PatchInterface, opt for _, opt := range opts { opt(options) } - + slog.Debug("write server data", "options", options) if workspace = options.Workspace; workspace == "" { @@ -167,7 +167,7 @@ func (s *ServerStorage) Write(ctx context.Context, input api.PatchInterface, opt for _, secret := range ext.Secrets { secret.Secret = "" // clear the secret to avoid storing encrypted secrets in the storage } - + if err = writeFiles(ext.Secrets, filepath.Join(workspacePath, "secrets"), func(id string, it *smodels.Secret) string { return id }); err != nil { @@ -180,7 +180,7 @@ func (s *ServerStorage) Write(ctx context.Context, input api.PatchInterface, opt return nil } -func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { +func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) { var ( path string workspace string diff --git a/internal/cac/storage/server_storage_test.go b/internal/cac/storage/server_storage_test.go index 2137259..757b834 100644 --- a/internal/cac/storage/server_storage_test.go +++ b/internal/cac/storage/server_storage_test.go @@ -22,24 +22,24 @@ import ( var dateTime, _ = strfmt.ParseDateTime("2024-01-23T23:19:30.004+01:00") func TestStorage(t *testing.T) { - tcs := []struct { - desc string - data *models.TreeServer - files []string - filters []string - assert func(t *testing.T, path string, bts []byte) - }{ - { - desc: "server", - data: &models.TreeServer{ - Name: "demo workspace", - AccessTokenTTL: strfmt.Duration(10 * time.Minute), - }, - files: []string{ - "workspaces/demo/server.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `access_token_ttl: 10m0s + tcs := []struct { + desc string + data *models.TreeServer + files []string + filters []string + assert func(t *testing.T, path string, bts []byte) + }{ + { + desc: "server", + data: &models.TreeServer{ + Name: "demo workspace", + AccessTokenTTL: strfmt.Duration(10 * time.Minute), + }, + files: []string{ + "workspaces/demo/server.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `access_token_ttl: 10m0s authentication_mechanisms: [] authorization_code_ttl: 0s scope_claim_formats: [] @@ -70,22 +70,22 @@ token_endpoint_auth_methods: [] token_endpoint_auth_signing_alg_values: [] token_endpoint_authn_methods: [] version: 0`, string(bts)) - }, - }, - { - desc: "clients", - data: &models.TreeServer{ - Clients: models.TreeClients{ - "demo-demo": models.TreeClient{ - ClientName: "Demo Portal", - }, - }, - }, - files: []string{ - "workspaces/demo/clients/Demo_Portal.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `application_types: [] + }, + }, + { + desc: "clients", + data: &models.TreeServer{ + Clients: models.TreeClients{ + "demo-demo": models.TreeClient{ + ClientName: "Demo Portal", + }, + }, + }, + files: []string{ + "workspaces/demo/clients/Demo_Portal.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `application_types: [] audience: [] authorization_details_types: [] backchannel_logout_session_required: false @@ -113,49 +113,49 @@ tls_client_certificate_bound_access_tokens: false trusted: false updated_at: 0001-01-01T00:00:00.000Z use_custom_token_ttls: false`, string(bts)) - }, - }, - { - desc: "idps", - data: &models.TreeServer{ - Idps: models.TreeIDPs{ - "some-idp": models.TreeIDP{ - Name: "Some IDP", - }, - }, - }, - files: []string{ - "workspaces/demo/idps/Some_IDP.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `disabled: false + }, + }, + { + desc: "idps", + data: &models.TreeServer{ + Idps: models.TreeIDPs{ + "some-idp": models.TreeIDP{ + Name: "Some IDP", + }, + }, + }, + files: []string{ + "workspaces/demo/idps/Some_IDP.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `disabled: false display_order: 0 hidden: false id: some-idp name: Some IDP static_amr: [] version: 0`, string(bts)) - }, - }, - { - desc: "claims", - data: &models.TreeServer{ - Claims: models.TreeClaims{ - "access_token": models.TreeClaimType{ - "customer_id": models.TreeClaim{ - Mapping: "customer_id", - Scopes: []string{"customer"}, - SourcePath: "customer_id", - SourceType: "authnCtx", - }, - }, - }, - }, - files: []string{ - "workspaces/demo/claims.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `access_token: + }, + }, + { + desc: "claims", + data: &models.TreeServer{ + Claims: models.TreeClaims{ + "access_token": models.TreeClaimType{ + "customer_id": models.TreeClaim{ + Mapping: "customer_id", + Scopes: []string{"customer"}, + SourcePath: "customer_id", + SourceType: "authnCtx", + }, + }, + }, + }, + files: []string{ + "workspaces/demo/claims.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `access_token: customer_id: mapping: customer_id opaque: false @@ -164,74 +164,74 @@ version: 0`, string(bts)) source_path: customer_id source_type: authnCtx verified: false`, string(bts)) - }, - }, - { - desc: "custom_apps", - data: &models.TreeServer{ - CustomApps: models.TreeCustomApps{ - "some-app": models.TreeCustomApp{ - Name: "Some App", - URL: "https://some-app.com", - }, - }, - }, - files: []string{ - "workspaces/demo/custom_apps/Some_App.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `id: some-app + }, + }, + { + desc: "custom_apps", + data: &models.TreeServer{ + CustomApps: models.TreeCustomApps{ + "some-app": models.TreeCustomApp{ + Name: "Some App", + URL: "https://some-app.com", + }, + }, + }, + files: []string{ + "workspaces/demo/custom_apps/Some_App.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `id: some-app name: Some App url: https://some-app.com`, string(bts)) - }, - }, - { - desc: "gateways", - data: &models.TreeServer{ - Gateways: models.TreeGateways{ - "some-gateway": models.TreeGateway{ - Name: "Some Gateway", - }, - }, - }, - files: []string{ - "workspaces/demo/gateways/Some_Gateway.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `create_and_bind_services_automatically: false + }, + }, + { + desc: "gateways", + data: &models.TreeServer{ + Gateways: models.TreeGateways{ + "some-gateway": models.TreeGateway{ + Name: "Some Gateway", + }, + }, + }, + files: []string{ + "workspaces/demo/gateways/Some_Gateway.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `create_and_bind_services_automatically: false id: some-gateway last_active: 0001-01-01T00:00:00.000Z name: Some Gateway`, string(bts)) - }, - }, - { - desc: "policy_execution_points", - data: &models.TreeServer{ - PolicyExecutionPoints: models.TreePolicyExecutionPoints{ - "server_user_token": "some_policy_id", - }, - }, - files: []string{ - "workspaces/demo/policy_execution_points.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `server_user_token: some_policy_id`, string(bts)) - }, - }, - { - desc: "pools", - data: &models.TreeServer{ - Pools: models.TreePools{ - "some-pool": models.TreePool{ - Name: "Some Pool", - }, - }, - }, - files: []string{ - "workspaces/demo/pools/Some_Pool.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `allow_skip_2fa: false + }, + }, + { + desc: "policy_execution_points", + data: &models.TreeServer{ + PolicyExecutionPoints: models.TreePolicyExecutionPoints{ + "server_user_token": "some_policy_id", + }, + }, + files: []string{ + "workspaces/demo/policy_execution_points.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `server_user_token: some_policy_id`, string(bts)) + }, + }, + { + desc: "pools", + data: &models.TreeServer{ + Pools: models.TreePools{ + "some-pool": models.TreePool{ + Name: "Some Pool", + }, + }, + }, + files: []string{ + "workspaces/demo/pools/Some_Pool.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `allow_skip_2fa: false deleted: false id: some-pool identifier_case_insensitive: false @@ -240,112 +240,112 @@ name: Some Pool public_registration_allowed: false second_factor_threshold: 0 system: false`, string(bts)) - }, - }, - { - desc: "scopes", - data: &models.TreeServer{ - ScopesWithoutService: models.TreeScopes{ - "some_scope": models.TreeScope{ - Description: "Some Scope", - PolicyExecutionPoints: models.TreePolicyExecutionPoints{ - "scope_user_grant": "some_policy_id", - }, - Transient: false, - }, - }, - }, - files: []string{ - "workspaces/demo/scopes.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `some_scope: + }, + }, + { + desc: "scopes", + data: &models.TreeServer{ + ScopesWithoutService: models.TreeScopes{ + "some_scope": models.TreeScope{ + Description: "Some Scope", + PolicyExecutionPoints: models.TreePolicyExecutionPoints{ + "scope_user_grant": "some_policy_id", + }, + Transient: false, + }, + }, + }, + files: []string{ + "workspaces/demo/scopes.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `some_scope: description: Some Scope implicit: false implicit_grant: false policy_execution_points: scope_user_grant: some_policy_id transient: false`, string(bts)) - }, - }, - { - desc: "script_execution_points", - data: &models.TreeServer{ - ScriptExecutionPoints: models.TreeScriptExecutionPoints{ - "token_minting": { - "demo": models.TreeScriptExecutionPoint{ - ScriptID: "some_script_id", - }, - }, - }, - }, - files: []string{ - "workspaces/demo/script_execution_points.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `token_minting: + }, + }, + { + desc: "script_execution_points", + data: &models.TreeServer{ + ScriptExecutionPoints: models.TreeScriptExecutionPoints{ + "token_minting": { + "demo": models.TreeScriptExecutionPoint{ + ScriptID: "some_script_id", + }, + }, + }, + }, + files: []string{ + "workspaces/demo/script_execution_points.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `token_minting: demo: script_id: some_script_id`, string(bts)) - }, - }, - { - desc: "consent", - data: &models.TreeServer{ - ServerConsent: &models.TreeServerConsent{ - Custom: &models.CustomServerConsent{ - ServerConsentURL: "https://example.com/consent", - }, - Type: "custom", - }, - }, - files: []string{ - "workspaces/demo/consent.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `custom: + }, + }, + { + desc: "consent", + data: &models.TreeServer{ + ServerConsent: &models.TreeServerConsent{ + Custom: &models.CustomServerConsent{ + ServerConsentURL: "https://example.com/consent", + }, + Type: "custom", + }, + }, + files: []string{ + "workspaces/demo/consent.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `custom: server_consent_url: https://example.com/consent type: custom`, string(bts)) - }, - }, - { - desc: "server bindings", - data: &models.TreeServer{ - ServersBindings: models.TreeServersBindings{ - "other_server": true, - }, - }, - files: []string{ - "workspaces/demo/servers_bindings.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `bindings: + }, + }, + { + desc: "server bindings", + data: &models.TreeServer{ + ServersBindings: models.TreeServersBindings{ + "other_server": true, + }, + }, + files: []string{ + "workspaces/demo/servers_bindings.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `bindings: - other_server`, string(bts)) - }, - }, - { - desc: "services", - data: &models.TreeServer{ - Services: models.TreeServices{ - "some_service": models.TreeService{ - Name: "Some Service", - UpdatedAt: dateTime, - CustomAudience: "some_custom_audience", - Scopes: models.TreeScopes{ - "some_scope": models.TreeScope{ - Description: "Some Scope", - PolicyExecutionPoints: models.TreePolicyExecutionPoints{ - "scope_user_grant": "some_policy_id", - }, - }, - }, - }, - }, - }, - files: []string{ - "workspaces/demo/services/Some_Service.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `id: some_service + }, + }, + { + desc: "services", + data: &models.TreeServer{ + Services: models.TreeServices{ + "some_service": models.TreeService{ + Name: "Some Service", + UpdatedAt: dateTime, + CustomAudience: "some_custom_audience", + Scopes: models.TreeScopes{ + "some_scope": models.TreeScope{ + Description: "Some Scope", + PolicyExecutionPoints: models.TreePolicyExecutionPoints{ + "scope_user_grant": "some_policy_id", + }, + }, + }, + }, + }, + }, + files: []string{ + "workspaces/demo/services/Some_Service.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `id: some_service name: Some Service scopes: some_scope: @@ -359,114 +359,114 @@ system: false custom_audience: some_custom_audience updated_at: 2024-01-23T23:19:30.004+01:00 with_specification: false`, string(bts)) - }, - }, - { - desc: "theme binding", - data: &models.TreeServer{ - ThemeBinding: &models.TreeThemeBinding{ - ThemeID: "some_theme", - }, - }, - files: []string{ - "workspaces/demo/theme_binding.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `theme_id: some_theme`, string(bts)) - }, - }, - { - desc: "webhooks", - data: &models.TreeServer{ - Webhooks: models.TreeWebhooks{ - "hook_id": models.TreeWebhook{ - Active: true, - URL: "https://example.com", - }, - }, - }, - files: []string{ - "workspaces/demo/webhooks/hook_id.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `active: true + }, + }, + { + desc: "theme binding", + data: &models.TreeServer{ + ThemeBinding: &models.TreeThemeBinding{ + ThemeID: "some_theme", + }, + }, + files: []string{ + "workspaces/demo/theme_binding.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `theme_id: some_theme`, string(bts)) + }, + }, + { + desc: "webhooks", + data: &models.TreeServer{ + Webhooks: models.TreeWebhooks{ + "hook_id": models.TreeWebhook{ + Active: true, + URL: "https://example.com", + }, + }, + }, + files: []string{ + "workspaces/demo/webhooks/hook_id.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `active: true id: hook_id insecure: false url: https://example.com`, string(bts)) - }, - }, - { - desc: "rego policies", - data: &models.TreeServer{ - Policies: models.TreePolicies{ - "some_policy": models.TreePolicy{ - Definition: ` + }, + }, + { + desc: "rego policies", + data: &models.TreeServer{ + Policies: models.TreePolicies{ + "some_policy": models.TreePolicy{ + Definition: ` package acp.authz default allow = false`, - Language: "rego", - PolicyName: "Some Rego Policy", - Type: "api", - }, - }, - }, - files: []string{ - "workspaces/demo/policies/Some_Rego_Policy.yaml", - "workspaces/demo/policies/Some_Rego_Policy.rego", - }, - assert: func(t *testing.T, path string, bts []byte) { - if strings.Contains(path, ".yaml") { - require.Equal(t, `id: some_policy + Language: "rego", + PolicyName: "Some Rego Policy", + Type: "api", + }, + }, + }, + files: []string{ + "workspaces/demo/policies/Some_Rego_Policy.yaml", + "workspaces/demo/policies/Some_Rego_Policy.rego", + }, + assert: func(t *testing.T, path string, bts []byte) { + if strings.Contains(path, ".yaml") { + require.Equal(t, `id: some_policy definition: {{ include "Some_Rego_Policy.rego" | nindent 2 }} language: rego policy_name: Some Rego Policy type: api validators: [] `, string(bts)) - } else { - require.Equal(t, ` + } else { + require.Equal(t, ` package acp.authz default allow = false`, string(bts)) - } - }, - }, - { - desc: "ce policies", - data: &models.TreeServer{ - Policies: models.TreePolicies{ - "some_policy": models.TreePolicy{ - Language: "cloudentity", - PolicyName: "Some CE Policy", - Type: "api", - Validators: []*models.ValidatorConfig{ - { - Conf: map[string]any{ - "fields": []map[string]any{ - { - "comparator": "contains", - "field": "login.verified_recovery_methods", - "value": []string{ - "mfa", - }, - }, - }, - }, - Recovery: []*models.RecoveryConfig{ - { - Type: "mfa", - }, - }, - }, - }, - }, - }, - }, - files: []string{ - "workspaces/demo/policies/Some_CE_Policy.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - require.YAMLEq(t, `id: some_policy + } + }, + }, + { + desc: "ce policies", + data: &models.TreeServer{ + Policies: models.TreePolicies{ + "some_policy": models.TreePolicy{ + Language: "cloudentity", + PolicyName: "Some CE Policy", + Type: "api", + Validators: []*models.ValidatorConfig{ + { + Conf: map[string]any{ + "fields": []map[string]any{ + { + "comparator": "contains", + "field": "login.verified_recovery_methods", + "value": []string{ + "mfa", + }, + }, + }, + }, + Recovery: []*models.RecoveryConfig{ + { + Type: "mfa", + }, + }, + }, + }, + }, + }, + }, + files: []string{ + "workspaces/demo/policies/Some_CE_Policy.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + require.YAMLEq(t, `id: some_policy language: cloudentity policy_name: Some CE Policy type: api @@ -480,115 +480,115 @@ validators: recovery: - type: mfa `, string(bts)) - }, - }, - { - desc: "js extensions (with tabs)", - data: &models.TreeServer{ - Scripts: models.TreeScripts{ - "some_script": models.TreeScript{ - Body: `module.exports = async function(context) { + }, + }, + { + desc: "js extensions (with tabs)", + data: &models.TreeServer{ + Scripts: models.TreeScripts{ + "some_script": models.TreeScript{ + Body: `module.exports = async function(context) { return { access_token: { x: "123" } }; }`, - Name: "Some Script", - }, - }, - }, - files: []string{ - "workspaces/demo/scripts/Some_Script.yaml", - "workspaces/demo/scripts/Some_Script.js", - }, - assert: func(t *testing.T, path string, bts []byte) { - if strings.Contains(path, ".yaml") { - require.Equal(t, `id: some_script + Name: "Some Script", + }, + }, + }, + files: []string{ + "workspaces/demo/scripts/Some_Script.yaml", + "workspaces/demo/scripts/Some_Script.js", + }, + assert: func(t *testing.T, path string, bts []byte) { + if strings.Contains(path, ".yaml") { + require.Equal(t, `id: some_script body: {{ include "Some_Script.js" | nindent 2 }} name: Some Script `, string(bts)) - } else { - require.Equal(t, `module.exports = async function(context) { + } else { + require.Equal(t, `module.exports = async function(context) { return { access_token: { x: "123" } }; }`, string(bts)) - } + } - }, - }, - { - desc: "js extensions (with spaces)", - data: &models.TreeServer{ - Scripts: models.TreeScripts{ - "some_script": models.TreeScript{ - Body: `module.exports = async function(context) { + }, + }, + { + desc: "js extensions (with spaces)", + data: &models.TreeServer{ + Scripts: models.TreeScripts{ + "some_script": models.TreeScript{ + Body: `module.exports = async function(context) { return { access_token: { x: "123" } }; }`, - Name: "Some Script", - }, - }, - }, - files: []string{ - "workspaces/demo/scripts/Some_Script.yaml", - "workspaces/demo/scripts/Some_Script.js", - }, - assert: func(t *testing.T, path string, bts []byte) { - if strings.Contains(path, ".yaml") { - require.Equal(t, `id: some_script + Name: "Some Script", + }, + }, + }, + files: []string{ + "workspaces/demo/scripts/Some_Script.yaml", + "workspaces/demo/scripts/Some_Script.js", + }, + assert: func(t *testing.T, path string, bts []byte) { + if strings.Contains(path, ".yaml") { + require.Equal(t, `id: some_script body: {{ include "Some_Script.js" | nindent 2 }} name: Some Script `, string(bts)) - } else { - require.Equal(t, `module.exports = async function(context) { + } else { + require.Equal(t, `module.exports = async function(context) { return { access_token: { x: "123" } }; }`, string(bts)) - } + } - }, - }, - { - desc: "idps, with filters", - data: &models.TreeServer{ - Idps: models.TreeIDPs{ - "some-idp": models.TreeIDP{ - Name: "Some IDP", - }, - }, - Pools: models.TreePools{ - "some-pool": models.TreePool{ - Name: "Some Pool", - }, - }, - }, - files: []string{ - "workspaces/demo/idps/Some_IDP.yaml", - "workspaces/demo/pools/Some_Pool.yaml", - }, - filters: []string{"idps"}, - assert: func(t *testing.T, path string, bts []byte) { - switch path { - case "workspaces/demo/idps/Some_IDP.yaml": - require.YAMLEq(t, `disabled: false + }, + }, + { + desc: "idps, with filters", + data: &models.TreeServer{ + Idps: models.TreeIDPs{ + "some-idp": models.TreeIDP{ + Name: "Some IDP", + }, + }, + Pools: models.TreePools{ + "some-pool": models.TreePool{ + Name: "Some Pool", + }, + }, + }, + files: []string{ + "workspaces/demo/idps/Some_IDP.yaml", + "workspaces/demo/pools/Some_Pool.yaml", + }, + filters: []string{"idps"}, + assert: func(t *testing.T, path string, bts []byte) { + switch path { + case "workspaces/demo/idps/Some_IDP.yaml": + require.YAMLEq(t, `disabled: false display_order: 0 hidden: false id: some-idp name: Some IDP static_amr: [] version: 0`, string(bts)) - case "workspaces/demo/pools/Some_Pool.yaml": - require.YAMLEq(t, `allow_skip_2fa: false + case "workspaces/demo/pools/Some_Pool.yaml": + require.YAMLEq(t, `allow_skip_2fa: false deleted: false id: some-pool identifier_case_insensitive: false @@ -597,66 +597,66 @@ name: Some Pool public_registration_allowed: false second_factor_threshold: 0 system: false`, string(bts)) - } - }, - }, - } - - for _, tc := range tcs { - t.Run(tc.desc, func(t *testing.T) { - err := logging.InitLogging(&logging.Configuration{ - Level: "debug", - }) + } + }, + }, + } - require.NoError(t, err) + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + err := logging.InitLogging(&logging.Configuration{ + Level: "debug", + }) - st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ - DirPath: []string{t.TempDir(), t.TempDir()}, - }, storage.InitServerStorage) + require.NoError(t, err) - require.NoError(t, err) + st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ + DirPath: []string{t.TempDir(), t.TempDir()}, + }, storage.InitServerStorage) - patchData, err := utils.FromModelToPatch(tc.data) - require.NoError(t, err) + require.NoError(t, err) - err = st.Write(context.Background(), &api.ServerPatch{ - Data: patchData, - }, api.WithWorkspace("demo")) - require.NoError(t, err) + patchData, err := utils.FromModelToPatch(tc.data) + require.NoError(t, err) - files, err := storage.ListFilesInDirectories(st.Config.DirPath...) + err = st.Write(context.Background(), &api.ServerPatch{ + Data: patchData, + }, api.WithWorkspace("demo")) + require.NoError(t, err) - require.NoError(t, err) - require.ElementsMatch(t, slices.Compact(append(tc.files, "workspaces/demo/server.yaml")), files) + files, err := storage.ListFilesInDirectories(st.Config.DirPath...) - // checking if files written to fs have expected content - for _, f := range tc.files { - // using first dirpath as multi storage stores everything there - bts, err := os.ReadFile(filepath.Join(st.Config.DirPath[0], f)) - require.NoError(t, err) + require.NoError(t, err) + require.ElementsMatch(t, slices.Compact(append(tc.files, "workspaces/demo/server.yaml")), files) - if tc.assert != nil { - tc.assert(t, f, bts) - } - } + // checking if files written to fs have expected content + for _, f := range tc.files { + // using first dirpath as multi storage stores everything there + bts, err := os.ReadFile(filepath.Join(st.Config.DirPath[0], f)) + require.NoError(t, err) - var readServer api.PatchInterface - readServer, err = st.Read(context.Background(), - api.WithWorkspace("demo"), - api.WithFilters(tc.filters)) + if tc.assert != nil { + tc.assert(t, f, bts) + } + } - require.NoError(t, err) + var readServer api.Patch + readServer, err = st.Read(context.Background(), + api.WithWorkspace("demo"), + api.WithFilters(tc.filters)) - // verifying if the data read from fs is the same as the provided test data + require.NoError(t, err) - patchData, err = utils.FilterPatch(patchData, tc.filters) - require.NoError(t, err) + // verifying if the data read from fs is the same as the provided test data - d, err := diff.Tree(&api.ServerPatch{ - Data: patchData, - }, readServer) - require.NoError(t, err) - require.Empty(t, d) - }) - } + patchData, err = utils.FilterPatch(patchData, tc.filters) + require.NoError(t, err) + + d, err := diff.Tree(&api.ServerPatch{ + Data: patchData, + }, readServer) + require.NoError(t, err) + require.Empty(t, d) + }) + } } diff --git a/internal/cac/storage/storage.go b/internal/cac/storage/storage.go index d5a08fe..f48d7dc 100644 --- a/internal/cac/storage/storage.go +++ b/internal/cac/storage/storage.go @@ -2,10 +2,11 @@ package storage import ( "context" + "github.com/cloudentity/cac/internal/cac/api" ) type Storage interface { - Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error - Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) + Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error + Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) } diff --git a/internal/cac/storage/tenant_storage.go b/internal/cac/storage/tenant_storage.go index 1c505c3..3e25220 100644 --- a/internal/cac/storage/tenant_storage.go +++ b/internal/cac/storage/tenant_storage.go @@ -23,19 +23,19 @@ type TenantStorage struct { ServerStorage Storage } -func (t *TenantStorage) Write(ctx context.Context, data api.PatchInterface, opts ...api.SourceOpt) error { +func (t *TenantStorage) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( path = t.Config.DirPath model *models.TreeTenant err error ) - slog.Debug("Writing tenant data", - "path", path, - "data", data.GetData(), - "extensions", data.GetExtensions(), - "workspace", opts, - ) + slog.Debug("Writing tenant data", + "path", path, + "data", data.GetData(), + "extensions", data.GetExtensions(), + "workspace", opts, + ) if model, err = utils.FromPatchToModel[models.TreeTenant](data.GetData()); err != nil { return err @@ -91,11 +91,11 @@ func (t *TenantStorage) Write(ctx context.Context, data api.PatchInterface, opts return err } - ext, ok := data.GetExtensions().(*api.TenantExtensions) + ext, ok := data.GetExtensions().(*api.TenantExtensions) - if !ok { - return errors.New("invalid extensions type, expected *api.TenantExtensions") - } + if !ok { + return errors.New("invalid extensions type, expected *api.TenantExtensions") + } if err = t.ServerStorage.Write(ctx, &api.ServerPatch{ Data: serverData, @@ -108,11 +108,11 @@ func (t *TenantStorage) Write(ctx context.Context, data api.PatchInterface, opts return nil } -func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.PatchInterface, error) { +func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, error) { var ( path = t.Config.DirPath tenant models.Rfc7396PatchOperation - ext = api.TenantExtensions{} + ext = api.TenantExtensions{} options = &api.Options{} themeDirs []string workspaces []string @@ -192,7 +192,7 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pa var servers = map[string]any{} for _, workspace := range workspaces { - var workspaceConfig api.PatchInterface + var workspaceConfig api.Patch opts = append(opts, api.WithWorkspace(workspace), api.WithFilters([]string{})) @@ -200,9 +200,9 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pa return nil, err } - data := workspaceConfig.GetData() + data := workspaceConfig.GetData() - utils.CleanPatch(data) + utils.CleanPatch(data) } tenant["servers"] = servers @@ -213,9 +213,9 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pa } return &api.TenantPatch{ - Data: tenant, - Ext: &ext, - }, nil + Data: tenant, + Ext: &ext, + }, nil } var _ Storage = &TenantStorage{} diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index a5c485c..a64d38b 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -1,65 +1,66 @@ package storage_test import ( - "context" - "github.com/cloudentity/acp-client-go/clients/hub/models" - "github.com/cloudentity/cac/internal/cac/api" - "github.com/cloudentity/cac/internal/cac/diff" - "github.com/cloudentity/cac/internal/cac/logging" - "github.com/cloudentity/cac/internal/cac/storage" - "github.com/cloudentity/cac/internal/cac/utils" - "github.com/go-openapi/strfmt" - "github.com/stretchr/testify/require" - "io/fs" - "os" - "path/filepath" - "testing" - "time" + "context" + "io/fs" + "os" + "path/filepath" + "testing" + "time" + + "github.com/cloudentity/acp-client-go/clients/hub/models" + "github.com/cloudentity/cac/internal/cac/api" + "github.com/cloudentity/cac/internal/cac/diff" + "github.com/cloudentity/cac/internal/cac/logging" + "github.com/cloudentity/cac/internal/cac/storage" + "github.com/cloudentity/cac/internal/cac/utils" + "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/require" ) func TestTenantStorage(t *testing.T) { - tcs := []struct { - desc string - data *models.TreeTenant - files []string - filters []string - assert func(t *testing.T, path string, bts []byte) - }{ - { - desc: "workspace and mfa_methods", - data: &models.TreeTenant{ - Servers: models.TreeServers{ - "demo": models.TreeServer{ - Name: "demo workspace", - AccessTokenTTL: strfmt.Duration(time.Minute * 10), - Idps: models.TreeIDPs{ - "oidc": models.TreeIDP{ - Name: "oidc", - Disabled: true, - }, - }, - }, - }, - MfaMethods: models.TreeMFAMethods{ - "sms": models.TreeMFAMethod{ - Enabled: true, - Mechanism: "sms", - }, - }, - }, - files: []string{ - "mfa_methods/sms.yaml", - "workspaces/demo/server.yaml", - "workspaces/demo/idps/oidc.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - switch path { - case "mfa_methods/sms.yaml": - require.YAMLEq(t, `enabled: true + tcs := []struct { + desc string + data *models.TreeTenant + files []string + filters []string + assert func(t *testing.T, path string, bts []byte) + }{ + { + desc: "workspace and mfa_methods", + data: &models.TreeTenant{ + Servers: models.TreeServers{ + "demo": models.TreeServer{ + Name: "demo workspace", + AccessTokenTTL: strfmt.Duration(time.Minute * 10), + Idps: models.TreeIDPs{ + "oidc": models.TreeIDP{ + Name: "oidc", + Disabled: true, + }, + }, + }, + }, + MfaMethods: models.TreeMFAMethods{ + "sms": models.TreeMFAMethod{ + Enabled: true, + Mechanism: "sms", + }, + }, + }, + files: []string{ + "mfa_methods/sms.yaml", + "workspaces/demo/server.yaml", + "workspaces/demo/idps/oidc.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + switch path { + case "mfa_methods/sms.yaml": + require.YAMLEq(t, `enabled: true id: sms mechanism: sms`, string(bts)) - case "workspaces/demo/server.yaml": - require.YAMLEq(t, `access_token_ttl: 10m0s + case "workspaces/demo/server.yaml": + require.YAMLEq(t, `access_token_ttl: 10m0s authentication_mechanisms: [] authorization_code_ttl: 0s scope_claim_formats: [] @@ -90,175 +91,175 @@ token_endpoint_auth_methods: [] token_endpoint_auth_signing_alg_values: [] token_endpoint_authn_methods: [] version: 0`, string(bts)) - case "workspaces/demo/idps/oidc.yaml": - require.YAMLEq(t, `disabled: true + case "workspaces/demo/idps/oidc.yaml": + require.YAMLEq(t, `disabled: true display_order: 0 hidden: false id: oidc name: oidc static_amr: [] version: 0`, string(bts)) - } - }, - }, - { - desc: "filtered workspace and mfa_methods", - filters: []string{"mfa_methods"}, - data: &models.TreeTenant{ - Servers: models.TreeServers{ - "demo": models.TreeServer{ - Name: "demo workspace", - AccessTokenTTL: strfmt.Duration(time.Minute * 10), - Idps: models.TreeIDPs{ - "oidc": models.TreeIDP{ - Name: "oidc", - Disabled: true, - }, - }, - }, - }, - MfaMethods: models.TreeMFAMethods{ - "sms": models.TreeMFAMethod{ - Enabled: true, - Mechanism: "sms", - }, - }, - }, - files: []string{ - "mfa_methods/sms.yaml", - "workspaces/demo/server.yaml", - "workspaces/demo/idps/oidc.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - switch path { - case "mfa_methods/sms.yaml": - require.YAMLEq(t, `enabled: true + } + }, + }, + { + desc: "filtered workspace and mfa_methods", + filters: []string{"mfa_methods"}, + data: &models.TreeTenant{ + Servers: models.TreeServers{ + "demo": models.TreeServer{ + Name: "demo workspace", + AccessTokenTTL: strfmt.Duration(time.Minute * 10), + Idps: models.TreeIDPs{ + "oidc": models.TreeIDP{ + Name: "oidc", + Disabled: true, + }, + }, + }, + }, + MfaMethods: models.TreeMFAMethods{ + "sms": models.TreeMFAMethod{ + Enabled: true, + Mechanism: "sms", + }, + }, + }, + files: []string{ + "mfa_methods/sms.yaml", + "workspaces/demo/server.yaml", + "workspaces/demo/idps/oidc.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + switch path { + case "mfa_methods/sms.yaml": + require.YAMLEq(t, `enabled: true id: sms mechanism: sms`, string(bts)) - } - }, - }, - { - desc: "themes and templates", - data: &models.TreeTenant{ - Themes: models.TreeThemes{ - "theme1": models.TreeTheme{ - Name: "theme1", - Templates: models.TreeTemplates{ - "pages/error/index.tmpl": models.TreeTemplate{ - Content: "template1 content", - }, - "shared/footer.tmpl": models.TreeTemplate{ - Content: "footer content", - }, - }, - }, - }, - }, - files: []string{ - "themes/theme1/theme.yaml", - "themes/theme1/templates/pages_error_index.tmpl", - "themes/theme1/templates/pages_error_index.tmpl.yaml", - "themes/theme1/templates/shared_footer.tmpl", - "themes/theme1/templates/shared_footer.tmpl.yaml", - }, - assert: func(t *testing.T, path string, bts []byte) { - switch path { - case "themes/theme1/theme.yaml": - require.YAMLEq(t, `name: theme1`, string(bts)) - case "themes/theme1/templates/pages_error_index.tmpl": - require.Equal(t, "template1 content", string(bts)) - case "themes/theme1/templates/shared_footer.tmpl": - require.Equal(t, "footer content", string(bts)) - case "themes/theme1/templates/pages/error_index.tmpl.yaml": - require.Equal(t, `content: {{ include "error_index.tmpl" | nindent 2 }} + } + }, + }, + { + desc: "themes and templates", + data: &models.TreeTenant{ + Themes: models.TreeThemes{ + "theme1": models.TreeTheme{ + Name: "theme1", + Templates: models.TreeTemplates{ + "pages/error/index.tmpl": models.TreeTemplate{ + Content: "template1 content", + }, + "shared/footer.tmpl": models.TreeTemplate{ + Content: "footer content", + }, + }, + }, + }, + }, + files: []string{ + "themes/theme1/theme.yaml", + "themes/theme1/templates/pages_error_index.tmpl", + "themes/theme1/templates/pages_error_index.tmpl.yaml", + "themes/theme1/templates/shared_footer.tmpl", + "themes/theme1/templates/shared_footer.tmpl.yaml", + }, + assert: func(t *testing.T, path string, bts []byte) { + switch path { + case "themes/theme1/theme.yaml": + require.YAMLEq(t, `name: theme1`, string(bts)) + case "themes/theme1/templates/pages_error_index.tmpl": + require.Equal(t, "template1 content", string(bts)) + case "themes/theme1/templates/shared_footer.tmpl": + require.Equal(t, "footer content", string(bts)) + case "themes/theme1/templates/pages/error_index.tmpl.yaml": + require.Equal(t, `content: {{ include "error_index.tmpl" | nindent 2 }} created_at: "0001-01-01T00:00:00.000Z" id: pages/error/index.tmpl updated_at: "0001-01-01T00:00:00.000Z"`, string(bts)) - case "themes/theme1/templates/shared_footer.tmpl.yaml": - require.Equal(t, `id: shared/footer.tmpl + case "themes/theme1/templates/shared_footer.tmpl.yaml": + require.Equal(t, `id: shared/footer.tmpl content: {{ include "shared_footer.tmpl" | nindent 2 }} created_at: 0001-01-01T00:00:00.000Z updated_at: 0001-01-01T00:00:00.000Z `, string(bts)) - } - }, - }, - } + } + }, + }, + } - for _, tc := range tcs { - t.Run(tc.desc, func(t *testing.T) { - err := logging.InitLogging(&logging.Configuration{ - Level: "debug", - }) + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + err := logging.InitLogging(&logging.Configuration{ + Level: "debug", + }) - require.NoError(t, err) + require.NoError(t, err) - st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ - DirPath: []string{t.TempDir(), t.TempDir()}, - }, storage.InitTenantStorage) + st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ + DirPath: []string{t.TempDir(), t.TempDir()}, + }, storage.InitTenantStorage) - require.NoError(t, err) + require.NoError(t, err) - patchData, err := utils.FromModelToPatch(tc.data) - require.NoError(t, err) + patchData, err := utils.FromModelToPatch(tc.data) + require.NoError(t, err) - err = st.Write(context.Background(), &api.TenantPatch{ - Data: patchData, - Ext: &api.TenantExtensions{}, - }, api.WithWorkspace("demo")) - require.NoError(t, err) + err = st.Write(context.Background(), &api.TenantPatch{ + Data: patchData, + Ext: &api.TenantExtensions{}, + }, api.WithWorkspace("demo")) + require.NoError(t, err) - var files []string + var files []string - for _, dir := range st.Config.DirPath { - err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } + for _, dir := range st.Config.DirPath { + err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } - if !info.IsDir() { - if path, err = filepath.Rel(dir, path); err != nil { - return err - } + if !info.IsDir() { + if path, err = filepath.Rel(dir, path); err != nil { + return err + } - files = append(files, path) - } - return nil - }) - } + files = append(files, path) + } + return nil + }) + } - require.NoError(t, err) - require.ElementsMatch(t, tc.files, files) + require.NoError(t, err) + require.ElementsMatch(t, tc.files, files) - // checking if files written to fs have expected content - for _, f := range tc.files { - // using first dirpath as multi storage stores everything there - bts, err := os.ReadFile(filepath.Join(st.Config.DirPath[0], f)) - require.NoError(t, err) + // checking if files written to fs have expected content + for _, f := range tc.files { + // using first dirpath as multi storage stores everything there + bts, err := os.ReadFile(filepath.Join(st.Config.DirPath[0], f)) + require.NoError(t, err) - if tc.assert != nil { - tc.assert(t, f, bts) - } - } + if tc.assert != nil { + tc.assert(t, f, bts) + } + } - var readServer api.PatchInterface - readServer, err = st.Read(context.Background(), - api.WithWorkspace("demo"), - api.WithFilters(tc.filters)) + var readServer api.Patch + readServer, err = st.Read(context.Background(), + api.WithWorkspace("demo"), + api.WithFilters(tc.filters)) - require.NoError(t, err) + require.NoError(t, err) - // verifying if the data read from fs is the same as the provided test data - patchData, err = utils.FilterPatch(patchData, tc.filters) - require.NoError(t, err) + // verifying if the data read from fs is the same as the provided test data + patchData, err = utils.FilterPatch(patchData, tc.filters) + require.NoError(t, err) - d, err := diff.Tree(&api.TenantPatch{ - Data: patchData, - Ext: &api.TenantExtensions{}, - }, readServer) - require.NoError(t, err) - require.Empty(t, d) - }) - } + d, err := diff.Tree(&api.TenantPatch{ + Data: patchData, + Ext: &api.TenantExtensions{}, + }, readServer) + require.NoError(t, err) + require.Empty(t, d) + }) + } } From 33b738312c73d0d424a7d0f2c98568fa7fbdb689 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 11:06:15 +0200 Subject: [PATCH 5/9] fix lint --- internal/cac/client/errors.go | 18 +++++++++--------- internal/cac/client/secrets_client.go | 2 +- internal/cac/client/tenant_client.go | 2 +- internal/cac/logging/logging.go | 3 ++- internal/cac/storage/dry.go | 18 ++++-------------- internal/cac/storage/secrets_test.go | 3 ++- internal/cac/storage/writer.go | 1 + internal/cac/templates/functions.go | 2 +- 8 files changed, 21 insertions(+), 28 deletions(-) diff --git a/internal/cac/client/errors.go b/internal/cac/client/errors.go index 8921fe5..c57b7cf 100644 --- a/internal/cac/client/errors.go +++ b/internal/cac/client/errors.go @@ -3,26 +3,26 @@ package client import ( "reflect" - "github.com/cloudentity/acp-client-go/clients/hub/client/workspace_configuration" "github.com/go-openapi/runtime" "golang.org/x/exp/slog" ) func logErr(err error) { - switch e := err.(type) { - case *runtime.APIError: + if e, ok := err.(*runtime.APIError); ok { traceID := "" resp, ok := e.Response.(runtime.ClientResponse) if ok { traceID = resp.GetHeader("X-Trace-ID") } slog.Error("Request failed", "code", e.Code, "trace.id", traceID) - case *workspace_configuration.PatchWorkspaceConfigRfc7396UnprocessableEntity: - case *workspace_configuration.PatchWorkspaceConfigRfc6902BadRequest: - case *workspace_configuration.ImportWorkspaceConfigBadRequest: - case *workspace_configuration.ImportWorkspaceConfigUnprocessableEntity: - slog.Error("Request failed", "code", e.Code, "message", e.Payload.Error) - default: + } else if e, ok := err.(errr); ok{ + slog.Error("Request failed", "code", e.Code(), "message", e.Error()) + } else { slog.Error("Request failed", "error", reflect.TypeOf(err), "message", err.Error()) } +} + +type errr interface { + Error() string + Code() int } \ No newline at end of file diff --git a/internal/cac/client/secrets_client.go b/internal/cac/client/secrets_client.go index c329142..6c65437 100644 --- a/internal/cac/client/secrets_client.go +++ b/internal/cac/client/secrets_client.go @@ -78,7 +78,7 @@ func (s *SecretsClient) Update(ctx context.Context, wid string, payload models.S func (s *SecretsClient) UpdateAll(ctx context.Context, wid string, payload []models.Secret) error { return s.patchAll(ctx, wid, payload, func(dest *models.Secret, source models.Secret) error { - dest = &source + *dest = source return nil }) } diff --git a/internal/cac/client/tenant_client.go b/internal/cac/client/tenant_client.go index 69040a5..219387c 100644 --- a/internal/cac/client/tenant_client.go +++ b/internal/cac/client/tenant_client.go @@ -48,7 +48,7 @@ func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pat } if options.Secrets { - for id, _ := range ok.Payload.Servers { + for id := range ok.Payload.Servers { var secrets map[string]*smodels.Secret slog.Info("Pulling all server secrets", "server", id) diff --git a/internal/cac/logging/logging.go b/internal/cac/logging/logging.go index 6935c1b..a12ba35 100644 --- a/internal/cac/logging/logging.go +++ b/internal/cac/logging/logging.go @@ -1,6 +1,7 @@ package logging import ( + "context" "os" "strings" @@ -59,5 +60,5 @@ func InitLogging(config *Configuration) (err error) { } func Trace(msg string, args... any) { - slog.Log(nil, LevelTrace, msg, args...) + slog.Log(context.TODO(), LevelTrace, msg, args...) } diff --git a/internal/cac/storage/dry.go b/internal/cac/storage/dry.go index dc28d07..807e8db 100644 --- a/internal/cac/storage/dry.go +++ b/internal/cac/storage/dry.go @@ -23,19 +23,7 @@ func InitDryStorage(out string, constr Constructor) (*DryStorage, error) { if out == "-" { logging.Trace("Writing to stdout") - delegatedWriter = func(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { - var ( - bts []byte - err error - ) - - if bts, err = utils.ToYaml(data); err != nil { - return err - } - - _, err = os.Stdout.Write(bts) - return err - } + delegatedWriter = stdWriter } else if out != "" { var ( file *os.File @@ -46,6 +34,8 @@ func InitDryStorage(out string, constr Constructor) (*DryStorage, error) { return nil, err } else if err == nil { // file already exists + + //nolint:errcheck defer file.Close() if info, err = file.Stat(); err != nil { @@ -85,7 +75,7 @@ func (d *DryStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch panic("read operation is not implemented for dry storage") } -var stdWriter = func(ctx context.Context, data *api.PatchImpl[any], opts ...api.SourceOpt) error { +var stdWriter = func(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( bts []byte err error diff --git a/internal/cac/storage/secrets_test.go b/internal/cac/storage/secrets_test.go index a2a5f7c..fb52ca4 100644 --- a/internal/cac/storage/secrets_test.go +++ b/internal/cac/storage/secrets_test.go @@ -83,7 +83,8 @@ func TestReadingSecrets(t *testing.T) { require.NoError(t, err) - os.MkdirAll(tmpDir+"/workspaces/demo/secrets", 0755) + err = os.MkdirAll(tmpDir+"/workspaces/demo/secrets", 0755) + require.NoError(t, err) err = os.WriteFile(tmpDir+"/workspaces/demo/secrets/Some_secret.yaml", yml, 0644) require.NoError(t, err) diff --git a/internal/cac/storage/writer.go b/internal/cac/storage/writer.go index edb9bcb..442638f 100644 --- a/internal/cac/storage/writer.go +++ b/internal/cac/storage/writer.go @@ -137,6 +137,7 @@ func RawWriter(dirPath string) (Writer[[]byte], error) { return errors.Wrapf(err, "failed to create file %s", filepath.Join(dirPath, name)) } + //nolint:errcheck defer file.Close() if _, err = file.Write(bts); err != nil { diff --git a/internal/cac/templates/functions.go b/internal/cac/templates/functions.go index 01bd61e..97389de 100644 --- a/internal/cac/templates/functions.go +++ b/internal/cac/templates/functions.go @@ -62,7 +62,7 @@ func env(key string) (any, error) { func nindent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) - return "|-\n" + pad + strings.Replace(v, "\n", "\n"+pad, -1) + return "|-\n" + pad + strings.ReplaceAll(v, "\n", "\n"+pad) } From 424ddf18b9560ac70b84d37f478a5d3b89ce26a6 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 11:18:18 +0200 Subject: [PATCH 6/9] fix pull output --- cmd/pull.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/pull.go b/cmd/pull.go index 9aa749d..7325923 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -58,9 +58,7 @@ var ( if _, err = os.Stdout.Write(bts); err != nil { return errors.Wrap(err, "failed to write diff result to stdout") } - } - - if err = os.WriteFile(pullConfig.Out, bts, 0644); err != nil { + } else if err = os.WriteFile(pullConfig.Out, bts, 0644); err != nil { return errors.Wrap(err, "failed to write diff result to file") } } From 436ef99a765d7c63c3bdfbabac123b28a6ef9595 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 11:24:47 +0200 Subject: [PATCH 7/9] improve trace level condition --- internal/cac/logging/logging.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cac/logging/logging.go b/internal/cac/logging/logging.go index a12ba35..fa293b3 100644 --- a/internal/cac/logging/logging.go +++ b/internal/cac/logging/logging.go @@ -38,9 +38,9 @@ func InitLogging(config *Configuration) (err error) { logger *slog.Logger ) - if err = levelRef.UnmarshalText([]byte(config.Level)); err != nil && strings.ToUpper(config.Level) == "TRACE" { + if strings.ToUpper(config.Level) == "TRACE" { levelRef.Set(LevelTrace) - } else if err != nil { + } else if err = levelRef.UnmarshalText([]byte(config.Level)); err != nil { return err } From 0a55bd589434fa5b12f66d475c83017ea242d6e6 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 11:27:17 +0200 Subject: [PATCH 8/9] update golang --- Dockerfile | 2 +- go.mod | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5d881f1..4fe1060 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22 AS build +FROM golang:1.24 AS build WORKDIR /app diff --git a/go.mod b/go.mod index 562a663..a397f84 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,13 @@ module github.com/cloudentity/cac -go 1.23.0 - -toolchain go1.24.3 +go 1.24 require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/cloudentity/acp-client-go v0.0.0-20250530113034-a2fab50491c3 github.com/corvus-ch/zbase32 v1.0.0 github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b + github.com/go-openapi/runtime v0.27.0 github.com/go-openapi/strfmt v0.22.0 github.com/goccy/go-yaml v1.12.0 github.com/google/go-cmp v0.6.0 @@ -36,7 +35,6 @@ require ( github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/loads v0.21.5 // indirect - github.com/go-openapi/runtime v0.27.0 // indirect github.com/go-openapi/spec v0.20.14 // indirect github.com/go-openapi/swag v0.22.8 // indirect github.com/go-openapi/validate v0.22.6 // indirect From 0e2f6216719ac40adee51f5e803197f01d0be102 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 17 Jun 2025 12:39:28 +0200 Subject: [PATCH 9/9] fix reading tenant data from storage --- internal/cac/storage/tenant_storage.go | 10 ++++++---- internal/cac/storage/tenant_storage_test.go | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/cac/storage/tenant_storage.go b/internal/cac/storage/tenant_storage.go index 3e25220..524a90a 100644 --- a/internal/cac/storage/tenant_storage.go +++ b/internal/cac/storage/tenant_storage.go @@ -81,7 +81,7 @@ func (t *TenantStorage) Write(ctx context.Context, data api.Patch, opts ...api.S } for k, server := range model.Servers { - opts = append(opts, api.WithWorkspace(k)) + wopts := append(opts, api.WithWorkspace(k)) var ( serverData models.Rfc7396PatchOperation @@ -100,7 +100,7 @@ func (t *TenantStorage) Write(ctx context.Context, data api.Patch, opts ...api.S if err = t.ServerStorage.Write(ctx, &api.ServerPatch{ Data: serverData, Ext: ext.GetServerExtensions(k), - }, opts...); err != nil { + }, wopts...); err != nil { return err } } @@ -194,15 +194,17 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pa for _, workspace := range workspaces { var workspaceConfig api.Patch - opts = append(opts, api.WithWorkspace(workspace), api.WithFilters([]string{})) + wopts := append(opts, api.WithWorkspace(workspace), api.WithFilters([]string{})) - if workspaceConfig, err = t.ServerStorage.Read(ctx, opts...); err != nil { + if workspaceConfig, err = t.ServerStorage.Read(ctx, wopts...); err != nil { return nil, err } data := workspaceConfig.GetData() utils.CleanPatch(data) + + servers[workspace] = data } tenant["servers"] = servers diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index a64d38b..6086434 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -188,6 +188,9 @@ updated_at: 0001-01-01T00:00:00.000Z } for _, tc := range tcs { + if tc.desc != "workspace and mfa_methods" { + continue + } t.Run(tc.desc, func(t *testing.T) { err := logging.InitLogging(&logging.Configuration{ Level: "debug", @@ -196,7 +199,7 @@ updated_at: 0001-01-01T00:00:00.000Z require.NoError(t, err) st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ - DirPath: []string{t.TempDir(), t.TempDir()}, + DirPath: []string{t.TempDir()}, }, storage.InitTenantStorage) require.NoError(t, err)