From 4031fc47cc6c6cd00572f7a2b099a9ff567cd998 Mon Sep 17 00:00:00 2001 From: jdabrowski Date: Thu, 20 Aug 2026 12:48:21 +0200 Subject: [PATCH 1/4] [AUT-13970] Keep phone_provider_config through a tenant pull/push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit storeTenant filters the tenant root through smodels.Tenant to drop the collections written to their own files. That struct predates phone_provider_config, so the field was discarded on pull and could never be pushed back — which is why affected repos have no phone provider configuration on disk at all. Bumps acp-client-go to the AUT-13969 sync so the config carries `mode`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AZykgo2CbxHjvnBzfWQxrL --- go.mod | 2 +- go.sum | 4 +- internal/cac/storage/server_storage_test.go | 2 + internal/cac/storage/tenant_storage.go | 11 +++- internal/cac/storage/tenant_storage_test.go | 67 +++++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1f84d9b..b634d84 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/Masterminds/sprig/v3 v3.2.3 - github.com/cloudentity/acp-client-go v0.0.0-20260527095100-008ff5049411 + github.com/cloudentity/acp-client-go v0.0.0-20260820095634-1f1938087bcf 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.24.0 diff --git a/go.sum b/go.sum index 953952e..ed04d77 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-20260527095100-008ff5049411 h1:oty7npFWtmLAegF5gYXQAAsY2DOZ7WxcCZVVqBSeIwE= -github.com/cloudentity/acp-client-go v0.0.0-20260527095100-008ff5049411/go.mod h1:Hr2WHHXmp+DC4B2oprhgP47yl/dsCAyj9HcG6P3z4m0= +github.com/cloudentity/acp-client-go v0.0.0-20260820095634-1f1938087bcf h1:Ir4JoN0wkeZYQgiM4il5Jrg/WJSIGLgjBeEtE7my4zY= +github.com/cloudentity/acp-client-go v0.0.0-20260820095634-1f1938087bcf/go.mod h1:Hr2WHHXmp+DC4B2oprhgP47yl/dsCAyj9HcG6P3z4m0= 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= diff --git a/internal/cac/storage/server_storage_test.go b/internal/cac/storage/server_storage_test.go index 14213e2..997fca8 100644 --- a/internal/cac/storage/server_storage_test.go +++ b/internal/cac/storage/server_storage_test.go @@ -98,6 +98,7 @@ client_secret_expires_at: 0 created_at: 0001-01-01T00:00:00.000Z dpop_bound_access_tokens: false dynamically_registered: false +enforce_application_membership: false grant_types: [] hashed_rotated_secrets: [] id: demo-demo @@ -111,6 +112,7 @@ saml_idp_override_enabled: false saml_metadata_updated_at: 0001-01-01T00:00:00.000Z saml_override_attributes: false scopes: [] +skip_dbfp: false system: false tls_client_certificate_bound_access_tokens: false trusted: false diff --git a/internal/cac/storage/tenant_storage.go b/internal/cac/storage/tenant_storage.go index 4611221..7e5f321 100644 --- a/internal/cac/storage/tenant_storage.go +++ b/internal/cac/storage/tenant_storage.go @@ -200,9 +200,18 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models var _ Storage = &TenantStorage{} +// tenantFile is what lands in tenant.yaml. smodels.Tenant is reused to strip the +// collections stored in their own files, but it predates phone_provider_config and +// would otherwise discard it. +type tenantFile struct { + smodels.Tenant + + PhoneProviderConfig *models.TreePhoneProviderConfig `json:"phone_provider_config,omitempty"` +} + func (t *TenantStorage) storeTenant(path string, data *models.TreeTenant) error { var ( - tenant smodels.Tenant + tenant tenantFile bts []byte err error ) diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index 6e97ff1..beaa3e6 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -176,6 +176,7 @@ public_registration_allowed: false second_factor_threshold: 0 system: false webauthn_settings: + require_user_interaction_before_prompt: false rp_id: example.com rp_origins: - https://www.sit2.example.com`, string(bts)) @@ -220,6 +221,36 @@ settings: } }, }, + { + desc: "phone provider config", + data: &models.TreeTenant{ + Name: "Default", + PhoneProviderConfig: &models.TreePhoneProviderConfig{ + Mode: "custom", + Providers: []*models.PhoneProvider{ + {Twilio: &models.TwilioPhoneProvider{ + Sid: "ACtest", AuthToken: "tok", From: "SecureAuth", + }}, + }, + }, + }, + files: []string{"tenant.yaml"}, + assert: func(t *testing.T, path string, bts []byte) { + switch path { + case "tenant.yaml": + require.YAMLEq(t, `name: Default +phone_provider_config: + active: false + mode: custom + providers: + - twilio: + sid: ACtest + auth_token: tok + from: SecureAuth + disable_delivery_callback_url: false`, string(bts)) + } + }, + }, { desc: "themes and templates", data: &models.TreeTenant{ @@ -338,3 +369,39 @@ updated_at: 0001-01-01T00:00:00.000Z }) } } + +func TestTenantStoragePhoneProviderConfigRoundTrip(t *testing.T) { + require.NoError(t, logging.InitLogging(&logging.Configuration{Level: "debug"})) + + st, err := storage.InitMultiStorage(&storage.MultiStorageConfiguration{ + DirPath: []string{t.TempDir()}, + }, storage.InitTenantStorage) + require.NoError(t, err) + + tree := &models.TreeTenant{ + Name: "Default", + PhoneProviderConfig: &models.TreePhoneProviderConfig{ + Mode: "custom", + Providers: []*models.PhoneProvider{ + {Twilio: &models.TwilioPhoneProvider{Sid: "ACtest", AuthToken: "tok", From: "SecureAuth"}}, + }, + }, + } + + written, err := utils.FromModelToPatch(tree) + require.NoError(t, err) + + require.NoError(t, st.Write(context.Background(), written, api.WithWorkspace("demo"))) + + read, err := st.Read(context.Background(), api.WithWorkspace("demo")) + require.NoError(t, err) + + back, err := utils.FromPatchToModel[models.TreeTenant](read) + require.NoError(t, err) + + require.NotNil(t, back.PhoneProviderConfig, "phone_provider_config did not survive the round trip") + require.Equal(t, "custom", back.PhoneProviderConfig.Mode) + require.Len(t, back.PhoneProviderConfig.Providers, 1) + require.Equal(t, "ACtest", back.PhoneProviderConfig.Providers[0].Twilio.Sid) + require.Equal(t, "tok", back.PhoneProviderConfig.Providers[0].Twilio.AuthToken) +} From b39f9c4ab0399d8f664511a2d52a6fd53e244317 Mon Sep 17 00:00:00 2001 From: jdabrowski Date: Thu, 20 Aug 2026 13:55:50 +0200 Subject: [PATCH 2/4] [AUT-13970] Store phone_provider_config in its own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the pattern server storage already uses for singular sub-resources (claims, consent, ciba, theme_binding): writeFile on the way out, readFileToMap on the way back. Replaces the earlier approach of widening the tenant.yaml model. That one broke the invariant behind utils.TenantRootKeys, which is derived from the same type storeTenant serializes into — phone_provider_config was written to tenant.yaml but missing from the root key set, so `push --filter root` silently dropped it. storeTenant is now untouched and the invariant holds. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AZykgo2CbxHjvnBzfWQxrL --- internal/cac/storage/tenant_storage.go | 19 +++++++++--------- internal/cac/storage/tenant_storage_test.go | 22 ++++++++++----------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/internal/cac/storage/tenant_storage.go b/internal/cac/storage/tenant_storage.go index 7e5f321..b94d9ca 100644 --- a/internal/cac/storage/tenant_storage.go +++ b/internal/cac/storage/tenant_storage.go @@ -37,6 +37,10 @@ func (t *TenantStorage) Write(ctx context.Context, data models.Rfc7396PatchOpera return err } + if err = writeFile(model.PhoneProviderConfig, filepath.Join(path, "phone_provider_config")); 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 { @@ -109,6 +113,10 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models return nil, err } + if err = readFileToMap(tenant, "phone_provider_config", filepath.Join(path, "phone_provider_config")); err != nil { + return nil, err + } + if err = readFilesToMap(tenant, "pools", filepath.Join(path, "pools")); err != nil { return nil, err } @@ -200,18 +208,9 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models var _ Storage = &TenantStorage{} -// tenantFile is what lands in tenant.yaml. smodels.Tenant is reused to strip the -// collections stored in their own files, but it predates phone_provider_config and -// would otherwise discard it. -type tenantFile struct { - smodels.Tenant - - PhoneProviderConfig *models.TreePhoneProviderConfig `json:"phone_provider_config,omitempty"` -} - func (t *TenantStorage) storeTenant(path string, data *models.TreeTenant) error { var ( - tenant tenantFile + tenant smodels.Tenant bts []byte err error ) diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index beaa3e6..f1ff700 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -234,20 +234,20 @@ settings: }, }, }, - files: []string{"tenant.yaml"}, + files: []string{"tenant.yaml", "phone_provider_config.yaml"}, assert: func(t *testing.T, path string, bts []byte) { switch path { case "tenant.yaml": - require.YAMLEq(t, `name: Default -phone_provider_config: - active: false - mode: custom - providers: - - twilio: - sid: ACtest - auth_token: tok - from: SecureAuth - disable_delivery_callback_url: false`, string(bts)) + require.YAMLEq(t, `name: Default`, string(bts)) + case "phone_provider_config.yaml": + require.YAMLEq(t, `active: false +mode: custom +providers: +- twilio: + sid: ACtest + auth_token: tok + from: SecureAuth + disable_delivery_callback_url: false`, string(bts)) } }, }, From 4866d5e21f3d52a83311ffef12b3b75e75d0ecd9 Mon Sep 17 00:00:00 2001 From: jdabrowski Date: Thu, 20 Aug 2026 14:59:02 +0200 Subject: [PATCH 3/4] [AUT-13970] Guard provider variant before dereferencing in the round-trip test Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AZykgo2CbxHjvnBzfWQxrL --- internal/cac/storage/tenant_storage_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/cac/storage/tenant_storage_test.go b/internal/cac/storage/tenant_storage_test.go index f1ff700..5a50846 100644 --- a/internal/cac/storage/tenant_storage_test.go +++ b/internal/cac/storage/tenant_storage_test.go @@ -402,6 +402,8 @@ func TestTenantStoragePhoneProviderConfigRoundTrip(t *testing.T) { require.NotNil(t, back.PhoneProviderConfig, "phone_provider_config did not survive the round trip") require.Equal(t, "custom", back.PhoneProviderConfig.Mode) require.Len(t, back.PhoneProviderConfig.Providers, 1) + require.NotNil(t, back.PhoneProviderConfig.Providers[0]) + require.NotNil(t, back.PhoneProviderConfig.Providers[0].Twilio) require.Equal(t, "ACtest", back.PhoneProviderConfig.Providers[0].Twilio.Sid) require.Equal(t, "tok", back.PhoneProviderConfig.Providers[0].Twilio.AuthToken) } From 114d445b4022e27046a5d6f5332348631d64262c Mon Sep 17 00:00:00 2001 From: jdabrowski Date: Tue, 25 Aug 2026 09:17:29 +0200 Subject: [PATCH 4/4] [AUT-13970] Re-pin acp-client-go to master now that #75 has merged Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QrGXNzbqi2CPHE4nWHri7N --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b634d84..96b5f60 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/Masterminds/sprig/v3 v3.2.3 - github.com/cloudentity/acp-client-go v0.0.0-20260820095634-1f1938087bcf + github.com/cloudentity/acp-client-go v0.0.0-20260825070526-1de34904f06f 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.24.0 diff --git a/go.sum b/go.sum index ed04d77..a395216 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-20260820095634-1f1938087bcf h1:Ir4JoN0wkeZYQgiM4il5Jrg/WJSIGLgjBeEtE7my4zY= -github.com/cloudentity/acp-client-go v0.0.0-20260820095634-1f1938087bcf/go.mod h1:Hr2WHHXmp+DC4B2oprhgP47yl/dsCAyj9HcG6P3z4m0= +github.com/cloudentity/acp-client-go v0.0.0-20260825070526-1de34904f06f h1:EVEtG2VSF7NojXtgOeWjUQtgwOGvnBlTWmVpD24LEVQ= +github.com/cloudentity/acp-client-go v0.0.0-20260825070526-1de34904f06f/go.mod h1:Hr2WHHXmp+DC4B2oprhgP47yl/dsCAyj9HcG6P3z4m0= 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=