Skip to content

Commit 30e7fc5

Browse files
NilsbakkenNilsbakken
authored andcommitted
Add support for optional client scopes
1 parent e8760b2 commit 30e7fc5

File tree

12 files changed

+383
-4
lines changed

12 files changed

+383
-4
lines changed

api/v1/keycloakclient_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ type KeycloakClientSpec struct {
9898
// +optional
9999
DefaultClientScopes []string `json:"defaultClientScopes,omitempty"`
100100

101+
// OptionalClientScopes is a list of optional client scopes assigned to client.
102+
// +nullable
103+
// +optional
104+
OptionalClientScopes []string `json:"optionalClientScopes,omitempty"`
105+
101106
// RedirectUris is a list of valid URI pattern a browser can redirect to after a successful login.
102107
// Simple wildcards are allowed such as 'https://example.com/*'.
103108
// Relative path can be specified too, such as /my/relative/path/*. Relative paths are relative to the client root URL.

api/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,13 @@ spec:
396396
name:
397397
description: Name is a client name.
398398
type: string
399+
optionalClientScopes:
400+
description: OptionalClientScopes is a list of optional client scopes
401+
assigned to client.
402+
items:
403+
type: string
404+
nullable: true
405+
type: array
399406
protocol:
400407
description: Protocol is a client protocol.
401408
nullable: true

controllers/keycloakclient/chain/put_client_scope.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,50 @@ func (el *PutClientScope) Serve(ctx context.Context, keycloakClient *keycloakApi
2727
}
2828

2929
func (el *PutClientScope) putClientScope(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
30+
if err := el.putDefaultClientScope(ctx, keycloakClient, realmName); err != nil {
31+
return err
32+
}
33+
34+
if err := el.putOptionalClientScope(ctx, keycloakClient, realmName); err != nil {
35+
return err
36+
}
37+
38+
return nil
39+
}
40+
41+
func (el *PutClientScope) putDefaultClientScope(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
3042
kCloakSpec := keycloakClient.Spec
43+
3144
if len(kCloakSpec.DefaultClientScopes) == 0 {
3245
return nil
3346
}
3447

35-
scopes, err := el.keycloakApiClient.GetClientScopesByNames(ctx, realmName, kCloakSpec.DefaultClientScopes)
48+
defaultScopes, err := el.keycloakApiClient.GetClientScopesByNames(ctx, realmName, kCloakSpec.DefaultClientScopes)
49+
if err != nil {
50+
return errors.Wrap(err, "error during GetClientScope")
51+
}
52+
53+
err = el.keycloakApiClient.AddDefaultScopeToClient(ctx, realmName, kCloakSpec.ClientId, defaultScopes)
54+
if err != nil {
55+
return fmt.Errorf("failed to add default scope to client %s: %w", keycloakClient.Name, err)
56+
}
57+
58+
return nil
59+
}
60+
61+
func (el *PutClientScope) putOptionalClientScope(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
62+
kCloakSpec := keycloakClient.Spec
63+
64+
if len(kCloakSpec.OptionalClientScopes) == 0 {
65+
return nil
66+
}
67+
68+
optionalScopes, err := el.keycloakApiClient.GetClientScopesByNames(ctx, realmName, kCloakSpec.OptionalClientScopes)
3669
if err != nil {
3770
return errors.Wrap(err, "error during GetClientScope")
3871
}
3972

40-
err = el.keycloakApiClient.AddDefaultScopeToClient(ctx, realmName, kCloakSpec.ClientId, scopes)
73+
err = el.keycloakApiClient.AddOptionalScopeToClient(ctx, realmName, kCloakSpec.ClientId, optionalScopes)
4174
if err != nil {
4275
return fmt.Errorf("failed to add default scope to client %s: %w", keycloakClient.Name, err)
4376
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package chain
2+
3+
import (
4+
"context"
5+
keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
6+
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/mocks"
7+
"github.com/go-logr/logr"
8+
"github.com/stretchr/testify/mock"
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+
"testing"
17+
)
18+
19+
func TestPutClientScope_Serve(t *testing.T) {
20+
t.Parallel()
21+
22+
tests := []struct {
23+
name string
24+
client func(t *testing.T) client.Client
25+
keycloakClient client.ObjectKey
26+
keycloakApiClient func(t *testing.T) *mocks.MockClient
27+
wantErr require.ErrorAssertionFunc
28+
}{
29+
{
30+
name: "with default scopes",
31+
client: func(t *testing.T) client.Client {
32+
s := runtime.NewScheme()
33+
require.NoError(t, keycloakApi.AddToScheme(s))
34+
require.NoError(t, corev1.AddToScheme(s))
35+
36+
return fake.NewClientBuilder().WithScheme(s).WithObjects(
37+
&keycloakApi.KeycloakClient{
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: "test-client",
40+
Namespace: "default",
41+
},
42+
Spec: keycloakApi.KeycloakClientSpec{
43+
ClientId: "test-client-id",
44+
DefaultClientScopes: []string{"default-scope"},
45+
},
46+
}).Build()
47+
},
48+
keycloakClient: client.ObjectKey{
49+
Name: "test-client",
50+
Namespace: "default",
51+
},
52+
keycloakApiClient: func(t *testing.T) *mocks.MockClient {
53+
m := mocks.NewMockClient(t)
54+
55+
m.On("GetClientScopesByNames", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
56+
m.On("AddDefaultScopeToClient", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
57+
58+
return m
59+
},
60+
wantErr: require.NoError,
61+
},
62+
{
63+
name: "with optional scopes",
64+
client: func(t *testing.T) client.Client {
65+
s := runtime.NewScheme()
66+
require.NoError(t, keycloakApi.AddToScheme(s))
67+
require.NoError(t, corev1.AddToScheme(s))
68+
69+
return fake.NewClientBuilder().WithScheme(s).WithObjects(
70+
&keycloakApi.KeycloakClient{
71+
ObjectMeta: metav1.ObjectMeta{
72+
Name: "test-client",
73+
Namespace: "default",
74+
},
75+
Spec: keycloakApi.KeycloakClientSpec{
76+
ClientId: "test-client-id",
77+
OptionalClientScopes: []string{"optional-scope"},
78+
},
79+
}).Build()
80+
},
81+
keycloakClient: client.ObjectKey{
82+
Name: "test-client",
83+
Namespace: "default",
84+
},
85+
keycloakApiClient: func(t *testing.T) *mocks.MockClient {
86+
m := mocks.NewMockClient(t)
87+
88+
m.On("GetClientScopesByNames", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
89+
m.On("AddOptionalScopeToClient", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
90+
91+
return m
92+
},
93+
wantErr: require.NoError,
94+
},
95+
}
96+
97+
for _, tt := range tests {
98+
tt := tt
99+
t.Run(tt.name, func(t *testing.T) {
100+
t.Parallel()
101+
102+
cl := &keycloakApi.KeycloakClient{}
103+
require.NoError(t, tt.client(t).Get(context.Background(), tt.keycloakClient, cl))
104+
105+
el := NewPutClientScope(tt.keycloakApiClient(t))
106+
err := el.Serve(
107+
ctrl.LoggerInto(context.Background(), logr.Discard()),
108+
cl,
109+
"realm",
110+
)
111+
tt.wantErr(t, err)
112+
})
113+
}
114+
}

deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,13 @@ spec:
396396
name:
397397
description: Name is a client name.
398398
type: string
399+
optionalClientScopes:
400+
description: OptionalClientScopes is a list of optional client scopes
401+
assigned to client.
402+
items:
403+
type: string
404+
nullable: true
405+
type: array
399406
protocol:
400407
description: Protocol is a client protocol.
401408
nullable: true

docs/api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,13 @@ KeycloakClientSpec defines the desired state of KeycloakClient.
13011301
Name is a client name.<br/>
13021302
</td>
13031303
<td>false</td>
1304+
</tr><tr>
1305+
<td><b>optionalClientScopes</b></td>
1306+
<td>[]string</td>
1307+
<td>
1308+
OptionalClientScopes is a list of optional client scopes assigned to client.<br/>
1309+
</td>
1310+
<td>false</td>
13041311
</tr><tr>
13051312
<td><b>protocol</b></td>
13061313
<td>string</td>

pkg/client/keycloak/adapter/gocloak.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type GoCloakClients interface {
4242
GetClientScope(ctx context.Context, token, realm, scopeID string) (*gocloak.ClientScope, error)
4343
GetClientsDefaultScopes(ctx context.Context, token, realm, clientID string) ([]*gocloak.ClientScope, error)
4444
AddDefaultScopeToClient(ctx context.Context, token, realm, clientID, scopeID string) error
45+
GetClientsOptionalScopes(ctx context.Context, token, realm, clientID string) ([]*gocloak.ClientScope, error)
46+
AddOptionalScopeToClient(ctx context.Context, token, realm, clientID, scopeID string) error
4547
GetClientScopes(ctx context.Context, token, realm string) ([]*gocloak.ClientScope, error)
4648

4749
GetScopes(ctx context.Context, token, realm, idOfClient string, params gocloak.GetScopeParams) ([]*gocloak.ScopeRepresentation, error)

pkg/client/keycloak/adapter/gocloak_adapter_client.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const defaultMax = 100
1212

1313
func (a GoCloakAdapter) AddDefaultScopeToClient(ctx context.Context, realmName, clientName string, scopes []ClientScope) error {
1414
log := a.log.WithValues("clientName", clientName, logKeyRealm, realmName)
15-
log.Info("Start add Client Scopes to client...")
15+
log.Info("Start add Default Client Scopes to client...")
1616

1717
clientID, err := a.GetClientID(clientName, realmName)
1818
if err != nil {
@@ -43,7 +43,45 @@ func (a GoCloakAdapter) AddDefaultScopeToClient(ctx context.Context, realmName,
4343
}
4444
}
4545

46-
log.Info("End add Client Scopes to client...")
46+
log.Info("End add Default Client Scopes to client...")
47+
48+
return nil
49+
}
50+
51+
func (a GoCloakAdapter) AddOptionalScopeToClient(ctx context.Context, realmName, clientName string, scopes []ClientScope) error {
52+
log := a.log.WithValues("clientName", clientName, logKeyRealm, realmName)
53+
log.Info("Start add Optional Client Scopes to client...")
54+
55+
clientID, err := a.GetClientID(clientName, realmName)
56+
if err != nil {
57+
return errors.Wrap(err, "error during GetClientId")
58+
}
59+
60+
existingScopes, err := a.client.GetClientsOptionalScopes(ctx, a.token.AccessToken, realmName, clientID)
61+
if err != nil {
62+
return errors.Wrap(err, fmt.Sprintf("failed to get existing client scope for client %s", clientName))
63+
}
64+
65+
existingScopesMap := make(map[string]*gocloak.ClientScope)
66+
67+
for _, s := range existingScopes {
68+
if s != nil {
69+
existingScopesMap[*s.ID] = s
70+
}
71+
}
72+
73+
for _, scope := range scopes {
74+
if _, ok := existingScopesMap[scope.ID]; ok {
75+
continue
76+
}
77+
78+
err := a.client.AddOptionalScopeToClient(ctx, a.token.AccessToken, realmName, clientID, scope.ID)
79+
if err != nil {
80+
a.log.Error(err, fmt.Sprintf("failed link scope %s to client %s", scope.Name, clientName))
81+
}
82+
}
83+
84+
log.Info("End add Optional Client Scopes to client...")
4785

4886
return nil
4987
}

0 commit comments

Comments
 (0)