Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,12 @@ Workspace resources: clients, idps, claims, custom_apps, gateways, policies, pol
server_consent, servers_bindings, services, theme_binding, webhooks,
ciba (alias of ciba_authentication_service)
Tenant resources: pools, schemas, mfa_methods, themes, servers
Reserved: root (only root-level tenant/workspace config, excluding nested resources)
Examples:
--filter clients
--filter clients,idps,policies`)
--filter clients,idps,policies
--filter root
--filter root,clients`)
diffCmd.PersistentFlags().StringVar(&diffConfig.Out, "out", "-", `Diff output destination: a file path or '-' for stdout.
Examples:
--out - (stdout)
Expand Down
5 changes: 4 additions & 1 deletion cmd/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ Workspace resources: clients, idps, claims, custom_apps, gateways, policies, pol
server_consent, servers_bindings, services, theme_binding, webhooks,
ciba (alias of ciba_authentication_service)
Tenant resources: pools, schemas, mfa_methods, themes, servers
Reserved: root (only root-level tenant/workspace config, excluding nested resources)
Examples:
--filter clients
--filter clients,idps,policies
--filter scopes --filter pools`)
--filter scopes --filter pools
--filter root
--filter root,clients`)
}
5 changes: 4 additions & 1 deletion cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,13 @@ Workspace resources: clients, idps, claims, custom_apps, gateways, policies, pol
server_consent, servers_bindings, services, theme_binding, webhooks,
ciba (alias of ciba_authentication_service)
Tenant resources: pools, schemas, mfa_methods, themes, servers
Reserved: root (only root-level tenant/workspace config, excluding nested resources)
Examples:
--filter clients
--filter clients,idps,policies
--filter scopes --filter pools`)
--filter scopes --filter pools
--filter root
--filter root,clients`)

mustMarkRequired(pushCmd, "method")
}
2 changes: 1 addition & 1 deletion internal/cac/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ 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 data, err = utils.FilterPatch(data, options.Filters); err != nil {
if data, err = utils.FilterPatch(data, options.Filters, utils.ServerRootKeys); err != nil {
return nil, errors.Wrap(err, "failed to filter patch")
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cac/client/tenant_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (t *TenantClient) Read(ctx context.Context, opts ...api.SourceOpt) (models.
return nil, err
}

if data, err = utils.FilterPatch(data, options.Filters); err != nil {
if data, err = utils.FilterPatch(data, options.Filters, utils.TenantRootKeys); err != nil {
return nil, err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cac/storage/server_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ func (s *ServerStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models
return nil, err
}

if server, err = utils.FilterPatch(server, options.Filters); err != nil {
if server, err = utils.FilterPatch(server, options.Filters, utils.ServerRootKeys); err != nil {
return nil, err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cac/storage/server_storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ system: false`, string(bts))

// verifying if the data read from fs is the same as the provided test data

patchData, err = utils.FilterPatch(patchData, tc.filters)
patchData, err = utils.FilterPatch(patchData, tc.filters, utils.ServerRootKeys)
require.NoError(t, err)

d, err := diff.Tree(patchData, readServer)
Expand Down
2 changes: 1 addition & 1 deletion internal/cac/storage/tenant_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func (t *TenantStorage) Read(ctx context.Context, opts ...api.SourceOpt) (models
tenant["servers"] = servers
}

if tenant, err = utils.FilterPatch(tenant, options.Filters); err != nil {
if tenant, err = utils.FilterPatch(tenant, options.Filters, utils.TenantRootKeys); err != nil {
return nil, err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cac/storage/tenant_storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ updated_at: 0001-01-01T00:00:00.000Z
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)
patchData, err = utils.FilterPatch(patchData, tc.filters, utils.TenantRootKeys)
require.NoError(t, err)

d, err := diff.Tree(patchData, readServer)
Expand Down
46 changes: 45 additions & 1 deletion internal/cac/utils/model.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package utils

import (
"reflect"
"strings"

"github.com/cloudentity/acp-client-go/clients/hub/models"
smodels "github.com/cloudentity/acp-client-go/clients/system/models"
"github.com/go-json-experiment/json"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -73,14 +77,54 @@ var staticFilterMappings = map[string]string{
"ciba": "ciba_authentication_service",
}

func FilterPatch(patch models.Rfc7396PatchOperation, filters []string) (models.Rfc7396PatchOperation, error) {
// RootFilter is a reserved --filter value that selects only root-level config,
// i.e. the tenant/workspace's own fields, excluding nested sub-resources.
const RootFilter = "root"

// TenantRootKeys and ServerRootKeys hold the top-level JSON field names that make
// up root-level tenant / workspace config. They are derived from the same models
// the storage layer serializes the root tenant/server file into (with nested
// dependencies stripped), so new root fields are picked up automatically without a
// hand-maintained list.
var (
TenantRootKeys = modelJSONKeys(reflect.TypeFor[smodels.Tenant]())
ServerRootKeys = modelJSONKeys(reflect.TypeFor[smodels.ServerDump]())
)
Comment thread
piotrek-janus marked this conversation as resolved.

// modelJSONKeys returns the set of top-level JSON field names of a struct type.
func modelJSONKeys(t reflect.Type) map[string]struct{} {
keys := make(map[string]struct{}, t.NumField())

for i := 0; i < t.NumField(); i++ {
name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
if name == "" || name == "-" {
continue
}

keys[name] = struct{}{}
}

return keys
}

func FilterPatch(patch models.Rfc7396PatchOperation, filters []string, rootKeys map[string]struct{}) (models.Rfc7396PatchOperation, error) {
if len(filters) == 0 {
return patch, nil
}

var newPatch = models.Rfc7396PatchOperation{}

for _, filter := range filters {
if filter == RootFilter {
for k := range rootKeys {
if v, ok := patch[k]; ok {
newPatch[k] = v
}
}

continue
}

if mapped, ok := staticFilterMappings[filter]; ok {
filter = mapped
}
Expand Down
80 changes: 77 additions & 3 deletions internal/cac/utils/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func TestFilterPatch(t *testing.T) {
name string
server models.Rfc7396PatchOperation
filters []string
rootKeys map[string]struct{}
expected models.Rfc7396PatchOperation
}{
{
Expand All @@ -29,7 +30,8 @@ func TestFilterPatch(t *testing.T) {
},
},
},
filters: []string{"clients"},
filters: []string{"clients"},
rootKeys: utils.ServerRootKeys,
expected: models.Rfc7396PatchOperation{
"clients": models.TreeClients{
"123": models.TreeClient{
Expand All @@ -55,7 +57,8 @@ func TestFilterPatch(t *testing.T) {
Type: "asd",
},
},
filters: []string{"scopes", "ciba"},
filters: []string{"scopes", "ciba"},
rootKeys: utils.ServerRootKeys,
expected: models.Rfc7396PatchOperation{
"scopes_without_service": models.TreeScopes{
"456": models.TreeScope{
Expand All @@ -67,11 +70,82 @@ func TestFilterPatch(t *testing.T) {
},
},
},
{
name: "root only keeps root-level workspace config and drops collections",
server: models.Rfc7396PatchOperation{
"name": "workspace1",
"grant_types": []string{"authorization_code"},
"token_endpoint_auth_methods": []string{"client_secret_basic"},
"clients": models.TreeClients{
"123": models.TreeClient{ClientName: "client1"},
},
"scopes_without_service": models.TreeScopes{
"456": models.TreeScope{Description: "some scope"},
},
},
filters: []string{utils.RootFilter},
rootKeys: utils.ServerRootKeys,
expected: models.Rfc7396PatchOperation{
"name": "workspace1",
"grant_types": []string{"authorization_code"},
"token_endpoint_auth_methods": []string{"client_secret_basic"},
},
},
{
name: "root combined with a collection keeps both",
server: models.Rfc7396PatchOperation{
"name": "workspace1",
"clients": models.TreeClients{
"123": models.TreeClient{ClientName: "client1"},
},
"idps": models.TreeIDPs{
"456": models.TreeIDP{Name: "idp1"},
},
},
filters: []string{utils.RootFilter, "clients"},
rootKeys: utils.ServerRootKeys,
expected: models.Rfc7396PatchOperation{
"name": "workspace1",
"clients": models.TreeClients{
"123": models.TreeClient{ClientName: "client1"},
},
},
},
{
name: "root only keeps root-level tenant config and drops collections",
server: models.Rfc7396PatchOperation{
"name": "tenant1",
"settings": map[string]any{"key": "value"},
"pools": models.TreePools{
"123": models.TreePool{Name: "pool1"},
},
"schemas": models.TreeSchemas{
"456": models.TreeSchema{Name: "schema1"},
},
},
filters: []string{utils.RootFilter},
rootKeys: utils.TenantRootKeys,
expected: models.Rfc7396PatchOperation{
"name": "tenant1",
"settings": map[string]any{"key": "value"},
},
},
{
name: "root only on a patch of collections yields empty result",
server: models.Rfc7396PatchOperation{
"clients": models.TreeClients{
"123": models.TreeClient{ClientName: "client1"},
},
},
filters: []string{utils.RootFilter},
rootKeys: utils.ServerRootKeys,
expected: models.Rfc7396PatchOperation{},
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
actual, err := utils.FilterPatch(tc.server, tc.filters)
actual, err := utils.FilterPatch(tc.server, tc.filters, tc.rootKeys)

require.NoError(t, err)

Expand Down
Loading