Skip to content

Commit

Permalink
feat: Add Admin Fine Grained Permissions to Keycloak Client
Browse files Browse the repository at this point in the history
Signed-off-by: Douglass Kirkley <doug.kirkley@gmail.com>
  • Loading branch information
dougkirkley committed Jan 2, 2025
1 parent bdfc056 commit be4345a
Show file tree
Hide file tree
Showing 15 changed files with 529 additions and 2 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ jobs:
# Run the make test
make test
- name: Keycloak logs
if: always()
run: kubectl logs --namespace keycloak svc/keycloak -c keycloak --tail 500

- name: Delete the Kubernetes cluster
if: always()
run: kind delete cluster
16 changes: 16 additions & 0 deletions api/v1/keycloakclient_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ type KeycloakClientSpec struct {
// AuthenticationFlowBindingOverrides client auth flow overrides
// +optional
AuthenticationFlowBindingOverrides *AuthenticationFlowBindingOverrides `json:"authenticationFlowBindingOverrides,omitempty"`

// AdminFineGrainedPermissions client admin fine-grained permissions
// +optional
AdminFineGrainedPermissions AdminFineGrainedPermissions `json:"adminFineGrainedPermissions,omitempty"`
}

type ServiceAccount struct {
Expand Down Expand Up @@ -261,6 +265,18 @@ type AuthenticationFlowBindingOverrides struct {
DirectGrant string `json:"directGrant,omitempty"`
}

type AdminFineGrainedPermissions struct {
Enabled bool `json:"enabled"`
// ScopePermissions mapping of scope and the policies attached
// +optional
ScopePermissions []ScopePermissions `json:"scopePermissions,omitempty"`
}

type ScopePermissions struct {
Name string `json:"name"`
Policies []string `json:"policies,omitempty"`
}

// KeycloakClientStatus defines the observed state of KeycloakClient.
type KeycloakClientStatus struct {
// +optional
Expand Down
24 changes: 24 additions & 0 deletions config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ spec:
spec:
description: KeycloakClientSpec defines the desired state of KeycloakClient.
properties:
adminFineGrainedPermissions:
description: AdminFineGrainedPermissions client admin fine-grained
permissions
properties:
enabled:
type: boolean
scopePermissions:
description: ScopePermissions mapping of scope and the policies
attached
items:
properties:
name:
type: string
policies:
items:
type: string
type: array
required:
- name
type: object
type: array
required:
- enabled
type: object
adminUrl:
description: |-
AdminUrl is client admin url.
Expand Down
7 changes: 7 additions & 0 deletions config/samples/v1_v1_keycloakclient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ spec:
- role-policy
scopes:
- scope1
adminFineGrainedPermissions:
enabled: true
scopePermissions:
- name: token-exchange
policies:
- policy1
- policy2

---

Expand Down
1 change: 1 addition & 0 deletions controllers/keycloakclient/chain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func MakeChain(
NewProcessResources(keycloakApiClient),
NewProcessPolicy(keycloakApiClient),
NewProcessPermissions(keycloakApiClient),
NewPutAdminFineGrainedPermissions(keycloakApiClient),
)

return c
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package chain

import (
"context"
"fmt"

"github.com/pkg/errors"
ctrl "sigs.k8s.io/controller-runtime"

keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/adapter"
)

type PutAdminFineGrainedPermissions struct {
keycloakApiClient keycloak.Client
}

func NewPutAdminFineGrainedPermissions(keycloakApiClient keycloak.Client) *PutAdminFineGrainedPermissions {
return &PutAdminFineGrainedPermissions{keycloakApiClient: keycloakApiClient}
}

func (el *PutAdminFineGrainedPermissions) Serve(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
clientID, err := el.keycloakApiClient.GetClientID(keycloakClient.Spec.ClientId, realmName)
if err != nil {
return fmt.Errorf("failed to get client id: %w", err)
}

if err := el.putKeycloakClientAdminFineGrainedPermissions(ctx, keycloakClient, realmName, clientID); err != nil {
return errors.Wrap(err, "unable to put keycloak client admin fine grained permissions")
}

if keycloakClient.Spec.AdminFineGrainedPermissions.Enabled {
if err := el.putKeycloakClientAdminPermissionPolicies(ctx, keycloakClient, realmName, clientID); err != nil {
return errors.Wrap(err, "unable to put keycloak client admin permission policies")
}
}

return nil
}

func (el *PutAdminFineGrainedPermissions) putKeycloakClientAdminFineGrainedPermissions(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName, clientID string) error {
reqLog := ctrl.LoggerFrom(ctx)
reqLog.Info("Start put keycloak client admin fine grained permissions")

managementPermissions := &adapter.ManagementPermissionRepresentation{
Enabled: &keycloakClient.Spec.AdminFineGrainedPermissions.Enabled,
}

if err := el.keycloakApiClient.UpdateClientManagementPermissions(realmName, clientID, managementPermissions); err != nil {
return errors.Wrap(err, "unable to update client management permissions")
}

reqLog.Info("End put keycloak client admin fine grained permissions")

return nil
}

func (el *PutAdminFineGrainedPermissions) putKeycloakClientAdminPermissionPolicies(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName, clientID string) error {
reqLog := ctrl.LoggerFrom(ctx)
reqLog.Info("Start put keycloak client admin permission policies")

realmManagementClientID, err := el.keycloakApiClient.GetClientID("realm-management", realmName)
if err != nil {
return fmt.Errorf("failed to get realm-management client id: %w", err)
}

realmManagementPermissions, err := el.keycloakApiClient.GetPermissions(ctx, realmName, realmManagementClientID)
if err != nil {
return fmt.Errorf("failed to get permissions for realm-management client: %w", err)
}

existingClientPermissions, err := el.keycloakApiClient.GetClientManagementPermissions(realmName, clientID)
if err != nil {
return fmt.Errorf("failed to get client permissions: %w", err)
}

existingScopePermissions := *existingClientPermissions.ScopePermissions

for i := 0; i < len(keycloakClient.Spec.AdminFineGrainedPermissions.ScopePermissions); i++ {
name := keycloakClient.Spec.AdminFineGrainedPermissions.ScopePermissions[i].Name
reqLog.Info("Processing scope permission", scopeLogKey, name)

if _, ok := existingScopePermissions[name]; !ok {
return fmt.Errorf("scope %s not found in permissions", name)
}

permissionName := fmt.Sprintf("%s.permission.client.%s", name, clientID)

if permission, ok := realmManagementPermissions[permissionName]; ok {
permission.Policies = &keycloakClient.Spec.AdminFineGrainedPermissions.ScopePermissions[i].Policies
if err = el.keycloakApiClient.UpdatePermission(ctx, realmName, realmManagementClientID, permission); err != nil {
return fmt.Errorf("failed to update permission %s: %w", permissionName, err)
}

reqLog.Info("Scope permission updated", scopeLogKey, name, permissionLogKey, permissionName)
}
}

reqLog.Info("End put keycloak client admin permission policies")

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package chain

import (
"context"
"testing"

"github.com/Nerzal/gocloak/v12"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/adapter"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/mocks"
)

func TestPutAdminFineGrainedPermissions_Serve(t *testing.T) {
t.Parallel()

tests := []struct {
name string
client func(t *testing.T) client.Client
keycloakClient client.ObjectKey
keycloakApiClient func(t *testing.T) *mocks.MockClient
wantErr require.ErrorAssertionFunc
}{
{
name: "with admin permission enabled",
client: func(t *testing.T) client.Client {
s := runtime.NewScheme()
require.NoError(t, keycloakApi.AddToScheme(s))
require.NoError(t, corev1.AddToScheme(s))

return fake.NewClientBuilder().WithScheme(s).WithObjects(
&keycloakApi.KeycloakClient{
ObjectMeta: metav1.ObjectMeta{
Name: "test-client",
Namespace: "default",
},
Spec: keycloakApi.KeycloakClientSpec{
ClientId: "test-client-id",
AdminFineGrainedPermissions: keycloakApi.AdminFineGrainedPermissions{
Enabled: true,
},
},
}).Build()
},
keycloakClient: client.ObjectKey{
Name: "test-client",
Namespace: "default",
},
keycloakApiClient: func(t *testing.T) *mocks.MockClient {
m := mocks.NewMockClient(t)

scopePermissions := map[string]string{
"map-role": "321",
}

m.On("GetClientID", "test-client-id", "realm").
Return("123", nil).
Once()

m.On("GetClientID", "realm-management", "realm").
Return("567", nil).
Once()

m.On("UpdateClientManagementPermissions", "realm", "123", &adapter.ManagementPermissionRepresentation{
Enabled: gocloak.BoolP(true),
}).
Return(nil)

m.On("GetClientManagementPermissions", "realm", "123").
Return(&adapter.ManagementPermissionRepresentation{
Enabled: gocloak.BoolP(true),
ScopePermissions: &scopePermissions,
}, nil)

m.On("GetPermissions", ctrl.LoggerInto(context.Background(), logr.Discard()), "realm", "567").
Return(map[string]gocloak.PermissionRepresentation{
"token-exchange": {
ID: gocloak.StringP("scope-permission-id"),
Name: gocloak.StringP("scope permission"),
},
"map-role": {
ID: gocloak.StringP("scope-permission2-id"),
Name: gocloak.StringP("scope-permission2"),
},
}, nil).Once()

return m
},
wantErr: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

cl := &keycloakApi.KeycloakClient{}
require.NoError(t, tt.client(t).Get(context.Background(), tt.keycloakClient, cl))

el := NewPutAdminFineGrainedPermissions(tt.keycloakApiClient(t))
err := el.Serve(
ctrl.LoggerInto(context.Background(), logr.Discard()),
cl,
"realm",
)
tt.wantErr(t, err)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
Browser: "browser",
DirectGrant: "direct grant",
},
AdminFineGrainedPermissions: keycloakApi.AdminFineGrainedPermissions{
Enabled: true,
},
},
}

Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
Eventually(func() bool {
createdKeycloakClient := &keycloakApi.KeycloakClient{}
Expand Down
24 changes: 24 additions & 0 deletions deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ spec:
spec:
description: KeycloakClientSpec defines the desired state of KeycloakClient.
properties:
adminFineGrainedPermissions:
description: AdminFineGrainedPermissions client admin fine-grained
permissions
properties:
enabled:
type: boolean
scopePermissions:
description: ScopePermissions mapping of scope and the policies
attached
items:
properties:
name:
type: string
policies:
items:
type: string
type: array
required:
- name
type: object
type: array
required:
- enabled
type: object
adminUrl:
description: |-
AdminUrl is client admin url.
Expand Down
Loading

0 comments on commit be4345a

Please sign in to comment.