Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
})
})
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
Loading