From 7040174f0d49cb71805838092bd5d3b7bc6f7e66 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Mon, 9 Dec 2024 16:42:34 +0100 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 07/12] 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 08/12] 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 09/12] 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) From 6c956e1e9ac9675c5bc8a99e476aebc6f0a2c574 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Wed, 25 Jun 2025 15:36:53 +0200 Subject: [PATCH 10/12] write secrets --- internal/cac/client/client.go | 29 +++++++++++++++++------- internal/cac/client/secrets_client.go | 10 ++++----- internal/cac/client/tenant_client.go | 32 ++++++++++++++++++++------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/internal/cac/client/client.go b/internal/cac/client/client.go index be79232..e3ae04a 100644 --- a/internal/cac/client/client.go +++ b/internal/cac/client/client.go @@ -4,7 +4,9 @@ import ( "context" "crypto/tls" "fmt" + "maps" "net/http" + "slices" acpclient "github.com/cloudentity/acp-client-go" "github.com/cloudentity/acp-client-go/clients/hub/client/workspace_configuration" @@ -109,6 +111,8 @@ func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (api.Patch, er func (c *Client) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( options = &api.Options{} + datF func(ctx context.Context, wid string, mode string, data api.Patch) error + secF func(ctx context.Context, wid string, payload []*smodels.Secret) error workspace string err error ) @@ -123,19 +127,28 @@ func (c *Client) Write(ctx context.Context, data api.Patch, opts ...api.SourceOp switch options.Method { case "import": - if err = c.Import(ctx, workspace, options.Mode, data); err != nil { - logErr(err) - return err - } + datF = c.Import + secF = c.sec.UpdateAll case "patch": - if err = c.Patch(ctx, workspace, options.Mode, data); err != nil { - logErr(err) - return err - } + datF = c.Patch + secF = c.sec.PatchAll default: return fmt.Errorf("unknown method: %v", options.Method) } + if err = datF(ctx, workspace, options.Mode, data); err != nil { + logErr(err) + return err + } + + ext := data.GetExtensions().(*api.ServerExtensions) + secrets := slices.Collect(maps.Values(ext.Secrets)) + + if err = secF(ctx, workspace, secrets); err != nil { + logErr(err) + return errors.Wrap(err, "failed to update secrets") + } + return nil } diff --git a/internal/cac/client/secrets_client.go b/internal/cac/client/secrets_client.go index 6c65437..6436bf4 100644 --- a/internal/cac/client/secrets_client.go +++ b/internal/cac/client/secrets_client.go @@ -76,13 +76,13 @@ func (s *SecretsClient) Update(ctx context.Context, wid string, payload models.S return nil } -func (s *SecretsClient) UpdateAll(ctx context.Context, wid string, payload []models.Secret) error { +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 { +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); }) @@ -90,7 +90,7 @@ func (s *SecretsClient) PatchAll(ctx context.Context, wid string, payload []mode 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 { +func (s *SecretsClient) patchAll(ctx context.Context, wid string, payload []*models.Secret, patchF PatchFunc) error { var ( existingSecrets []*models.Secret @@ -108,7 +108,7 @@ func (s *SecretsClient) patchAll(ctx context.Context, wid string, payload []mode for _, secret := range payload { if existingSecret, exists := existingMap[secret.ID]; exists { - if err = patchF(existingSecret, secret); err != nil { + if err = patchF(existingSecret, *secret); err != nil { return errors.Wrapf(err, "failed to merge secret %s", secret.ID) } @@ -117,7 +117,7 @@ func (s *SecretsClient) patchAll(ctx context.Context, wid string, payload []mode } delete(existingMap, existingSecret.ID) // Remove from map to avoid creating it later } else { - if _, err = s.Create(ctx, wid, secret); err != nil { + if _, err = s.Create(ctx, wid, *secret); err != nil { return errors.Wrapf(err, "failed to create secret %s", secret.ID) } } diff --git a/internal/cac/client/tenant_client.go b/internal/cac/client/tenant_client.go index 219387c..df40110 100644 --- a/internal/cac/client/tenant_client.go +++ b/internal/cac/client/tenant_client.go @@ -3,6 +3,8 @@ package client import ( "context" "fmt" + "maps" + "slices" acpclient "github.com/cloudentity/acp-client-go" "github.com/cloudentity/acp-client-go/clients/hub/client/tenant_configuration" @@ -77,6 +79,8 @@ func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pat func (t *TenantClient) Write(ctx context.Context, data api.Patch, opts ...api.SourceOpt) error { var ( options = &api.Options{} + datF func(ctx context.Context, mode string, data models.Rfc7396PatchOperation) error + secF func(ctx context.Context, wid string, payload []*smodels.Secret) error err error ) @@ -86,19 +90,31 @@ func (t *TenantClient) Write(ctx context.Context, data api.Patch, opts ...api.So switch options.Method { case "import": - if err = t.Import(ctx, options.Mode, data.GetData()); err != nil { - logErr(err) - return err - } + datF = t.Import + secF = t.sec.UpdateAll case "patch": - if err = t.Patch(ctx, options.Mode, data.GetData()); err != nil { - logErr(err) - return err - } + datF = t.Patch + secF = t.sec.PatchAll default: return fmt.Errorf("unknown method: %v", options.Method) } + if err = datF(ctx, options.Mode, data.GetData()); err != nil { + logErr(err) + return err + } + + ext := data.GetExtensions().(*api.TenantExtensions) + for serverID, server := range ext.Servers { + slog.Info("pushing server secrets", "server", serverID) + secrets := slices.Collect(maps.Values(server.Secrets)) + if err = secF(ctx, serverID, secrets); err != nil { + logErr(err) + return fmt.Errorf("failed to update secrets for server %s: %w", serverID, err) + } + slog.Info("pushed server secrets", "server", serverID) + } + return nil } From 79b3a1df58796a29361ae27996e0f651b449da67 Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Sat, 13 Jun 2026 01:19:39 +0200 Subject: [PATCH 11/12] fix: collect server secret extensions when reading tenant config from storage TenantStorage.Read previously kept only GetData() from each workspace's ServerStorage.Read result and discarded GetExtensions(), leaving the returned TenantPatch with empty extensions. As a result a tenant-level pull/push of secrets from local storage lost all secrets. Initialize the tenant Servers extension map and copy each workspace's ServerExtensions into it. Add a round-trip test reading tenant secrets. --- internal/cac/storage/secrets_test.go | 65 ++++++++++++++++++++++---- internal/cac/storage/tenant_storage.go | 8 +++- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/internal/cac/storage/secrets_test.go b/internal/cac/storage/secrets_test.go index fb52ca4..b0ab997 100644 --- a/internal/cac/storage/secrets_test.go +++ b/internal/cac/storage/secrets_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/require" ) - func TestWritingSecrets(t *testing.T) { var dateTime, _ = strfmt.ParseDateTime("2024-01-23T23:19:30.004+01:00") @@ -39,8 +38,8 @@ func TestWritingSecrets(t *testing.T) { Ext: &api.ServerExtensions{ Secrets: map[string]*smodels.Secret{ "Some_secret": &smodels.Secret{ - ID: "Some_secret", - Value: "test", + ID: "Some_secret", + Value: "test", CreatedAt: dateTime, }, }, @@ -67,13 +66,12 @@ func TestWritingSecrets(t *testing.T) { 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", + ID: "Some_secret", + Value: "test", CreatedAt: dateTime, } @@ -115,7 +113,7 @@ func TestReadingSecrets(t *testing.T) { 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) @@ -131,4 +129,55 @@ func TestReadingSecrets(t *testing.T) { require.Equal(t, "test", secret.Value) require.Equal(t, dateTime.String(), secret.CreatedAt.String()) -} \ No newline at end of file +} + +func TestReadingTenantSecrets(t *testing.T) { + var dateTime, _ = strfmt.ParseDateTime("2024-01-23T23:19:30.004+01:00") + + secret := smodels.Secret{ + ID: "Some_secret", + Value: "test", + CreatedAt: dateTime, + } + + tmpDir := t.TempDir() + + yml, err := utils.ToYaml(secret) + require.NoError(t, err) + + 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) + + yml, err = utils.ToYaml(models.TreeServer{Name: "demo workspace"}) + 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.InitTenantStorage) + require.NoError(t, err) + + readData, err := st.Read(context.Background(), api.WithSecrets(true)) + require.NoError(t, err) + require.NotNil(t, readData) + + ext, ok := readData.GetExtensions().(*api.TenantExtensions) + require.True(t, ok) + + serverExt := ext.GetServerExtensions("demo") + require.NotNil(t, serverExt, "tenant extensions should carry the demo server's secrets") + require.Len(t, serverExt.Secrets, 1) + + s, ok := serverExt.Secrets["Some_secret"] + require.True(t, ok) + require.Equal(t, "test", s.Value) + require.Equal(t, dateTime.String(), s.CreatedAt.String()) +} diff --git a/internal/cac/storage/tenant_storage.go b/internal/cac/storage/tenant_storage.go index 524a90a..b8188bd 100644 --- a/internal/cac/storage/tenant_storage.go +++ b/internal/cac/storage/tenant_storage.go @@ -112,7 +112,7 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pa var ( path = t.Config.DirPath tenant models.Rfc7396PatchOperation - ext = api.TenantExtensions{} + ext = api.TenantExtensions{Servers: map[string]api.ServerExtensions{}} options = &api.Options{} themeDirs []string workspaces []string @@ -204,7 +204,11 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (api.Pa utils.CleanPatch(data) - servers[workspace] = data + servers[workspace] = data + + if sext, ok := workspaceConfig.GetExtensions().(*api.ServerExtensions); ok && sext != nil { + ext.Servers[workspace] = *sext + } } tenant["servers"] = servers From 5171efb0b3c73319e6af9d013f8f6ee11649401b Mon Sep 17 00:00:00 2001 From: Piotr Janus Date: Tue, 16 Jun 2026 14:40:45 +0200 Subject: [PATCH 12/12] refactor: harden patch merge and pull --out output - Make Patch.Merge tolerate nil extensions on either side instead of passing a nil pointer into mergo (#3); extract the shared merge logic into a generic mergePatch helper used by both ServerPatch and TenantPatch. - Add the missing var _ Patch = &TenantPatch{} compile-time assertion and fix copy-pasted receiver names (#10). - pull --out now emits the configuration tree (data.GetData()) instead of the internal patch envelope with data/ext wrapper keys (#11). Add merge unit tests covering data/extension merge and nil-extension cases. --- cmd/pull.go | 4 ++- internal/cac/api/model.go | 48 +++++++++++++++++------------- internal/cac/api/model_test.go | 54 ++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 internal/cac/api/model_test.go diff --git a/cmd/pull.go b/cmd/pull.go index 7325923..b2abcc8 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -48,7 +48,9 @@ var ( return err } } else { - bts, err := utils.ToYaml(data) + // emit the configuration tree itself rather than the internal + // patch envelope (data/ext wrapper) + bts, err := utils.ToYaml(data.GetData()) if err != nil { return errors.Wrap(err, "failed to marshal data to YAML") diff --git a/internal/cac/api/model.go b/internal/cac/api/model.go index b40c1f1..0cf4191 100644 --- a/internal/cac/api/model.go +++ b/internal/cac/api/model.go @@ -37,6 +37,26 @@ type PatchImpl[T any] struct { Ext *T `json:"ext,omitempty"` } +// mergePatch merges another patch's data and extensions into the given data +// map and extension pointer, tolerating nil extensions on either side. +func mergePatch[T any](data *models.Rfc7396PatchOperation, ext **T, other Patch) error { + if err := mergo.Merge(data, other.GetData(), mergo.WithOverride); err != nil { + return err + } + + otherExt, _ := other.GetExtensions().(*T) + if otherExt == nil { + return nil + } + + if *ext == nil { + *ext = otherExt + return nil + } + + return mergo.Merge(*ext, otherExt, mergo.WithOverride) +} + type ServerPatch PatchImpl[ServerExtensions] var _ Patch = &ServerPatch{} @@ -44,37 +64,23 @@ var _ Patch = &ServerPatch{} func (sp *ServerPatch) GetData() models.Rfc7396PatchOperation { return sp.Data } -func (tp *ServerPatch) GetExtensions() any { - return tp.Ext +func (sp *ServerPatch) GetExtensions() any { + return sp.Ext } func (sp *ServerPatch) Merge(other Patch) 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 + return mergePatch(&sp.Data, &sp.Ext, other) } type TenantPatch PatchImpl[TenantExtensions] +var _ Patch = &TenantPatch{} + func (tp *TenantPatch) GetData() models.Rfc7396PatchOperation { return tp.Data } func (tp *TenantPatch) GetExtensions() any { return tp.Ext } -func (sp *TenantPatch) Merge(other Patch) 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 +func (tp *TenantPatch) Merge(other Patch) error { + return mergePatch(&tp.Data, &tp.Ext, other) } diff --git a/internal/cac/api/model_test.go b/internal/cac/api/model_test.go new file mode 100644 index 0000000..5cd9d10 --- /dev/null +++ b/internal/cac/api/model_test.go @@ -0,0 +1,54 @@ +package api_test + +import ( + "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/stretchr/testify/require" +) + +func TestServerPatchMerge(t *testing.T) { + t.Run("merges data and extensions", func(t *testing.T) { + dst := &api.ServerPatch{ + Data: models.Rfc7396PatchOperation{"name": "old", "a": "1"}, + Ext: &api.ServerExtensions{Secrets: map[string]*smodels.Secret{"s1": {ID: "s1"}}}, + } + src := &api.ServerPatch{ + Data: models.Rfc7396PatchOperation{"name": "new", "b": "2"}, + Ext: &api.ServerExtensions{Secrets: map[string]*smodels.Secret{"s2": {ID: "s2"}}}, + } + + require.NoError(t, dst.Merge(src)) + + require.Equal(t, "new", dst.Data["name"]) + require.Equal(t, "1", dst.Data["a"]) + require.Equal(t, "2", dst.Data["b"]) + require.Contains(t, dst.Ext.Secrets, "s1") + require.Contains(t, dst.Ext.Secrets, "s2") + }) + + t.Run("tolerates nil destination extensions", func(t *testing.T) { + dst := &api.ServerPatch{Data: models.Rfc7396PatchOperation{}} + src := &api.ServerPatch{ + Data: models.Rfc7396PatchOperation{}, + Ext: &api.ServerExtensions{Secrets: map[string]*smodels.Secret{"s1": {ID: "s1"}}}, + } + + require.NoError(t, dst.Merge(src)) + require.NotNil(t, dst.Ext) + require.Contains(t, dst.Ext.Secrets, "s1") + }) + + t.Run("tolerates nil source extensions", func(t *testing.T) { + dst := &api.ServerPatch{ + Data: models.Rfc7396PatchOperation{}, + Ext: &api.ServerExtensions{Secrets: map[string]*smodels.Secret{"s1": {ID: "s1"}}}, + } + src := &api.ServerPatch{Data: models.Rfc7396PatchOperation{}} + + require.NoError(t, dst.Merge(src)) + require.Contains(t, dst.Ext.Secrets, "s1") + }) +}