Skip to content

Commit 047a54b

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 047a54b

File tree

13 files changed

+331
-70
lines changed

13 files changed

+331
-70
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+
direct_grant: "direct grant"
1720

1821
---
1922

controllers/keycloakclient/chain/put_client.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ func (el *PutClient) putKeycloakClient(ctx context.Context, keycloakClient *keyc
9494
}
9595

9696
func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) (*dto.Client, error) {
97+
authFlowOverrides := keycloakClient.Spec.AuthenticationFlowBindingOverrides
98+
if authFlowOverrides.Browser != "" || authFlowOverrides.DirectGrant != "" {
99+
authFlows, err := el.getAuthFlows(keycloakClient, realmName)
100+
if err != nil {
101+
return nil, fmt.Errorf("unable to get auth flows: %w", err)
102+
}
103+
104+
keycloakClient.Spec.AuthenticationFlowBindingOverrides = *authFlows
105+
}
106+
97107
if keycloakClient.Spec.Public {
98108
res := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName)
99109
return res, nil
@@ -181,3 +191,35 @@ func (el *PutClient) setSecretRef(ctx context.Context, keycloakClient *keycloakA
181191

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

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 {

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

docs/api.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,13 @@ If empty - WebUrl will be used.<br/>
19541954
<i>Default</i>: map[post.logout.redirect.uris:+]<br/>
19551955
</td>
19561956
<td>false</td>
1957+
</tr><tr>
1958+
<td><b><a href="#keycloakclientspecauthenticationflowbindingoverrides">authenticationFlowBindingOverrides</a></b></td>
1959+
<td>object</td>
1960+
<td>
1961+
AuthenticationFlowBindingOverrides client auth flow overrides<br/>
1962+
</td>
1963+
<td>false</td>
19571964
</tr><tr>
19581965
<td><b><a href="#keycloakclientspecauthorization">authorization</a></b></td>
19591966
<td>object</td>
@@ -2190,6 +2197,40 @@ If not specified, the value from `WebUrl` is used<br/>
21902197
</table>
21912198

21922199

2200+
### KeycloakClient.spec.authenticationFlowBindingOverrides
2201+
<sup><sup>[↩ Parent](#keycloakclientspec)</sup></sup>
2202+
2203+
2204+
2205+
AuthenticationFlowBindingOverrides client auth flow overrides
2206+
2207+
<table>
2208+
<thead>
2209+
<tr>
2210+
<th>Name</th>
2211+
<th>Type</th>
2212+
<th>Description</th>
2213+
<th>Required</th>
2214+
</tr>
2215+
</thead>
2216+
<tbody><tr>
2217+
<td><b>browser</b></td>
2218+
<td>string</td>
2219+
<td>
2220+
<br/>
2221+
</td>
2222+
<td>false</td>
2223+
</tr><tr>
2224+
<td><b>directGrant</b></td>
2225+
<td>string</td>
2226+
<td>
2227+
<br/>
2228+
</td>
2229+
<td>false</td>
2230+
</tr></tbody>
2231+
</table>
2232+
2233+
21932234
### KeycloakClient.spec.authorization
21942235
<sup><sup>[↩ Parent](#keycloakclientspec)</sup></sup>
21952236

pkg/client/keycloak/adapter/gocloak_adapter.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -524,15 +524,16 @@ func getGclCln(client *dto.Client) gocloak.Client {
524524
RedirectURIs: &[]string{
525525
client.WebUrl + "/*",
526526
},
527-
RegistrationAccessToken: &client.RegistrationAccessToken,
528-
RootURL: &client.WebUrl,
529-
AdminURL: &client.AdminUrl,
530-
BaseURL: &client.HomeUrl,
531-
Secret: &client.ClientSecret,
532-
ServiceAccountsEnabled: &client.ServiceAccountEnabled,
533-
StandardFlowEnabled: &client.StandardFlowEnabled,
534-
SurrogateAuthRequired: &client.SurrogateAuthRequired,
535-
WebOrigins: &client.WebOrigins,
527+
RegistrationAccessToken: &client.RegistrationAccessToken,
528+
RootURL: &client.WebUrl,
529+
AdminURL: &client.AdminUrl,
530+
BaseURL: &client.HomeUrl,
531+
Secret: &client.ClientSecret,
532+
ServiceAccountsEnabled: &client.ServiceAccountEnabled,
533+
StandardFlowEnabled: &client.StandardFlowEnabled,
534+
SurrogateAuthRequired: &client.SurrogateAuthRequired,
535+
WebOrigins: &client.WebOrigins,
536+
AuthenticationFlowBindingOverrides: &client.AuthenticationFlowBindingOverrides,
536537
}
537538

538539
// Set the admin URL to the web URL for backwards compatibility.

pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow)
305305
return "", errAuthFlowNotFound
306306
}
307307

308-
flows, err := a.getRealmAuthFlows(realmName)
308+
flows, err := a.GetRealmAuthFlows(realmName)
309309
if err != nil {
310310
return "", errors.Wrap(err, "unable to get realm auth flows")
311311
}
@@ -334,7 +334,7 @@ func (a GoCloakAdapter) getFlowExecution(realmName string, flow *KeycloakAuthFlo
334334
return nil, errAuthFlowNotFound
335335
}
336336

337-
func (a GoCloakAdapter) getRealmAuthFlows(realmName string) ([]KeycloakAuthFlow, error) {
337+
func (a GoCloakAdapter) GetRealmAuthFlows(realmName string) ([]KeycloakAuthFlow, error) {
338338
var flows []KeycloakAuthFlow
339339

340340
resp, err := a.startRestyRequest().
@@ -540,7 +540,7 @@ func (a GoCloakAdapter) unsetBrowserFlow(realmName, flowAlias string) (realm *go
540540
return realm, false, nil
541541
}
542542

543-
authFlows, err := a.getRealmAuthFlows(realmName)
543+
authFlows, err := a.GetRealmAuthFlows(realmName)
544544
if err != nil {
545545
return nil, false, errors.Wrapf(err, "unable to get auth flows for realm: %s", realmName)
546546
}

0 commit comments

Comments
 (0)