From 0cd265cfa02edf67c69a26fad6fe6ae302f65405 Mon Sep 17 00:00:00 2001 From: kereis Date: Wed, 15 Feb 2023 16:16:19 +0000 Subject: [PATCH] Add reconciliation of authorization services in KeycloakClients --- Makefile | 4 + pkg/common/client.go | 137 +++++++++++++- pkg/common/client_state.go | 17 ++ pkg/common/cluster_actions.go | 177 +++++++++++++++++- .../keycloakclient_reconciler.go | 174 +++++++++++++++++ .../keycloakclient_reconciler_test.go | 143 +++++++++++++- pkg/model/util.go | 76 ++++++++ 7 files changed, 723 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 6a7a465df..144effad0 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,10 @@ setup/linter: code/run: @operator-sdk run local --watch-namespace=${NAMESPACE} +.PHONY: code/run-debug +code/run-debug: + @operator-sdk run local --enable-delve --watch-namespace=${NAMESPACE} + .PHONY: code/run-as-container code/run-as-container: code/delete-container eval $$(minikube -p minikube docker-env); \ diff --git a/pkg/common/client.go b/pkg/common/client.go index 3e733b8e7..8a53c97b9 100644 --- a/pkg/common/client.go +++ b/pkg/common/client.go @@ -24,7 +24,9 @@ import ( ) const ( - authURL = "realms/master/protocol/openid-connect/token" + authURL = "realms/master/protocol/openid-connect/token" + clientAuthorizationResourceResourceName = "client authorization service => resource" + clientAuthorizationPolicyResourceName = "client authorization service => policy" ) type Requester interface { @@ -78,6 +80,28 @@ func (c *Client) create(obj T, resourcePath, resourceName string) (string, error fmt.Println("user response ", string(d)) } + // Endpoint for creating authorization resources does not return an ID in Header "Location", but a JSON + // of the created resource instead. We need to parse it and then return its ID. + if resourceName == clientAuthorizationResourceResourceName { + var resource v1alpha1.KeycloakResource + data, _ := ioutil.ReadAll(res.Body) + err := json.Unmarshal(data, &resource) + if err != nil { + return "", err + } + return resource.ID, nil + } + + if resourceName == clientAuthorizationPolicyResourceName { + var policy v1alpha1.KeycloakPolicy + data, _ := ioutil.ReadAll(res.Body) + err := json.Unmarshal(data, &policy) + if err != nil { + return "", err + } + return policy.ID, nil + } + location := strings.Split(res.Header.Get("Location"), "/") uid := location[len(location)-1] return uid, nil @@ -114,6 +138,103 @@ func (c *Client) CreateClientClientScopeMappings(specClient *v1alpha1.KeycloakAP return err } +func (c *Client) ListClientAuthorizationResources(clientID, realmName string) ([]v1alpha1.KeycloakResource, error) { + result, err := c.list( + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/resource", realmName, clientID), + clientAuthorizationResourceResourceName, + func(body []byte) (T, error) { + var resources []v1alpha1.KeycloakResource + err := json.Unmarshal(body, &resources) + return resources, err + }, + ) + + if err != nil { + return nil, err + } + + res, ok := result.([]v1alpha1.KeycloakResource) + + if !ok { + return nil, errors.Errorf("error decoding list client authorization service => resources") + } + + return res, nil +} + +func (c *Client) CreateClientAuthorizationResource(specClient *v1alpha1.KeycloakAPIClient, specResource *v1alpha1.KeycloakResource, realmName string) (string, error) { + return c.create( + specResource, + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/resource", realmName, specClient.ID), + clientAuthorizationResourceResourceName, + ) +} + +func (c *Client) UpdateClientAuthorizationResource(specClient *v1alpha1.KeycloakAPIClient, newResource *v1alpha1.KeycloakResource, oldResource *v1alpha1.KeycloakResource, realmName string) error { + return c.update( + newResource, + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/resource/%s", realmName, specClient.ID, oldResource.ID), + clientAuthorizationResourceResourceName, + ) +} + +func (c *Client) DeleteClientAuthorizationResource(specClient *v1alpha1.KeycloakAPIClient, specResource *v1alpha1.KeycloakResource, realmName string) error { + return c.delete( + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/resource/%s", realmName, specClient.ID, specResource.ID), + clientAuthorizationResourceResourceName, + nil, + ) +} + +func (c *Client) ListClientAuthorizationPolicies(clientID, realmName string) ([]v1alpha1.KeycloakPolicy, error) { + result, err := c.list( + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/policy", realmName, clientID), + clientAuthorizationPolicyResourceName, + func(body []byte) (T, error) { + var policies []v1alpha1.KeycloakPolicy + err := json.Unmarshal(body, &policies) + return policies, err + }, + ) + + if err != nil { + return nil, err + } + + res, ok := result.([]v1alpha1.KeycloakPolicy) + + if !ok { + return nil, errors.Errorf("error decoding list client authorization service => policies") + } + + return res, nil +} + +func (c *Client) CreateClientAuthorizationPolicy(specClient *v1alpha1.KeycloakAPIClient, specPolicy *v1alpha1.KeycloakPolicy, realmName string) (string, error) { + return c.create( + specPolicy, + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/policy", realmName, specClient.ID), + clientAuthorizationPolicyResourceName, + ) +} + +func (c *Client) UpdateClientAuthorizationPolicy(specClient *v1alpha1.KeycloakAPIClient, newPolicy *v1alpha1.KeycloakPolicy, oldPolicy *v1alpha1.KeycloakPolicy, realmName string) error { + return c.update( + newPolicy, + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/policy/%s", realmName, specClient.ID, + oldPolicy.ID), + clientAuthorizationPolicyResourceName, + ) +} + +func (c *Client) DeleteClientAuthorizationPolicy(specClient *v1alpha1.KeycloakAPIClient, specPolicy *v1alpha1.KeycloakPolicy, realmName string) error { + return c.delete( + fmt.Sprintf("realms/%s/clients/%s/authz/resource-server/policy/%s", realmName, specClient.ID, specPolicy.ID), + clientAuthorizationPolicyResourceName, + nil, + ) +} + func (c *Client) CreateUser(user *v1alpha1.KeycloakAPIUser, realmName string) (string, error) { return c.create(user, fmt.Sprintf("realms/%s/users", realmName), "user") } @@ -963,6 +1084,18 @@ type KeycloakInterface interface { UpdateClientOptionalClientScope(specClient *v1alpha1.KeycloakAPIClient, clientScope *v1alpha1.KeycloakClientScope, realmName string) error DeleteClientOptionalClientScope(specClient *v1alpha1.KeycloakAPIClient, clientScope *v1alpha1.KeycloakClientScope, realmName string) error + ListClientAuthorizationResources(clientID, realmName string) ([]v1alpha1.KeycloakResource, error) + CreateClientAuthorizationResource(specClient *v1alpha1.KeycloakAPIClient, specResource *v1alpha1.KeycloakResource, realmName string) (string, error) + UpdateClientAuthorizationResource(specClient *v1alpha1.KeycloakAPIClient, newResource *v1alpha1.KeycloakResource, oldResource *v1alpha1.KeycloakResource, realmName string) error + DeleteClientAuthorizationResource(specClient *v1alpha1.KeycloakAPIClient, specResource *v1alpha1.KeycloakResource, realmName string) error + + // TODO implement API for updating authorization scopes? + + ListClientAuthorizationPolicies(clientID, realmName string) ([]v1alpha1.KeycloakPolicy, error) + CreateClientAuthorizationPolicy(specClient *v1alpha1.KeycloakAPIClient, specPolicy *v1alpha1.KeycloakPolicy, realmName string) (string, error) + UpdateClientAuthorizationPolicy(specClient *v1alpha1.KeycloakAPIClient, newPolicy *v1alpha1.KeycloakPolicy, oldPolicy *v1alpha1.KeycloakPolicy, realmName string) error + DeleteClientAuthorizationPolicy(specClient *v1alpha1.KeycloakAPIClient, specPolicy *v1alpha1.KeycloakPolicy, realmName string) error + CreateUser(user *v1alpha1.KeycloakAPIUser, realmName string) (string, error) CreateFederatedIdentity(fid v1alpha1.FederatedIdentity, userID string, realmName string) (string, error) RemoveFederatedIdentity(fid v1alpha1.FederatedIdentity, userID string, realmName string) error @@ -1006,7 +1139,7 @@ var _ KeycloakInterface = &Client{} //go:generate moq -out keycloakClientFactory_moq.go . KeycloakClientFactory -//KeycloakClientFactory interface +// KeycloakClientFactory interface type KeycloakClientFactory interface { AuthenticatedClient(kc v1alpha1.ExternalKeycloak) (KeycloakInterface, error) } diff --git a/pkg/common/client_state.go b/pkg/common/client_state.go index 3a20c3422..847ce1816 100644 --- a/pkg/common/client_state.go +++ b/pkg/common/client_state.go @@ -25,6 +25,9 @@ type ClientState struct { DeprecatedClientSecret *v1.Secret // keycloak-client-secret- Keycloak kc.ExternalKeycloak ServiceAccountUserState *UserState + AuthorizationPolicies []kc.KeycloakPolicy + AuthorizationResources []kc.KeycloakResource + AuthorizationScopes []kc.KeycloakScope } func NewClientState(context context.Context, realm *kc.KeycloakRealm, keycloak kc.ExternalKeycloak) *ClientState { @@ -96,6 +99,20 @@ func (i *ClientState) Read(context context.Context, cr *kc.KeycloakClient, realm return err } + if i.Client.AuthorizationServicesEnabled { + i.AuthorizationResources, err = realmClient.ListClientAuthorizationResources(cr.Spec.Client.ID, i.Realm.Spec.Realm.Realm) + if err != nil { + return err + } + + i.AuthorizationPolicies, err = realmClient.ListClientAuthorizationPolicies(cr.Spec.Client.ID, i.Realm.Spec.Realm.Realm) + if err != nil { + return err + } + + // TODO implement API for updating authorization scopes? + } + if i.Client.ServiceAccountsEnabled { user, err := realmClient.GetServiceAccountUser(i.Realm.Spec.Realm.Realm, cr.Spec.Client.ID) if err != nil { diff --git a/pkg/common/cluster_actions.go b/pkg/common/cluster_actions.go index 3e6fdcd0b..e76574484 100644 --- a/pkg/common/cluster_actions.go +++ b/pkg/common/cluster_actions.go @@ -41,6 +41,12 @@ type ActionRunner interface { DeleteClientDefaultClientScope(keycloakClient *v1alpha1.KeycloakClient, clientScope *v1alpha1.KeycloakClientScope, realm string) error UpdateClientOptionalClientScope(keycloakClient *v1alpha1.KeycloakClient, clientScope *v1alpha1.KeycloakClientScope, realm string) error DeleteClientOptionalClientScope(keycloakClient *v1alpha1.KeycloakClient, clientScope *v1alpha1.KeycloakClientScope, realm string) error + DeleteClientAuthorizationResource(keycloakClient *v1alpha1.KeycloakClient, resource *v1alpha1.KeycloakResource, realm string) error + CreateClientAuthorizationResource(keycloakClient *v1alpha1.KeycloakClient, resource *v1alpha1.KeycloakResource, realm string) error + UpdateClientAuthorizationResource(keycloakClient *v1alpha1.KeycloakClient, newResource *v1alpha1.KeycloakResource, oldResource *v1alpha1.KeycloakResource, realm string) error + CreateClientAuthorizationPolicy(keycloakClient *v1alpha1.KeycloakClient, policy *v1alpha1.KeycloakPolicy, realm string) error + UpdateClientAuthorizationPolicy(keycloakClient *v1alpha1.KeycloakClient, newPolicy *v1alpha1.KeycloakPolicy, oldPolicy *v1alpha1.KeycloakPolicy, realm string) error + DeleteClientAuthorizationPolicy(keycloakClient *v1alpha1.KeycloakClient, policy *v1alpha1.KeycloakPolicy, realm string) error CreateUser(obj *v1alpha1.KeycloakUser, realm string) error UpdateUser(obj *v1alpha1.KeycloakUser, realm string) error DeleteUser(id, realm string) error @@ -142,15 +148,38 @@ func (i *ClusterActionRunner) CreateClient(obj *v1alpha1.KeycloakClient, realm s return errors.Errorf("cannot perform client create when client is nil") } + // Keep original KeycloakClient struct + originalObj := obj.DeepCopy() + + // Remove authorization settings + obj.Spec.Client.AuthorizationSettings = nil + uid, err := i.keycloakClient.CreateClient(obj.Spec.Client, realm) if err != nil { return err } - obj.Spec.Client.ID = uid + // Set UUID of created client in original KeycloakClient and return that + originalObj.Spec.Client.ID = uid - return i.client.Update(i.context, obj) + // Remove default authorization service entities before proceeding + if originalObj.Spec.Client.AuthorizationServicesEnabled { + if resources, err := i.keycloakClient.ListClientAuthorizationResources(uid, realm); err == nil { + for _, resource := range resources { + err := i.keycloakClient.DeleteClientAuthorizationResource(originalObj.Spec.Client, resource.DeepCopy(), realm) + if err != nil { + return err + } + } + } else { + return err + } + + // TODO scopes? + } + + return i.client.Update(i.context, originalObj) } func (i *ClusterActionRunner) UpdateClient(obj *v1alpha1.KeycloakClient, realm string) error { @@ -401,6 +430,82 @@ func (i *ClusterActionRunner) configureBrowserRedirector(provider, flow string, return nil } +func (i *ClusterActionRunner) DeleteClientAuthorizationResource(keycloakClient *v1alpha1.KeycloakClient, resource *v1alpha1.KeycloakResource, realm string) error { + if i.keycloakClient == nil { + return errors.Errorf("cannot perform authorization resource delete when client is nil") + } + + return i.keycloakClient.DeleteClientAuthorizationResource(keycloakClient.Spec.Client, resource, realm) +} + +func (i *ClusterActionRunner) CreateClientAuthorizationResource(keycloakClient *v1alpha1.KeycloakClient, resource *v1alpha1.KeycloakResource, realm string) error { + if i.keycloakClient == nil { + return errors.Errorf("cannot perform authorization resource create when client is nil") + } + + // We need to set the ID of the created resource so the operator is able to update these resource using their IDs. + resourceID, err := i.keycloakClient.CreateClientAuthorizationResource(keycloakClient.Spec.Client, resource, realm) + if err != nil { + return err + } + + resources := keycloakClient.Spec.Client.AuthorizationSettings.Resources + for idx, _ := range resources { + if resources[idx].Name == resource.Name { + resources[idx].ID = resourceID + break + } + } + + return i.client.Update(i.context, keycloakClient) +} + +func (i *ClusterActionRunner) UpdateClientAuthorizationResource(keycloakClient *v1alpha1.KeycloakClient, newResource *v1alpha1.KeycloakResource, oldResource *v1alpha1.KeycloakResource, realm string) error { + if i.keycloakClient == nil { + return errors.Errorf("cannot perform authorization resource update when client is nil") + } + + return i.keycloakClient.UpdateClientAuthorizationResource(keycloakClient.Spec.Client, newResource, oldResource, realm) +} + +func (i *ClusterActionRunner) CreateClientAuthorizationPolicy(keycloakClient *v1alpha1.KeycloakClient, policy *v1alpha1.KeycloakPolicy, realm string) error { + if i.keycloakClient == nil { + return errors.Errorf("cannot perform authorization policy create when client is nil") + } + + // We need to set the ID of the created policy so the operator is able to update these policies using their IDs. + policyID, err := i.keycloakClient.CreateClientAuthorizationPolicy(keycloakClient.Spec.Client, policy, realm) + if err != nil { + return err + } + + policies := keycloakClient.Spec.Client.AuthorizationSettings.Policies + for idx, _ := range policies { + if policies[idx].Name == policy.Name { + policies[idx].ID = policyID + break + } + } + + return i.client.Update(i.context, keycloakClient) +} + +func (i *ClusterActionRunner) UpdateClientAuthorizationPolicy(keycloakClient *v1alpha1.KeycloakClient, newPolicy *v1alpha1.KeycloakPolicy, oldPolicy *v1alpha1.KeycloakPolicy, realm string) error { + if i.keycloakClient == nil { + return errors.Errorf("cannot perform authorization policy update when client is nil") + } + + return i.keycloakClient.UpdateClientAuthorizationPolicy(keycloakClient.Spec.Client, newPolicy, oldPolicy, realm) +} + +func (i *ClusterActionRunner) DeleteClientAuthorizationPolicy(keycloakClient *v1alpha1.KeycloakClient, policy *v1alpha1.KeycloakPolicy, realm string) error { + if i.keycloakClient == nil { + return errors.Errorf("cannot perform authorization policy delete when client is nil") + } + + return i.keycloakClient.DeleteClientAuthorizationPolicy(keycloakClient.Spec.Client, policy, realm) +} + // An action to create generic kubernetes resources // (resources that don't require special treatment) type GenericCreateAction struct { @@ -601,6 +706,50 @@ type RemoveClientRoleAction struct { Msg string } +type DeleteClientAuthorizationResourceAction struct { + AuthorizationResource *v1alpha1.KeycloakResource + Ref *v1alpha1.KeycloakClient + Realm string + Msg string +} + +type CreateClientAuthorizationResourceAction struct { + AuthorizationResource *v1alpha1.KeycloakResource + Ref *v1alpha1.KeycloakClient + Realm string + Msg string +} + +type UpdateClientAuthorizationResourceAction struct { + NewAuthorizationResource *v1alpha1.KeycloakResource + OldAuthorizationResource *v1alpha1.KeycloakResource + Ref *v1alpha1.KeycloakClient + Realm string + Msg string +} + +type DeleteClientAuthorizationPolicyAction struct { + AuthorizationPolicy *v1alpha1.KeycloakPolicy + Ref *v1alpha1.KeycloakClient + Realm string + Msg string +} + +type CreateClientAuthorizationPolicyAction struct { + AuthorizationPolicy *v1alpha1.KeycloakPolicy + Ref *v1alpha1.KeycloakClient + Realm string + Msg string +} + +type UpdateClientAuthorizationPolicyAction struct { + NewAuthorizationPolicy *v1alpha1.KeycloakPolicy + OldAuthorizationPolicy *v1alpha1.KeycloakPolicy + Ref *v1alpha1.KeycloakClient + Realm string + Msg string +} + func (i GenericCreateAction) Run(runner ActionRunner) (string, error) { return i.Msg, runner.Create(i.Ref) } @@ -720,3 +869,27 @@ func (i AssignClientRoleAction) Run(runner ActionRunner) (string, error) { func (i RemoveClientRoleAction) Run(runner ActionRunner) (string, error) { return i.Msg, runner.RemoveClientRole(i.Ref, i.ClientID, i.UserID, i.Realm) } + +func (i DeleteClientAuthorizationPolicyAction) Run(runner ActionRunner) (string, error) { + return i.Msg, runner.DeleteClientAuthorizationPolicy(i.Ref, i.AuthorizationPolicy, i.Realm) +} + +func (i CreateClientAuthorizationPolicyAction) Run(runner ActionRunner) (string, error) { + return i.Msg, runner.CreateClientAuthorizationPolicy(i.Ref, i.AuthorizationPolicy, i.Realm) +} + +func (i UpdateClientAuthorizationPolicyAction) Run(runner ActionRunner) (string, error) { + return i.Msg, runner.UpdateClientAuthorizationPolicy(i.Ref, i.NewAuthorizationPolicy, i.OldAuthorizationPolicy, i.Realm) +} + +func (i DeleteClientAuthorizationResourceAction) Run(runner ActionRunner) (string, error) { + return i.Msg, runner.DeleteClientAuthorizationResource(i.Ref, i.AuthorizationResource, i.Realm) +} + +func (i CreateClientAuthorizationResourceAction) Run(runner ActionRunner) (string, error) { + return i.Msg, runner.CreateClientAuthorizationResource(i.Ref, i.AuthorizationResource, i.Realm) +} + +func (i UpdateClientAuthorizationResourceAction) Run(runner ActionRunner) (string, error) { + return i.Msg, runner.UpdateClientAuthorizationResource(i.Ref, i.NewAuthorizationResource, i.OldAuthorizationResource, i.Realm) +} diff --git a/pkg/controller/keycloakclient/keycloakclient_reconciler.go b/pkg/controller/keycloakclient/keycloakclient_reconciler.go index 3c96ae23d..bd2f90390 100644 --- a/pkg/controller/keycloakclient/keycloakclient_reconciler.go +++ b/pkg/controller/keycloakclient/keycloakclient_reconciler.go @@ -5,6 +5,7 @@ import ( "github.com/keycloak/keycloak-realm-operator/pkg/controller/keycloakuser" + "github.com/keycloak/keycloak-realm-operator/pkg/apis/keycloak/v1alpha1" kc "github.com/keycloak/keycloak-realm-operator/pkg/apis/keycloak/v1alpha1" "github.com/keycloak/keycloak-realm-operator/pkg/common" "github.com/keycloak/keycloak-realm-operator/pkg/model" @@ -63,6 +64,12 @@ func (i *KeycloakClientReconciler) Reconcile(state *common.ClientState, cr *kc.K i.ReconcileDefaultClientRoles(state, cr, &desired) + if cr.Spec.Client.AuthorizationServicesEnabled { + i.ReconcileAuthorizationResources(state, cr, &desired) + + i.ReconcileAuthorizationPolicies(state, cr, &desired) + } + if cr.Spec.Client.ServiceAccountsEnabled { i.ReconcileServiceAccountRoles(state, cr, &desired) } @@ -171,6 +178,117 @@ func (i *KeycloakClientReconciler) ReconcileClientScopes(state *common.ClientSta } } +func (i *KeycloakClientReconciler) ReconcileAuthorizationResources(state *common.ClientState, cr *kc.KeycloakClient, desired *common.DesiredClusterState) { + if cr.Spec.Client.AuthorizationSettings.Resources != nil { + resourcesDeleted, _ := model.AuthorizationResourcesDifferenceIntersection(state.AuthorizationResources, cr.Spec.Client.AuthorizationSettings.Resources) + + // Delete any resources that only exist in state, but not in CR + for _, resource := range resourcesDeleted { + desired.AddAction(i.getDeletedClientAuthorizationResourceState(state, cr, resource.DeepCopy())) + } + + // Track resources that exist in state + existingResourcesById := make(map[string]kc.KeycloakResource) + existingResourcesByName := make(map[string]kc.KeycloakResource) // Resource names are unique + for _, resource := range state.AuthorizationResources { + existingResourcesById[resource.ID] = resource + existingResourcesByName[resource.Name] = resource + } + + // Check for new resources in CR, and matching resources in both CR and state + newResources, matchingResources := model.AuthorizationResourcesDifferenceIntersection(cr.Spec.Client.AuthorizationSettings.Resources, state.AuthorizationResources) + renamedPoliciesOldNames := make(map[string]bool) + for _, resource := range matchingResources { + // If their ID exists, update that resource + if resource.ID != "" { + // In case the name changes, we need the previous revision of the resource and use its ID + oldResource := existingResourcesById[resource.ID] + desired.AddAction(i.getUpdatedClientAuthorizationResourceState(state, cr, resource.DeepCopy(), oldResource.DeepCopy())) + + // If the name has changed, keep track of old names before renaming + if resource.Name != oldResource.Name { + renamedPoliciesOldNames[oldResource.Name] = true + } + } + } + + // seemingly matching resources without an ID can either be regular updates + // or re-creations after renames (not deletions) + for _, resource := range matchingResources { + if resource.ID == "" { + if _, contains := renamedPoliciesOldNames[resource.Name]; contains { + desired.AddAction(i.getCreatedClientAuthorizationResourceState(state, cr, resource.DeepCopy())) + } else { + resource.ID = existingResourcesByName[resource.Name].ID + desired.AddAction(i.getUpdatedClientAuthorizationResourceState(state, cr, resource.DeepCopy(), resource.DeepCopy())) + } + } + } + + // always create resources that don't match any existing ones + for _, resource := range newResources { + desired.AddAction(i.getCreatedClientAuthorizationResourceState(state, cr, resource.DeepCopy())) + } + } else { + log.Info("Authorization => resources not found, skipping authorization resource reconciliation") + } +} + +func (i *KeycloakClientReconciler) ReconcileAuthorizationPolicies(state *common.ClientState, cr *kc.KeycloakClient, desired *common.DesiredClusterState) { + if cr.Spec.Client.AuthorizationSettings.Policies != nil { + policiesDeleted, _ := model.AuthorizationPoliciesDifferenceIntersection(state.AuthorizationPolicies, cr.Spec.Client.AuthorizationSettings.Policies) + + // Delete any policies that only exist in state, but not in CR + for _, policy := range policiesDeleted { + desired.AddAction(i.getDeletedClientAuthorizationPolicyState(state, cr, policy.DeepCopy())) + } + + // Track policies that exist in state + existingPoliciesById := make(map[string]v1alpha1.KeycloakPolicy) + existingPoliciesByName := make(map[string]v1alpha1.KeycloakPolicy) + for _, policy := range state.AuthorizationPolicies { + existingPoliciesById[policy.ID] = policy + existingPoliciesByName[policy.Name] = policy + } + + // Check for new policies in CR, and matching policies in both CR and state + newPolicies, matchingPolicies := model.AuthorizationPoliciesDifferenceIntersection(cr.Spec.Client.AuthorizationSettings.Policies, state.AuthorizationPolicies) + renamedPoliciesOldNames := make(map[string]bool) + for _, policy := range matchingPolicies { + // If their ID exists, update that policy + if policy.ID != "" { + oldPolicy := existingPoliciesById[policy.ID] + desired.AddAction(i.getUpdatedClientAuthorizationPolicyState(state, cr, policy.DeepCopy(), oldPolicy.DeepCopy())) + + // If the name has changed, keep track of old names before renaming + if policy.Name != oldPolicy.Name { + renamedPoliciesOldNames[oldPolicy.Name] = true + } + } + } + + // seemingly matching policies without an ID can either be regular updates + // or re-creations after renames (not deletions) + for _, policy := range matchingPolicies { + if policy.ID == "" { + if _, contains := renamedPoliciesOldNames[policy.Name]; contains { + desired.AddAction(i.getCreatedClientAuthorizationPolicyState(state, cr, policy.DeepCopy())) + } else { + policy.ID = existingPoliciesByName[policy.Name].ID + desired.AddAction(i.getUpdatedClientAuthorizationPolicyState(state, cr, policy.DeepCopy(), policy.DeepCopy())) + } + } + } + + // always create policies that don't match any existing ones + for _, policy := range newPolicies { + desired.AddAction(i.getCreatedClientAuthorizationPolicyState(state, cr, policy.DeepCopy())) + } + } else { + log.Info("Authorization => policies not found, skipping authorization settings reconciliation") + } +} + func (i *KeycloakClientReconciler) ReconcileServiceAccountRoles(state *common.ClientState, cr *kc.KeycloakClient, desired *common.DesiredClusterState) { if state.ServiceAccountUserState != nil { log.Info("Reconciling service account roles") @@ -459,3 +577,59 @@ func (i *KeycloakClientReconciler) getDeletedClientOptionalClientScopeState(stat Msg: fmt.Sprintf("delete client optional client scope %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, clientScope.Name), } } + +func (i *KeycloakClientReconciler) getCreatedClientAuthorizationResourceState(state *common.ClientState, cr *kc.KeycloakClient, policy *kc.KeycloakResource) common.ClusterAction { + return common.CreateClientAuthorizationResourceAction{ + AuthorizationResource: policy, + Ref: cr, + Realm: state.Realm.Spec.Realm.Realm, + Msg: fmt.Sprintf("create client authorization resource %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, policy.Name), + } +} + +func (i *KeycloakClientReconciler) getUpdatedClientAuthorizationResourceState(state *common.ClientState, cr *kc.KeycloakClient, newResource *kc.KeycloakResource, oldResource *kc.KeycloakResource) common.ClusterAction { + return common.UpdateClientAuthorizationResourceAction{ + NewAuthorizationResource: newResource, + OldAuthorizationResource: oldResource, + Ref: cr, + Realm: state.Realm.Spec.Realm.Realm, + Msg: fmt.Sprintf("update client authorization resource %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, newResource.Name), + } +} + +func (i *KeycloakClientReconciler) getDeletedClientAuthorizationResourceState(state *common.ClientState, cr *kc.KeycloakClient, resource *kc.KeycloakResource) common.ClusterAction { + return common.DeleteClientAuthorizationResourceAction{ + AuthorizationResource: resource, + Ref: cr, + Realm: state.Realm.Spec.Realm.Realm, + Msg: fmt.Sprintf("delete client authorization resource %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, resource.Name), + } +} + +func (i *KeycloakClientReconciler) getCreatedClientAuthorizationPolicyState(state *common.ClientState, cr *kc.KeycloakClient, policy *kc.KeycloakPolicy) common.ClusterAction { + return common.CreateClientAuthorizationPolicyAction{ + AuthorizationPolicy: policy, + Ref: cr, + Realm: state.Realm.Spec.Realm.Realm, + Msg: fmt.Sprintf("create client authorization policy %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, policy.Name), + } +} + +func (i *KeycloakClientReconciler) getUpdatedClientAuthorizationPolicyState(state *common.ClientState, cr *kc.KeycloakClient, newPolicy *kc.KeycloakPolicy, oldPolicy *kc.KeycloakPolicy) common.ClusterAction { + return common.UpdateClientAuthorizationPolicyAction{ + NewAuthorizationPolicy: newPolicy, + OldAuthorizationPolicy: oldPolicy, + Ref: cr, + Realm: state.Realm.Spec.Realm.Realm, + Msg: fmt.Sprintf("update client authorization policy %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, newPolicy.Name), + } +} + +func (i *KeycloakClientReconciler) getDeletedClientAuthorizationPolicyState(state *common.ClientState, cr *kc.KeycloakClient, policy *kc.KeycloakPolicy) common.ClusterAction { + return common.DeleteClientAuthorizationPolicyAction{ + AuthorizationPolicy: policy, + Ref: cr, + Realm: state.Realm.Spec.Realm.Realm, + Msg: fmt.Sprintf("delete client authorization policy %v/%v => %v", cr.Namespace, cr.Spec.Client.ClientID, policy.Name), + } +} diff --git a/pkg/controller/keycloakclient/keycloakclient_reconciler_test.go b/pkg/controller/keycloakclient/keycloakclient_reconciler_test.go index 1e2a1e000..bad2fb011 100644 --- a/pkg/controller/keycloakclient/keycloakclient_reconciler_test.go +++ b/pkg/controller/keycloakclient/keycloakclient_reconciler_test.go @@ -203,6 +203,64 @@ func TestKeycloakClientReconciler_Test_Update_Client(t *testing.T) { AuthorizationServicesEnabled: true, DefaultClientScopes: []string{"profile"}, OptionalClientScopes: []string{"email"}, + AuthorizationSettings: &v1alpha1.KeycloakResourceServer{ + AllowRemoteResourceManagement: true, + DecisionStrategy: "UNANIMOUS", + PolicyEnforcementMode: "ENFORCING", + Policies: []v1alpha1.KeycloakPolicy{ + { + ID: "update-policy-1", + Name: "Update Policy", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "role", + Config: map[string]string{"roles": "[{\"id\":\"test/update\",\"required\":true}]"}, + }, + { + ID: "update-permission-1", + Name: "Update Permission 2", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "resource", + Config: map[string]string{ + "applyPolicies": "[\"Update Policy\"", + "resources": "[\"Update Resource\"]", + }, + }, + { + Name: "Patch Policy", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "role", + Config: map[string]string{"roles": "[{\"id\":\"test/update\",\"required\":true}]"}, + }, + { + Name: "Patch Permission", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "resource", + Config: map[string]string{ + "applyPolicies": "[\"Patch Policy\"", + "resources": "[\"Patch Resource\"]", + }, + }, + }, + Resources: []v1alpha1.KeycloakResource{ + { + ID: "update-resource-1", + DisplayName: "Update Resource Updated!", + Name: "Update Resource", + OwnerManagedAccess: false, + Uris: []string{"/api/update/*"}, + }, + { + DisplayName: "Patch Resource", + Name: "Patch Resource", + OwnerManagedAccess: false, + Uris: []string{"/api/patch/*"}, + }, + }, + }, }, Roles: []v1alpha1.RoleRepresentation{ {ID: "delete_recreateID2", Name: "delete_recreate"}, @@ -243,6 +301,62 @@ func TestKeycloakClientReconciler_Test_Update_Client(t *testing.T) { AvailableClientScopes: []v1alpha1.KeycloakClientScope{{Name: "address", ID: "222"}, {Name: "email", ID: "421"}, {Name: "profile", ID: "314"}}, DefaultClientScopes: []v1alpha1.KeycloakClientScope{}, OptionalClientScopes: []v1alpha1.KeycloakClientScope{{Name: "address", ID: "222"}}, + AuthorizationResources: []v1alpha1.KeycloakResource{ + { + ID: "delete-resource-1", + DisplayName: "Delete Resource", + Name: "Delete Resource", + OwnerManagedAccess: false, + Uris: []string{"/api/delete/*"}, + }, + { + ID: "update-resource-1", + DisplayName: "Update Resource", + Name: "Update Resource", + OwnerManagedAccess: false, + Uris: []string{"/api/update/*"}, + }, + }, + AuthorizationPolicies: []v1alpha1.KeycloakPolicy{ + { + ID: "delete-policy-1", + Name: "Delete Policy", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "role", + Config: map[string]string{"roles": "[{\"id\":\"test/update\",\"required\":true}]"}, + }, + { + ID: "delete-permission-1", + Name: "Delete Permission", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "resource", + Config: map[string]string{ + "applyPolicies": "[\"Delete Policy\"", + "resources": "[\"Delete Resource\"]", + }, + }, + { + ID: "update-policy-1", + Name: "Update Policy", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "role", + Config: map[string]string{"roles": "[{\"id\":\"test/update\",\"required\":true}]"}, + }, + { + ID: "update-permission-1", + Name: "Update Permission", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "resource", + Config: map[string]string{ + "applyPolicies": "[\"Update Policy\"", + "resources": "[\"Update Resource\"]", + }, + }, + }, } // when @@ -289,7 +403,34 @@ func TestKeycloakClientReconciler_Test_Update_Client(t *testing.T) { assert.IsType(t, common.DeleteClientOptionalClientScopeAction{}, desiredState[16]) assert.Equal(t, "222", desiredState[16].(common.DeleteClientOptionalClientScopeAction).ClientScope.ID) - assert.Equal(t, 17, len(desiredState)) + assert.IsType(t, common.DeleteClientAuthorizationResourceAction{}, desiredState[17]) + assert.Equal(t, "delete-resource-1", desiredState[17].(common.DeleteClientAuthorizationResourceAction).AuthorizationResource.ID) + assert.IsType(t, common.UpdateClientAuthorizationResourceAction{}, desiredState[18]) + assert.Equal(t, "update-resource-1", desiredState[18].(common.UpdateClientAuthorizationResourceAction).NewAuthorizationResource.ID) + assert.IsType(t, common.CreateClientAuthorizationResourceAction{}, desiredState[19]) + assert.Equal(t, "Patch Resource", desiredState[19].(common.CreateClientAuthorizationResourceAction). + AuthorizationResource.Name) + + assert.IsType(t, common.DeleteClientAuthorizationPolicyAction{}, desiredState[20]) + assert.Equal(t, "delete-policy-1", desiredState[20].(common.DeleteClientAuthorizationPolicyAction). + AuthorizationPolicy.ID) + assert.IsType(t, common.DeleteClientAuthorizationPolicyAction{}, desiredState[21]) + assert.Equal(t, "delete-permission-1", desiredState[21].(common.DeleteClientAuthorizationPolicyAction). + AuthorizationPolicy.ID) + assert.IsType(t, common.UpdateClientAuthorizationPolicyAction{}, desiredState[22]) + assert.Equal(t, "update-policy-1", desiredState[22].(common.UpdateClientAuthorizationPolicyAction). + NewAuthorizationPolicy.ID) + assert.IsType(t, common.UpdateClientAuthorizationPolicyAction{}, desiredState[23]) + assert.Equal(t, "update-permission-1", desiredState[23].(common.UpdateClientAuthorizationPolicyAction). + NewAuthorizationPolicy.ID) + assert.IsType(t, common.CreateClientAuthorizationPolicyAction{}, desiredState[24]) + assert.Equal(t, "Patch Policy", desiredState[24].(common.CreateClientAuthorizationPolicyAction). + AuthorizationPolicy.Name) + assert.IsType(t, common.CreateClientAuthorizationPolicyAction{}, desiredState[25]) + assert.Equal(t, "Patch Permission", desiredState[25].(common.CreateClientAuthorizationPolicyAction). + AuthorizationPolicy.Name) + + assert.Equal(t, 26, len(desiredState)) } func TestKeycloakClientReconciler_Test_Marshal_Client(t *testing.T) { diff --git a/pkg/model/util.go b/pkg/model/util.go index 3dcd824e2..f3b5fb617 100644 --- a/pkg/model/util.go +++ b/pkg/model/util.go @@ -219,6 +219,82 @@ func FilterClientScopesByNames(clientScopes []v1alpha1.KeycloakClientScope, name return filteredScopes } +func AuthorizationPoliciesDifferenceIntersection(a []v1alpha1.KeycloakPolicy, b []v1alpha1.KeycloakPolicy) (d []v1alpha1.KeycloakPolicy, i []v1alpha1.KeycloakPolicy) { + for _, policy := range a { + if hasMatchingPolicy(b, policy) { + i = append(i, policy) + } else { + d = append(d, policy) + } + } + + return d, i +} + +func hasMatchingPolicy(policies []v1alpha1.KeycloakPolicy, otherPolicy v1alpha1.KeycloakPolicy) bool { + for _, policy := range policies { + if policyMatches(policy, otherPolicy) { + return true + } + } + + return false +} + +func policyMatches(a v1alpha1.KeycloakPolicy, b v1alpha1.KeycloakPolicy) bool { + if a.ID != "" && b.ID != "" { + return a.ID == b.ID + } + + return a.Name == b.Name +} + +func AuthorizationResourcesDifferenceIntersection(a []v1alpha1.KeycloakResource, b []v1alpha1.KeycloakResource) (d []v1alpha1.KeycloakResource, i []v1alpha1.KeycloakResource) { + for _, resource := range a { + if hasMatchingResource(b, resource) { + i = append(i, resource) + } else { + d = append(d, resource) + } + } + + return d, i +} + +func hasMatchingResource(resources []v1alpha1.KeycloakResource, otherResource v1alpha1.KeycloakResource) bool { + for _, resource := range resources { + if resourceMatches(resource, otherResource) { + return true + } + } + + return false +} + +func resourceMatches(a v1alpha1.KeycloakResource, b v1alpha1.KeycloakResource) bool { + if a.ID != "" && b.ID != "" { + return a.ID == b.ID + } + + return a.Name == b.Name +} + +func FilterAuthorizationPoliciesByName(policies []v1alpha1.KeycloakPolicy, names []string) (filteredPolicies []v1alpha1.KeycloakPolicy) { + hashMap := make(map[string]v1alpha1.KeycloakPolicy) + + for _, policy := range policies { + hashMap[policy.Name] = policy + } + + for _, name := range names { + if policy, retrieved := hashMap[name]; retrieved { + filteredPolicies = append(filteredPolicies, policy) + } + } + + return filteredPolicies +} + func SanitizeResourceNameWithAlphaNum(text string) string { // we only want letters and numbers reg := []rune(SanitizeResourceName(text))