Skip to content

Commit a4a78ae

Browse files
dougkirkleyMykolaMarusenko
authored andcommitted
feat: Add Admin Fine Grained Permissions to Keycloak Client
Signed-off-by: Douglass Kirkley <doug.kirkley@gmail.com>
1 parent bdfc056 commit a4a78ae

16 files changed

+547
-9
lines changed

api/v1/keycloakclient_types.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,14 @@ type KeycloakClientSpec struct {
134134
// +optional
135135
ImplicitFlowEnabled bool `json:"implicitFlowEnabled,omitempty"`
136136

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

141+
// AdminFineGrainedPermissionsEnabled enable/disable fine-grained admin permissions for a client.
142+
// +optional
143+
AdminFineGrainedPermissionsEnabled bool `json:"adminFineGrainedPermissionsEnabled,omitempty"`
144+
141145
// BearerOnly is a flag to enable bearer-only.
142146
// +optional
143147
BearerOnly bool `json:"bearerOnly,omitempty"`
@@ -182,6 +186,11 @@ type KeycloakClientSpec struct {
182186
// +optional
183187
Authorization *Authorization `json:"authorization,omitempty"`
184188

189+
// Permission is a client permissions configuration
190+
// +nullable
191+
// +optional
192+
Permission *AdminFineGrainedPermission `json:"permission,omitempty"`
193+
185194
// AuthenticationFlowBindingOverrides client auth flow overrides
186195
// +optional
187196
AuthenticationFlowBindingOverrides *AuthenticationFlowBindingOverrides `json:"authenticationFlowBindingOverrides,omitempty"`
@@ -261,6 +270,17 @@ type AuthenticationFlowBindingOverrides struct {
261270
DirectGrant string `json:"directGrant,omitempty"`
262271
}
263272

273+
type AdminFineGrainedPermission struct {
274+
// ScopePermissions mapping of scope and the policies attached
275+
// +optional
276+
ScopePermissions []ScopePermissions `json:"scopePermissions,omitempty"`
277+
}
278+
279+
type ScopePermissions struct {
280+
Name string `json:"name"`
281+
Policies []string `json:"policies,omitempty"`
282+
}
283+
264284
// KeycloakClientStatus defines the observed state of KeycloakClient.
265285
type KeycloakClientStatus struct {
266286
// +optional

config/crd/bases/v1.edp.epam.com_keycloakclients.yaml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ spec:
4444
spec:
4545
description: KeycloakClientSpec defines the desired state of KeycloakClient.
4646
properties:
47+
adminFineGrainedPermissionsEnabled:
48+
description: AdminFineGrainedPermissionsEnabled enable/disable fine-grained
49+
admin permissions for a client.
50+
type: boolean
4751
adminUrl:
4852
description: |-
4953
AdminUrl is client admin url.
@@ -400,8 +404,8 @@ spec:
400404
type: array
401405
type: object
402406
authorizationServicesEnabled:
403-
description: ServiceAccountsEnabled enable/disable fine-grained authorization
404-
support for a client.
407+
description: AuthorizationServicesEnabled enable/disable fine-grained
408+
authorization support for a client.
405409
type: boolean
406410
bearerOnly:
407411
description: BearerOnly is a flag to enable bearer-only.
@@ -466,6 +470,26 @@ spec:
466470
type: string
467471
nullable: true
468472
type: array
473+
permission:
474+
description: Permission is a client permissions configuration
475+
nullable: true
476+
properties:
477+
scopePermissions:
478+
description: ScopePermissions mapping of scope and the policies
479+
attached
480+
items:
481+
properties:
482+
name:
483+
type: string
484+
policies:
485+
items:
486+
type: string
487+
type: array
488+
required:
489+
- name
490+
type: object
491+
type: array
492+
type: object
469493
protocol:
470494
description: Protocol is a client protocol.
471495
nullable: true

config/samples/v1_v1_keycloakclient.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ spec:
3333
webUrl: https:///example.com
3434
directAccess: true
3535
authorizationServicesEnabled: true
36+
adminFineGrainedPermissionsEnabled: true
3637
serviceAccount:
3738
enabled: true
3839
authorization:
@@ -115,7 +116,11 @@ spec:
115116
- role-policy
116117
scopes:
117118
- scope1
118-
119+
permission:
120+
scopePermissions:
121+
- name: token-exchange
122+
policies:
123+
- policy1
119124
---
120125

121126
apiVersion: v1

controllers/keycloakclient/chain/chain.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func MakeChain(
7070
NewProcessResources(keycloakApiClient),
7171
NewProcessPolicy(keycloakApiClient),
7272
NewProcessPermissions(keycloakApiClient),
73+
NewPutAdminFineGrainedPermissions(keycloakApiClient),
7374
)
7475

7576
return c
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package chain
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
ctrl "sigs.k8s.io/controller-runtime"
8+
9+
keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
10+
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak"
11+
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/adapter"
12+
)
13+
14+
const (
15+
// RealmManagementClient built-in Keycloak client for the realm
16+
// This client manages admin fine-grained permissions for other clients
17+
RealmManagementClient = "realm-management"
18+
)
19+
20+
type PutAdminFineGrainedPermissions struct {
21+
keycloakApiClient keycloak.Client
22+
}
23+
24+
func NewPutAdminFineGrainedPermissions(keycloakApiClient keycloak.Client) *PutAdminFineGrainedPermissions {
25+
return &PutAdminFineGrainedPermissions{keycloakApiClient: keycloakApiClient}
26+
}
27+
28+
func (el *PutAdminFineGrainedPermissions) Serve(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
29+
clientID, err := el.keycloakApiClient.GetClientID(keycloakClient.Spec.ClientId, realmName)
30+
if err != nil {
31+
return fmt.Errorf("failed to get client id: %w", err)
32+
}
33+
34+
if err := el.putKeycloakClientAdminFineGrainedPermissions(ctx, keycloakClient, realmName, clientID); err != nil {
35+
return fmt.Errorf("unable to put keycloak client admin fine grained permissions: %w", err)
36+
}
37+
38+
if keycloakClient.Spec.AdminFineGrainedPermissionsEnabled && keycloakClient.Spec.Permission != nil {
39+
if err := el.putKeycloakClientAdminPermissionPolicies(ctx, keycloakClient, realmName, clientID); err != nil {
40+
return fmt.Errorf("unable to put keycloak client admin permission policies: %w", err)
41+
}
42+
}
43+
44+
return nil
45+
}
46+
47+
func (el *PutAdminFineGrainedPermissions) putKeycloakClientAdminFineGrainedPermissions(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName, clientID string) error {
48+
reqLog := ctrl.LoggerFrom(ctx)
49+
reqLog.Info("Start put keycloak client admin fine grained permissions")
50+
51+
managementPermissions := adapter.ManagementPermissionRepresentation{
52+
Enabled: &keycloakClient.Spec.AdminFineGrainedPermissionsEnabled,
53+
}
54+
55+
if err := el.keycloakApiClient.UpdateClientManagementPermissions(realmName, clientID, managementPermissions); err != nil {
56+
return fmt.Errorf("unable to update client management permissions: %w", err)
57+
}
58+
59+
reqLog.Info("End put keycloak client admin fine grained permissions")
60+
61+
return nil
62+
}
63+
64+
func (el *PutAdminFineGrainedPermissions) putKeycloakClientAdminPermissionPolicies(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName, clientID string) error {
65+
reqLog := ctrl.LoggerFrom(ctx)
66+
reqLog.Info("Start put keycloak client admin permission policies")
67+
68+
realmManagementClientID, err := el.keycloakApiClient.GetClientID(RealmManagementClient, realmName)
69+
if err != nil {
70+
return fmt.Errorf("failed to get %s client id: %w", RealmManagementClient, err)
71+
}
72+
73+
realmManagementPermissions, err := el.keycloakApiClient.GetPermissions(ctx, realmName, realmManagementClientID)
74+
if err != nil {
75+
return fmt.Errorf("failed to get permissions for %s client: %w", RealmManagementClient, err)
76+
}
77+
78+
existingClientPermissions, err := el.keycloakApiClient.GetClientManagementPermissions(realmName, clientID)
79+
if err != nil {
80+
return fmt.Errorf("failed to get client permissions: %w", err)
81+
}
82+
83+
existingScopePermissions := *existingClientPermissions.ScopePermissions
84+
85+
for i := 0; i < len(keycloakClient.Spec.Permission.ScopePermissions); i++ {
86+
name := keycloakClient.Spec.Permission.ScopePermissions[i].Name
87+
reqLog.Info("Processing scope permission", scopeLogKey, name)
88+
89+
if _, ok := existingScopePermissions[name]; !ok {
90+
return fmt.Errorf("scope %s not found in permissions", name)
91+
}
92+
93+
permissionName := fmt.Sprintf("%s.permission.client.%s", name, clientID)
94+
95+
if permission, ok := realmManagementPermissions[permissionName]; ok {
96+
permission.Policies = &keycloakClient.Spec.Permission.ScopePermissions[i].Policies
97+
if err = el.keycloakApiClient.UpdatePermission(ctx, realmName, realmManagementClientID, permission); err != nil {
98+
return fmt.Errorf("failed to update permission %s: %w", permissionName, err)
99+
}
100+
101+
reqLog.Info("Scope permission updated", scopeLogKey, name, permissionLogKey, permissionName)
102+
}
103+
}
104+
105+
reqLog.Info("End put keycloak client admin permission policies")
106+
107+
return nil
108+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package chain
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/Nerzal/gocloak/v12"
8+
"github.com/go-logr/logr"
9+
"github.com/stretchr/testify/require"
10+
corev1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
ctrl "sigs.k8s.io/controller-runtime"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
15+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
16+
17+
keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
18+
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/adapter"
19+
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/mocks"
20+
)
21+
22+
func TestPutAdminFineGrainedPermissions_Serve(t *testing.T) {
23+
t.Parallel()
24+
25+
tests := []struct {
26+
name string
27+
client func(t *testing.T) client.Client
28+
keycloakClient client.ObjectKey
29+
keycloakApiClient func(t *testing.T) *mocks.MockClient
30+
wantErr require.ErrorAssertionFunc
31+
}{
32+
{
33+
name: "with admin permission enabled",
34+
client: func(t *testing.T) client.Client {
35+
s := runtime.NewScheme()
36+
require.NoError(t, keycloakApi.AddToScheme(s))
37+
require.NoError(t, corev1.AddToScheme(s))
38+
39+
return fake.NewClientBuilder().WithScheme(s).WithObjects(
40+
&keycloakApi.KeycloakClient{
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: "test-client",
43+
Namespace: "default",
44+
},
45+
Spec: keycloakApi.KeycloakClientSpec{
46+
ClientId: "test-client-id",
47+
AdminFineGrainedPermissionsEnabled: true,
48+
Permission: &keycloakApi.AdminFineGrainedPermission{
49+
ScopePermissions: []keycloakApi.ScopePermissions{
50+
{
51+
Name: "map-role",
52+
Policies: []string{"scope permission"},
53+
},
54+
},
55+
},
56+
},
57+
}).Build()
58+
},
59+
keycloakClient: client.ObjectKey{
60+
Name: "test-client",
61+
Namespace: "default",
62+
},
63+
keycloakApiClient: func(t *testing.T) *mocks.MockClient {
64+
m := mocks.NewMockClient(t)
65+
66+
scopePermissions := map[string]string{
67+
"map-role": "321",
68+
}
69+
70+
m.On("GetClientID", "test-client-id", "realm").
71+
Return("123", nil).
72+
Once()
73+
74+
m.On("GetClientID", "realm-management", "realm").
75+
Return("567", nil).
76+
Once()
77+
78+
m.On("UpdateClientManagementPermissions", "realm", "123", adapter.ManagementPermissionRepresentation{
79+
Enabled: gocloak.BoolP(true),
80+
}).
81+
Return(nil)
82+
83+
m.On("GetClientManagementPermissions", "realm", "123").
84+
Return(&adapter.ManagementPermissionRepresentation{
85+
Enabled: gocloak.BoolP(true),
86+
ScopePermissions: &scopePermissions,
87+
}, nil)
88+
89+
m.On("GetPermissions", ctrl.LoggerInto(context.Background(), logr.Discard()), "realm", "567").
90+
Return(map[string]gocloak.PermissionRepresentation{
91+
"token-exchange": {
92+
ID: gocloak.StringP("scope-permission-id"),
93+
Name: gocloak.StringP("scope permission"),
94+
},
95+
"map-role": {
96+
ID: gocloak.StringP("scope-permission2-id"),
97+
Name: gocloak.StringP("scope-permission2"),
98+
},
99+
}, nil).Once()
100+
101+
return m
102+
},
103+
wantErr: require.NoError,
104+
},
105+
}
106+
107+
for _, tt := range tests {
108+
t.Run(tt.name, func(t *testing.T) {
109+
t.Parallel()
110+
111+
cl := &keycloakApi.KeycloakClient{}
112+
require.NoError(t, tt.client(t).Get(context.Background(), tt.keycloakClient, cl))
113+
114+
el := NewPutAdminFineGrainedPermissions(tt.keycloakApiClient(t))
115+
err := el.Serve(
116+
ctrl.LoggerInto(context.Background(), logr.Discard()),
117+
cl,
118+
"realm",
119+
)
120+
tt.wantErr(t, err)
121+
})
122+
}
123+
}

controllers/keycloakclient/keycloakclient_controller_integration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
6565
Browser: "browser",
6666
DirectGrant: "direct grant",
6767
},
68+
AdminFineGrainedPermissionsEnabled: true,
6869
},
6970
}
71+
7072
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
7173
Eventually(func() bool {
7274
createdKeycloakClient := &keycloakApi.KeycloakClient{}

0 commit comments

Comments
 (0)