Skip to content

Commit a1d7591

Browse files
committed
feat: Add childRequirement for KeycloakAuthFlow (#82)
1 parent 7b70159 commit a1d7591

File tree

8 files changed

+205
-4
lines changed

8 files changed

+205
-4
lines changed

api/v1/keycloakauthflow_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ type KeycloakAuthFlowSpec struct {
4545
// ChildType is type for auth flow if it has a parent, available options: basic-flow, form-flow
4646
// +optional
4747
ChildType string `json:"childType,omitempty"`
48+
49+
// ChildRequirement is requirement for child execution. Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.
50+
// +optional
51+
ChildRequirement string `json:"childRequirement,omitempty"`
4852
}
4953

5054
// AuthenticationExecution defines keycloak authentication execution.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ spec:
9191
builtIn:
9292
description: BuiltIn is true if this is built-in auth flow.
9393
type: boolean
94+
childRequirement:
95+
description: 'ChildRequirement is requirement for child execution.
96+
Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.'
97+
type: string
9498
childType:
9599
description: 'ChildType is type for auth flow if it has a parent,
96100
available options: basic-flow, form-flow'

controllers/keycloakauthflow/keycloakauthflow_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ func authFlowSpecToAdapterAuthFlow(spec *keycloakApi.KeycloakAuthFlowSpec) *adap
198198
AuthenticationExecutions: make([]adapter.AuthenticationExecution, 0, len(spec.AuthenticationExecutions)),
199199
ParentName: spec.ParentName,
200200
ChildType: spec.ChildType,
201+
ChildRequirement: spec.ChildRequirement,
201202
}
202203

203204
for _, ae := range spec.AuthenticationExecutions {

controllers/keycloakauthflow/keycloakrauthflow_controller_integration_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,56 @@ var _ = Describe("KeycloakAuthFlow controller", Ordered, func() {
125125
g.Expect(createdAuthFlow.Status.Value).Should(ContainSubstring("unable to sync auth flow"))
126126
}).WithTimeout(time.Second * 10).WithPolling(time.Second).Should(Succeed())
127127
})
128+
It("Should create child KeycloakAuthFlow", func() {
129+
By("Creating a parent KeycloakAuthFlow")
130+
parentAuthFlow := &keycloakApi.KeycloakAuthFlow{
131+
ObjectMeta: metav1.ObjectMeta{
132+
Name: "test-auth-flow-parent",
133+
Namespace: ns,
134+
},
135+
Spec: keycloakApi.KeycloakAuthFlowSpec{
136+
RealmRef: common.RealmRef{
137+
Kind: keycloakApi.KeycloakRealmKind,
138+
Name: KeycloakRealmCR,
139+
},
140+
Alias: "test-auth-flow-parent",
141+
Description: "test-auth-flow-parent",
142+
ProviderID: "basic-flow",
143+
TopLevel: true,
144+
},
145+
}
146+
Expect(k8sClient.Create(ctx, parentAuthFlow)).Should(Succeed())
147+
Eventually(func(g Gomega) {
148+
createdParentAuthFlow := &keycloakApi.KeycloakAuthFlow{}
149+
err := k8sClient.Get(ctx, types.NamespacedName{Name: parentAuthFlow.Name, Namespace: ns}, createdParentAuthFlow)
150+
g.Expect(err).ShouldNot(HaveOccurred())
151+
g.Expect(createdParentAuthFlow.Status.Value).Should(Equal(helper.StatusOK))
152+
}).WithTimeout(time.Second * 20).WithPolling(time.Second).Should(Succeed())
153+
By("Creating a child KeycloakAuthFlow")
154+
childAuthFlow := &keycloakApi.KeycloakAuthFlow{
155+
ObjectMeta: metav1.ObjectMeta{
156+
Name: "test-auth-flow-child",
157+
Namespace: ns,
158+
},
159+
Spec: keycloakApi.KeycloakAuthFlowSpec{
160+
RealmRef: common.RealmRef{
161+
Kind: keycloakApi.KeycloakRealmKind,
162+
Name: KeycloakRealmCR,
163+
},
164+
Alias: "test-auth-flow-child",
165+
Description: "test-auth-flow-child",
166+
ProviderID: "basic-flow",
167+
ParentName: parentAuthFlow.Name,
168+
ChildType: "basic-flow",
169+
ChildRequirement: "REQUIRED",
170+
},
171+
}
172+
Expect(k8sClient.Create(ctx, childAuthFlow)).Should(Succeed())
173+
Eventually(func(g Gomega) {
174+
createdChildAuthFlow := &keycloakApi.KeycloakAuthFlow{}
175+
err := k8sClient.Get(ctx, types.NamespacedName{Name: childAuthFlow.Name, Namespace: ns}, createdChildAuthFlow)
176+
g.Expect(err).ShouldNot(HaveOccurred())
177+
g.Expect(createdChildAuthFlow.Status.Value).Should(Equal(helper.StatusOK))
178+
}).WithTimeout(time.Second * 20).WithPolling(time.Second).Should(Succeed())
179+
})
128180
})

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ spec:
9191
builtIn:
9292
description: BuiltIn is true if this is built-in auth flow.
9393
type: boolean
94+
childRequirement:
95+
description: 'ChildRequirement is requirement for child execution.
96+
Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.'
97+
type: string
9498
childType:
9599
description: 'ChildType is type for auth flow if it has a parent,
96100
available options: basic-flow, form-flow'

docs/api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,13 @@ KeycloakAuthFlowSpec defines the desired state of KeycloakAuthFlow.
890890
AuthenticationExecutions is list of authentication executions for this auth flow.<br/>
891891
</td>
892892
<td>false</td>
893+
</tr><tr>
894+
<td><b>childRequirement</b></td>
895+
<td>string</td>
896+
<td>
897+
ChildRequirement is requirement for child execution. Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.<br/>
898+
</td>
899+
<td>false</td>
893900
</tr><tr>
894901
<td><b>childType</b></td>
895902
<td>string</td>

pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/pkg/errors"
1414
)
1515

16+
var errAuthFlowNotFound = NotFoundError("auth flow not found")
17+
1618
type KeycloakAuthFlow struct {
1719
ID string `json:"id,omitempty"`
1820
Alias string `json:"alias"`
@@ -22,6 +24,7 @@ type KeycloakAuthFlow struct {
2224
BuiltIn bool `json:"builtIn"`
2325
ParentName string `json:"-"`
2426
ChildType string `json:"-"`
27+
ChildRequirement string `json:"-"`
2528
AuthenticationExecutions []AuthenticationExecution `json:"-"`
2629
}
2730

@@ -177,6 +180,20 @@ func (a GoCloakAdapter) syncBaseAuthFlow(realmName string, flow *KeycloakAuthFlo
177180
}
178181
}
179182

183+
if flow.ParentName != "" && flow.ChildRequirement != "" {
184+
exec, err := a.getFlowExecution(realmName, flow)
185+
if err != nil {
186+
return "", err
187+
}
188+
189+
// We cant set child flow requirement during creation, so we need to update it.
190+
exec.Requirement = flow.ChildRequirement
191+
192+
if err := a.updateFlowExecution(realmName, flow.ParentName, exec); err != nil {
193+
return "", fmt.Errorf("unable to update flow execution requirement: %w", err)
194+
}
195+
}
196+
180197
if err := a.validateChildFlowsCreated(realmName, flow); err != nil {
181198
return "", errors.Wrap(err, "child flows validation failed")
182199
}
@@ -269,7 +286,7 @@ func (a GoCloakAdapter) getFlowExecutionID(realmName string, flow *KeycloakAuthF
269286
}
270287
}
271288

272-
return "", NotFoundError("auth flow not found")
289+
return "", errAuthFlowNotFound
273290
}
274291

275292
func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow) (string, error) {
@@ -285,7 +302,7 @@ func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow)
285302
}
286303
}
287304

288-
return "", NotFoundError("auth flow not found")
305+
return "", errAuthFlowNotFound
289306
}
290307

291308
flows, err := a.getRealmAuthFlows(realmName)
@@ -299,7 +316,22 @@ func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow)
299316
}
300317
}
301318

302-
return "", NotFoundError("auth flow not found")
319+
return "", errAuthFlowNotFound
320+
}
321+
322+
func (a GoCloakAdapter) getFlowExecution(realmName string, flow *KeycloakAuthFlow) (*FlowExecution, error) {
323+
execs, err := a.getFlowExecutions(realmName, flow.ParentName)
324+
if err != nil {
325+
return nil, fmt.Errorf("unable to get auth flow executions: %w", err)
326+
}
327+
328+
for i := range execs {
329+
if execs[i].DisplayName == flow.Alias {
330+
return &execs[i], nil
331+
}
332+
}
333+
334+
return nil, errAuthFlowNotFound
303335
}
304336

305337
func (a GoCloakAdapter) getRealmAuthFlows(realmName string) ([]KeycloakAuthFlow, error) {

pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func (e *ExecFlowTestSuite) TestGetAuthFlowID() {
233233
id, err := e.adapter.getAuthFlowID(e.realmName, &flow)
234234

235235
assert.NoError(e.T(), err)
236-
assert.Equal(e.T(), id, flowID)
236+
assert.Equal(e.T(), flowID, id)
237237
}
238238

239239
func (e *ExecFlowTestSuite) TestSetRealmBrowserFlow() {
@@ -309,6 +309,103 @@ func (e *ExecFlowTestSuite) TestSyncBaseAuthFlow() {
309309
assert.EqualError(e.T(), err, "child flows validation failed: not all child flows created")
310310
}
311311

312+
func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowShouldUpdateChildFlowRequirement() {
313+
flow := KeycloakAuthFlow{
314+
Alias: "flow1",
315+
ParentName: "parent",
316+
ChildRequirement: "REQUIRED",
317+
}
318+
flowID := "flow-id-1"
319+
320+
httpmock.RegisterResponder(
321+
http.MethodGet,
322+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
323+
httpmock.NewJsonResponderOrPanic(
324+
http.StatusOK,
325+
[]FlowExecution{{
326+
DisplayName: flow.Alias,
327+
FlowID: flowID,
328+
}},
329+
),
330+
)
331+
332+
httpmock.RegisterResponder(
333+
http.MethodGet,
334+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.Alias),
335+
httpmock.NewJsonResponderOrPanic(http.StatusOK, []FlowExecution{}),
336+
)
337+
338+
httpmock.RegisterResponder(
339+
http.MethodPut,
340+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
341+
httpmock.NewJsonResponderOrPanic(http.StatusOK, map[string]string{}),
342+
)
343+
344+
_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)
345+
346+
assert.NoError(e.T(), err)
347+
}
348+
349+
func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowFailedUpdateChildFlowRequirement() {
350+
flow := KeycloakAuthFlow{
351+
Alias: "flow1",
352+
ParentName: "parent",
353+
ChildRequirement: "REQUIRED",
354+
}
355+
flowID := "flow-id-1"
356+
357+
httpmock.RegisterResponder(
358+
http.MethodGet,
359+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
360+
httpmock.NewJsonResponderOrPanic(
361+
http.StatusOK,
362+
[]FlowExecution{{
363+
DisplayName: flow.Alias,
364+
FlowID: flowID,
365+
}},
366+
),
367+
)
368+
369+
httpmock.RegisterResponder(
370+
http.MethodGet,
371+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.Alias),
372+
httpmock.NewJsonResponderOrPanic(http.StatusOK, []FlowExecution{}),
373+
)
374+
375+
httpmock.RegisterResponder(
376+
http.MethodPut,
377+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
378+
httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, map[string]string{}),
379+
)
380+
381+
_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)
382+
383+
assert.Error(e.T(), err)
384+
assert.Contains(e.T(), err.Error(), "unable to update flow execution requirement")
385+
}
386+
387+
func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowFailedToGetFlowExecution() {
388+
flow := KeycloakAuthFlow{
389+
Alias: "flow1",
390+
ParentName: "parent",
391+
ChildRequirement: "REQUIRED",
392+
}
393+
394+
httpmock.RegisterResponder(
395+
http.MethodGet,
396+
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
397+
httpmock.NewJsonResponderOrPanic(
398+
http.StatusInternalServerError,
399+
[]FlowExecution{},
400+
),
401+
)
402+
403+
_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)
404+
405+
assert.Error(e.T(), err)
406+
assert.Contains(e.T(), err.Error(), "unable to get auth flow")
407+
}
408+
312409
func (e *ExecFlowTestSuite) TestGetFlowExecutionID() {
313410
flow := KeycloakAuthFlow{ParentName: "parent", Alias: "fff"}
314411
_, err := e.adapter.getFlowExecutionID(e.realmName, &flow)

0 commit comments

Comments
 (0)