Skip to content

Commit 1bd9fe6

Browse files
committed
feat: Add Browser and Direct Grant Flow fields to Keycloak Client
Signed-off-by: Douglass Kirkley <doug.kirkley@gmail.com>
1 parent 0ae2f8a commit 1bd9fe6

16 files changed

+354
-77
lines changed

api/v1/keycloakclient_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ type KeycloakClientSpec struct {
181181
// +nullable
182182
// +optional
183183
Authorization *Authorization `json:"authorization,omitempty"`
184+
185+
// AuthenticationFlowBindingOverrides client auth flow overrides
186+
// +optional
187+
AuthenticationFlowBindingOverrides *AuthenticationFlowBindingOverrides `json:"authenticationFlowBindingOverrides,omitempty"`
184188
}
185189

186190
type ServiceAccount struct {
@@ -252,6 +256,11 @@ type Authorization struct {
252256
Resources []Resource `json:"resources,omitempty"`
253257
}
254258

259+
type AuthenticationFlowBindingOverrides struct {
260+
Browser string `json:"browser,omitempty"`
261+
DirectGrant string `json:"directGrant,omitempty"`
262+
}
263+
255264
// KeycloakClientStatus defines the observed state of KeycloakClient.
256265
type KeycloakClientStatus struct {
257266
// +optional

api/v1/zz_generated.deepcopy.go

Lines changed: 16 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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ spec:
6161
description: Attributes is a map of client attributes.
6262
nullable: true
6363
type: object
64+
authenticationFlowBindingOverrides:
65+
description: AuthenticationFlowBindingOverrides client auth flow overrides
66+
properties:
67+
browser:
68+
type: string
69+
directGrant:
70+
type: string
71+
type: object
6472
authorization:
6573
description: Authorization is a client authorization configuration.
6674
nullable: true

config/samples/v1_v1_keycloakclient.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ spec:
1414
webUrl: https://argocd.example.com
1515
defaultClientScopes:
1616
- argocd_groups
17+
authenticationFlowBindingOverrides:
18+
browser: "browser"
19+
directGrant: "direct grant"
1720

1821
---
1922

controllers/keycloakclient/chain/put_client.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const (
2424
passwordLength = 36
2525
passwordDigits = 9
2626
passwordSymbols = 0
27+
28+
browserAuthFlow = "browser"
29+
directGrantAuthFlow = "direct_grant"
2730
)
2831

2932
// secretRef is an interface for getting secret from ref.
@@ -57,7 +60,19 @@ func (el *PutClient) putKeycloakClient(ctx context.Context, keycloakClient *keyc
5760
log := ctrl.LoggerFrom(ctx)
5861
log.Info("Start creation of Keycloak client")
5962

60-
clientDto, err := el.convertCrToDto(ctx, keycloakClient, realmName)
63+
var (
64+
authFlowOverrides map[string]string
65+
err error
66+
)
67+
68+
if keycloakClient.Spec.AuthenticationFlowBindingOverrides != nil {
69+
authFlowOverrides, err = el.getAuthFlows(keycloakClient, realmName)
70+
if err != nil {
71+
return "", fmt.Errorf("unable to get auth flows: %w", err)
72+
}
73+
}
74+
75+
clientDto, err := el.convertCrToDto(ctx, keycloakClient, realmName, authFlowOverrides)
6176
if err != nil {
6277
return "", fmt.Errorf("error during convertCrToDto: %w", err)
6378
}
@@ -93,9 +108,9 @@ func (el *PutClient) putKeycloakClient(ctx context.Context, keycloakClient *keyc
93108
return id, nil
94109
}
95110

96-
func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) (*dto.Client, error) {
111+
func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string, authflowOverrides map[string]string) (*dto.Client, error) {
97112
if keycloakClient.Spec.Public {
98-
res := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName)
113+
res := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName, authflowOverrides)
99114
return res, nil
100115
}
101116

@@ -104,7 +119,7 @@ func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloa
104119
return nil, fmt.Errorf("unable to get secret, err: %w", err)
105120
}
106121

107-
return dto.ConvertSpecToClient(&keycloakClient.Spec, secret, realmName), nil
122+
return dto.ConvertSpecToClient(&keycloakClient.Spec, secret, realmName, authflowOverrides), nil
108123
}
109124

110125
func (el *PutClient) getSecret(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient) (string, error) {
@@ -181,3 +196,37 @@ func (el *PutClient) setSecretRef(ctx context.Context, keycloakClient *keycloakA
181196

182197
return nil
183198
}
199+
200+
func (el *PutClient) getAuthFlows(keycloakClient *keycloakApi.KeycloakClient, realmName string) (map[string]string, error) {
201+
clientAuthFlows := keycloakClient.Spec.AuthenticationFlowBindingOverrides
202+
203+
flows, err := el.keycloakApiClient.GetRealmAuthFlows(realmName)
204+
if err != nil {
205+
return nil, fmt.Errorf("unable to get realm: %w", err)
206+
}
207+
208+
realmAuthFlows := make(map[string]string)
209+
for i := range flows {
210+
realmAuthFlows[flows[i].Alias] = flows[i].ID
211+
}
212+
213+
authFlowOverrides := make(map[string]string)
214+
215+
if clientAuthFlows.Browser != "" {
216+
if _, ok := realmAuthFlows[clientAuthFlows.Browser]; !ok {
217+
return nil, fmt.Errorf("auth flow %s not found in realm %s", clientAuthFlows.Browser, realmName)
218+
}
219+
220+
authFlowOverrides[browserAuthFlow] = realmAuthFlows[clientAuthFlows.Browser]
221+
}
222+
223+
if clientAuthFlows.DirectGrant != "" {
224+
if _, ok := realmAuthFlows[clientAuthFlows.DirectGrant]; !ok {
225+
return nil, fmt.Errorf("auth flow %s not found in realm %s", clientAuthFlows.DirectGrant, realmName)
226+
}
227+
228+
authFlowOverrides[directGrantAuthFlow] = realmAuthFlows[clientAuthFlows.DirectGrant]
229+
}
230+
231+
return authFlowOverrides, nil
232+
}

controllers/keycloakclient/chain/put_client_role.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (el *PutClientRole) putKeycloakClientRole(ctx context.Context, keycloakClie
3131
reqLog := ctrl.LoggerFrom(ctx)
3232
reqLog.Info("Start put keycloak client role")
3333

34-
clientDto := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName)
34+
clientDto := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName, nil)
3535

3636
for _, role := range clientDto.Roles {
3737
exist, err := el.keycloakApiClient.ExistClientRole(clientDto, role)

controllers/keycloakclient/chain/put_client_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,71 @@ func TestPutClient_Serve(t *testing.T) {
310310
},
311311
wantErr: require.NoError,
312312
},
313+
{
314+
name: "create client with auth flows",
315+
fields: fields{
316+
client: func(t *testing.T) client.Client {
317+
s := runtime.NewScheme()
318+
require.NoError(t, keycloakApi.AddToScheme(s))
319+
require.NoError(t, corev1.AddToScheme(s))
320+
321+
return fake.NewClientBuilder().
322+
WithScheme(s).
323+
WithObjects(
324+
&keycloakApi.KeycloakClient{
325+
ObjectMeta: metav1.ObjectMeta{
326+
Name: "test-client",
327+
Namespace: "default",
328+
},
329+
Spec: keycloakApi.KeycloakClientSpec{
330+
ClientId: "test-client-id",
331+
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
332+
Browser: "browser",
333+
DirectGrant: "direct grant",
334+
},
335+
},
336+
},
337+
).
338+
Build()
339+
},
340+
secretRef: func(t *testing.T) secretRef {
341+
return mocks.NewRefClient(t)
342+
},
343+
},
344+
args: args{
345+
keycloakClient: client.ObjectKey{
346+
Name: "test-client",
347+
Namespace: "default",
348+
},
349+
adapterClient: func(t *testing.T) keycloak.Client {
350+
m := keycloakmocks.NewMockClient(t)
351+
352+
m.On("GetClientID", "test-client-id", "realm").
353+
Return("", adapter.NotFoundError("not found")).
354+
Once()
355+
356+
m.On("CreateClient", testifymock.Anything, testifymock.Anything).
357+
Return(nil)
358+
359+
m.On("GetClientID", "test-client-id", "realm").
360+
Return("123", nil)
361+
m.On("GetRealmAuthFlows", "realm").
362+
Return([]adapter.KeycloakAuthFlow{
363+
{
364+
ID: "A39C9C6E-430A-4891-8592-FC195AF2581B",
365+
Alias: "browser",
366+
},
367+
{
368+
ID: "8BF514C6-922C-44A0-8F90-488D1B9DE437",
369+
Alias: "direct grant",
370+
},
371+
}, nil)
372+
373+
return m
374+
},
375+
},
376+
wantErr: require.NoError,
377+
},
313378
}
314379

315380
for _, tt := range tests {

controllers/keycloakclient/chain/put_protocol_mappers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (el *PutProtocolMappers) putProtocolMappers(keycloakClient *keycloakApi.Key
4949
}
5050

5151
if err := el.keycloakApiClient.SyncClientProtocolMapper(
52-
dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName),
52+
dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName, nil),
5353
protocolMappers, keycloakClient.GetReconciliationStrategy() == keycloakApi.ReconciliationStrategyAddOnly); err != nil {
5454
return errors.Wrap(err, "unable to sync protocol mapper")
5555
}

controllers/keycloakclient/keycloakclient_controller_integration_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
6161
Name: "test name",
6262
StandardFlowEnabled: true,
6363
ClientAuthenticatorType: "client-secret",
64+
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
65+
Browser: "browser",
66+
DirectGrant: "direct grant",
67+
},
6468
},
6569
}
6670
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
@@ -101,6 +105,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
101105
Name: KeycloakRealmCR,
102106
Kind: keycloakApi.KeycloakRealmKind,
103107
},
108+
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
109+
Browser: "browser",
110+
DirectGrant: "direct grant",
111+
},
104112
},
105113
}
106114
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
@@ -140,6 +148,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
140148
Kind: keycloakApi.KeycloakRealmKind,
141149
},
142150
Secret: clientSecret.Name,
151+
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
152+
Browser: "browser",
153+
DirectGrant: "direct grant",
154+
},
143155
},
144156
}
145157
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
@@ -208,6 +220,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
208220
ServiceAccount: &keycloakApi.ServiceAccount{
209221
Enabled: true,
210222
},
223+
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
224+
Browser: "browser",
225+
DirectGrant: "direct grant",
226+
},
211227
Authorization: &keycloakApi.Authorization{
212228
Policies: []keycloakApi.Policy{
213229
{

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ spec:
6161
description: Attributes is a map of client attributes.
6262
nullable: true
6363
type: object
64+
authenticationFlowBindingOverrides:
65+
description: AuthenticationFlowBindingOverrides client auth flow overrides
66+
properties:
67+
browser:
68+
type: string
69+
directGrant:
70+
type: string
71+
type: object
6472
authorization:
6573
description: Authorization is a client authorization configuration.
6674
nullable: true

0 commit comments

Comments
 (0)