Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Admin Fine Grained Permissions to Keycloak Client #119

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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
22 changes: 21 additions & 1 deletion api/v1/keycloakclient_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,14 @@ type KeycloakClientSpec struct {
// +optional
ImplicitFlowEnabled bool `json:"implicitFlowEnabled,omitempty"`

// ServiceAccountsEnabled enable/disable fine-grained authorization support for a client.
// AuthorizationServicesEnabled enable/disable fine-grained authorization support for a client.
// +optional
AuthorizationServicesEnabled bool `json:"authorizationServicesEnabled,omitempty"`

// AdminFineGrainedPermissionsEnabled enable/disable fine-grained admin permissions for a client.
// +optional
AdminFineGrainedPermissionsEnabled bool `json:"adminFineGrainedPermissionsEnabled,omitempty"`

// BearerOnly is a flag to enable bearer-only.
// +optional
BearerOnly bool `json:"bearerOnly,omitempty"`
Expand Down Expand Up @@ -182,6 +186,11 @@ type KeycloakClientSpec struct {
// +optional
Authorization *Authorization `json:"authorization,omitempty"`

// Permission is a client permissions configuration
// +nullable
// +optional
Permission *AdminFineGrainedPermission `json:"permission,omitempty"`

// AuthenticationFlowBindingOverrides client auth flow overrides
// +optional
AuthenticationFlowBindingOverrides *AuthenticationFlowBindingOverrides `json:"authenticationFlowBindingOverrides,omitempty"`
Expand Down Expand Up @@ -261,6 +270,17 @@ type AuthenticationFlowBindingOverrides struct {
DirectGrant string `json:"directGrant,omitempty"`
}

type AdminFineGrainedPermission struct {
// 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
28 changes: 26 additions & 2 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,10 @@ spec:
spec:
description: KeycloakClientSpec defines the desired state of KeycloakClient.
properties:
adminFineGrainedPermissionsEnabled:
description: AdminFineGrainedPermissionsEnabled enable/disable fine-grained
admin permissions for a client.
type: boolean
adminUrl:
description: |-
AdminUrl is client admin url.
Expand Down Expand Up @@ -400,8 +404,8 @@ spec:
type: array
type: object
authorizationServicesEnabled:
description: ServiceAccountsEnabled enable/disable fine-grained authorization
support for a client.
description: AuthorizationServicesEnabled enable/disable fine-grained
authorization support for a client.
type: boolean
bearerOnly:
description: BearerOnly is a flag to enable bearer-only.
Expand Down Expand Up @@ -466,6 +470,26 @@ spec:
type: string
nullable: true
type: array
permission:
description: Permission is a client permissions configuration
nullable: true
properties:
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
type: object
protocol:
description: Protocol is a client protocol.
nullable: true
Expand Down
7 changes: 6 additions & 1 deletion config/samples/v1_v1_keycloakclient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ spec:
webUrl: https:///example.com
directAccess: true
authorizationServicesEnabled: true
adminFineGrainedPermissionsEnabled: true
serviceAccount:
enabled: true
authorization:
Expand Down Expand Up @@ -115,7 +116,11 @@ spec:
- role-policy
scopes:
- scope1

permission:
scopePermissions:
- name: token-exchange
policies:
- policy1
---

apiVersion: v1
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.AdminFineGrainedPermissionsEnabled && keycloakClient.Spec.Permission != nil {
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.AdminFineGrainedPermissionsEnabled,
}

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.Permission.ScopePermissions); i++ {
name := keycloakClient.Spec.Permission.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.Permission.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,123 @@
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",
AdminFineGrainedPermissionsEnabled: true,
Permission: &keycloakApi.AdminFineGrainedPermission{
ScopePermissions: []keycloakApi.ScopePermissions{
{
Name: "map-role",
Policies: []string{"scope permission"},
},
},
},
},
}).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,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
Browser: "browser",
DirectGrant: "direct grant",
},
AdminFineGrainedPermissionsEnabled: true,
},
}

Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
Eventually(func() bool {
createdKeycloakClient := &keycloakApi.KeycloakClient{}
Expand Down
Loading
Loading