Skip to content

Commit

Permalink
feat: Add childRequirement for KeycloakAuthFlow (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
zmotso authored and SergK committed Aug 7, 2024
1 parent 7b70159 commit 4817492
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 4 deletions.
4 changes: 4 additions & 0 deletions api/v1/keycloakauthflow_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ type KeycloakAuthFlowSpec struct {
// ChildType is type for auth flow if it has a parent, available options: basic-flow, form-flow
// +optional
ChildType string `json:"childType,omitempty"`

// ChildRequirement is requirement for child execution. Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.
// +optional
ChildRequirement string `json:"childRequirement,omitempty"`
}

// AuthenticationExecution defines keycloak authentication execution.
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/v1.edp.epam.com_keycloakauthflows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ spec:
builtIn:
description: BuiltIn is true if this is built-in auth flow.
type: boolean
childRequirement:
description: 'ChildRequirement is requirement for child execution.
Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.'
type: string
childType:
description: 'ChildType is type for auth flow if it has a parent,
available options: basic-flow, form-flow'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func authFlowSpecToAdapterAuthFlow(spec *keycloakApi.KeycloakAuthFlowSpec) *adap
AuthenticationExecutions: make([]adapter.AuthenticationExecution, 0, len(spec.AuthenticationExecutions)),
ParentName: spec.ParentName,
ChildType: spec.ChildType,
ChildRequirement: spec.ChildRequirement,
}

for _, ae := range spec.AuthenticationExecutions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,56 @@ var _ = Describe("KeycloakAuthFlow controller", Ordered, func() {
g.Expect(createdAuthFlow.Status.Value).Should(ContainSubstring("unable to sync auth flow"))
}).WithTimeout(time.Second * 10).WithPolling(time.Second).Should(Succeed())
})
It("Should create child KeycloakAuthFlow", func() {
By("Creating a parent KeycloakAuthFlow")
parentAuthFlow := &keycloakApi.KeycloakAuthFlow{
ObjectMeta: metav1.ObjectMeta{
Name: "test-auth-flow-parent",
Namespace: ns,
},
Spec: keycloakApi.KeycloakAuthFlowSpec{
RealmRef: common.RealmRef{
Kind: keycloakApi.KeycloakRealmKind,
Name: KeycloakRealmCR,
},
Alias: "test-auth-flow-parent",
Description: "test-auth-flow-parent",
ProviderID: "basic-flow",
TopLevel: true,
},
}
Expect(k8sClient.Create(ctx, parentAuthFlow)).Should(Succeed())
Eventually(func(g Gomega) {
createdParentAuthFlow := &keycloakApi.KeycloakAuthFlow{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: parentAuthFlow.Name, Namespace: ns}, createdParentAuthFlow)
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(createdParentAuthFlow.Status.Value).Should(Equal(helper.StatusOK))
}).WithTimeout(time.Second * 20).WithPolling(time.Second).Should(Succeed())
By("Creating a child KeycloakAuthFlow")
childAuthFlow := &keycloakApi.KeycloakAuthFlow{
ObjectMeta: metav1.ObjectMeta{
Name: "test-auth-flow-child",
Namespace: ns,
},
Spec: keycloakApi.KeycloakAuthFlowSpec{
RealmRef: common.RealmRef{
Kind: keycloakApi.KeycloakRealmKind,
Name: KeycloakRealmCR,
},
Alias: "test-auth-flow-child",
Description: "test-auth-flow-child",
ProviderID: "basic-flow",
ParentName: parentAuthFlow.Name,
ChildType: "basic-flow",
ChildRequirement: "REQUIRED",
},
}
Expect(k8sClient.Create(ctx, childAuthFlow)).Should(Succeed())
Eventually(func(g Gomega) {
createdChildAuthFlow := &keycloakApi.KeycloakAuthFlow{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: childAuthFlow.Name, Namespace: ns}, createdChildAuthFlow)
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(createdChildAuthFlow.Status.Value).Should(Equal(helper.StatusOK))
}).WithTimeout(time.Second * 20).WithPolling(time.Second).Should(Succeed())
})
})
4 changes: 4 additions & 0 deletions deploy-templates/crds/v1.edp.epam.com_keycloakauthflows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ spec:
builtIn:
description: BuiltIn is true if this is built-in auth flow.
type: boolean
childRequirement:
description: 'ChildRequirement is requirement for child execution.
Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.'
type: string
childType:
description: 'ChildType is type for auth flow if it has a parent,
available options: basic-flow, form-flow'
Expand Down
7 changes: 7 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,13 @@ KeycloakAuthFlowSpec defines the desired state of KeycloakAuthFlow.
AuthenticationExecutions is list of authentication executions for this auth flow.<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>childRequirement</b></td>
<td>string</td>
<td>
ChildRequirement is requirement for child execution. Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>childType</b></td>
<td>string</td>
Expand Down
38 changes: 35 additions & 3 deletions pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/pkg/errors"
)

var errAuthFlowNotFound = NotFoundError("auth flow not found")

type KeycloakAuthFlow struct {
ID string `json:"id,omitempty"`
Alias string `json:"alias"`
Expand All @@ -22,6 +24,7 @@ type KeycloakAuthFlow struct {
BuiltIn bool `json:"builtIn"`
ParentName string `json:"-"`
ChildType string `json:"-"`
ChildRequirement string `json:"-"`
AuthenticationExecutions []AuthenticationExecution `json:"-"`
}

Expand Down Expand Up @@ -177,6 +180,20 @@ func (a GoCloakAdapter) syncBaseAuthFlow(realmName string, flow *KeycloakAuthFlo
}
}

if flow.ParentName != "" && flow.ChildRequirement != "" {
exec, err := a.getFlowExecution(realmName, flow)
if err != nil {
return "", err
}

// We cant set child flow requirement during creation, so we need to update it.
exec.Requirement = flow.ChildRequirement

if err := a.updateFlowExecution(realmName, flow.ParentName, exec); err != nil {
return "", fmt.Errorf("unable to update flow execution requirement: %w", err)
}
}

if err := a.validateChildFlowsCreated(realmName, flow); err != nil {
return "", errors.Wrap(err, "child flows validation failed")
}
Expand Down Expand Up @@ -269,7 +286,7 @@ func (a GoCloakAdapter) getFlowExecutionID(realmName string, flow *KeycloakAuthF
}
}

return "", NotFoundError("auth flow not found")
return "", errAuthFlowNotFound
}

func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow) (string, error) {
Expand All @@ -285,7 +302,7 @@ func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow)
}
}

return "", NotFoundError("auth flow not found")
return "", errAuthFlowNotFound
}

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

return "", NotFoundError("auth flow not found")
return "", errAuthFlowNotFound
}

func (a GoCloakAdapter) getFlowExecution(realmName string, flow *KeycloakAuthFlow) (*FlowExecution, error) {
execs, err := a.getFlowExecutions(realmName, flow.ParentName)
if err != nil {
return nil, fmt.Errorf("unable to get auth flow executions: %w", err)
}

for i := range execs {
if execs[i].DisplayName == flow.Alias {
return &execs[i], nil
}
}

return nil, errAuthFlowNotFound
}

func (a GoCloakAdapter) getRealmAuthFlows(realmName string) ([]KeycloakAuthFlow, error) {
Expand Down
130 changes: 129 additions & 1 deletion pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func (e *ExecFlowTestSuite) TestGetAuthFlowID() {
id, err := e.adapter.getAuthFlowID(e.realmName, &flow)

assert.NoError(e.T(), err)
assert.Equal(e.T(), id, flowID)
assert.Equal(e.T(), flowID, id)
}

func (e *ExecFlowTestSuite) TestSetRealmBrowserFlow() {
Expand Down Expand Up @@ -309,6 +309,134 @@ func (e *ExecFlowTestSuite) TestSyncBaseAuthFlow() {
assert.EqualError(e.T(), err, "child flows validation failed: not all child flows created")
}

func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowShouldUpdateChildFlowRequirement() {
flow := KeycloakAuthFlow{
Alias: "flow1",
ParentName: "parent",
ChildRequirement: "REQUIRED",
}
flowID := "flow-id-1"

httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(
http.StatusOK,
[]FlowExecution{{
DisplayName: flow.Alias,
FlowID: flowID,
}},
),
)

httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.Alias),
httpmock.NewJsonResponderOrPanic(http.StatusOK, []FlowExecution{}),
)

httpmock.RegisterResponder(
http.MethodPut,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(http.StatusOK, map[string]string{}),
)

_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)

assert.NoError(e.T(), err)
}

func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowFailedUpdateChildFlowRequirement() {
flow := KeycloakAuthFlow{
Alias: "flow1",
ParentName: "parent",
ChildRequirement: "REQUIRED",
}
flowID := "flow-id-1"

httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(
http.StatusOK,
[]FlowExecution{{
DisplayName: flow.Alias,
FlowID: flowID,
}},
),
)

httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.Alias),
httpmock.NewJsonResponderOrPanic(http.StatusOK, []FlowExecution{}),
)

httpmock.RegisterResponder(
http.MethodPut,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, map[string]string{}),
)

_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)

require.Error(e.T(), err)
assert.Contains(e.T(), err.Error(), "unable to update flow execution requirement")
}

func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowFailedToGetFlowExecution() {
flow := KeycloakAuthFlow{
Alias: "flow1",
ParentName: "parent",
ChildRequirement: "REQUIRED",
}

httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(
http.StatusInternalServerError,
[]FlowExecution{},
),
)

_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)

require.Error(e.T(), err)
assert.Contains(e.T(), err.Error(), "unable to get auth flow")
}

func (e *ExecFlowTestSuite) TestSyncBaseAuthFlowFailedToCreateChildFlow() {
flow := KeycloakAuthFlow{
Alias: "flow1",
ParentName: "parent",
ChildRequirement: "REQUIRED",
}

httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(
http.StatusOK,
[]FlowExecution{},
),
)

httpmock.RegisterResponder(
http.MethodPost,
fmt.Sprintf("/admin/realms/%s/authentication/flows/%s/executions/flow", e.realmName, flow.ParentName),
httpmock.NewJsonResponderOrPanic(
http.StatusInternalServerError,
map[string]string{},
),
)

_, err := e.adapter.syncBaseAuthFlow(e.realmName, &flow)

require.Error(e.T(), err)
assert.Contains(e.T(), err.Error(), "unable to create child auth flow in realm")
}

func (e *ExecFlowTestSuite) TestGetFlowExecutionID() {
flow := KeycloakAuthFlow{ParentName: "parent", Alias: "fff"}
_, err := e.adapter.getFlowExecutionID(e.realmName, &flow)
Expand Down

0 comments on commit 4817492

Please sign in to comment.