diff --git a/api/v1/keycloakauthflow_types.go b/api/v1/keycloakauthflow_types.go
index 424123fb..7f0b8cd3 100644
--- a/api/v1/keycloakauthflow_types.go
+++ b/api/v1/keycloakauthflow_types.go
@@ -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.
diff --git a/config/crd/bases/v1.edp.epam.com_keycloakauthflows.yaml b/config/crd/bases/v1.edp.epam.com_keycloakauthflows.yaml
index c9457a3c..7ac30a14 100644
--- a/config/crd/bases/v1.edp.epam.com_keycloakauthflows.yaml
+++ b/config/crd/bases/v1.edp.epam.com_keycloakauthflows.yaml
@@ -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'
diff --git a/controllers/keycloakauthflow/keycloakauthflow_controller.go b/controllers/keycloakauthflow/keycloakauthflow_controller.go
index 59a05246..c4af98ef 100644
--- a/controllers/keycloakauthflow/keycloakauthflow_controller.go
+++ b/controllers/keycloakauthflow/keycloakauthflow_controller.go
@@ -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 {
diff --git a/controllers/keycloakauthflow/keycloakrauthflow_controller_integration_test.go b/controllers/keycloakauthflow/keycloakrauthflow_controller_integration_test.go
index d21ceb86..223ab00e 100644
--- a/controllers/keycloakauthflow/keycloakrauthflow_controller_integration_test.go
+++ b/controllers/keycloakauthflow/keycloakrauthflow_controller_integration_test.go
@@ -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())
+ })
})
diff --git a/deploy-templates/crds/v1.edp.epam.com_keycloakauthflows.yaml b/deploy-templates/crds/v1.edp.epam.com_keycloakauthflows.yaml
index c9457a3c..7ac30a14 100644
--- a/deploy-templates/crds/v1.edp.epam.com_keycloakauthflows.yaml
+++ b/deploy-templates/crds/v1.edp.epam.com_keycloakauthflows.yaml
@@ -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'
diff --git a/docs/api.md b/docs/api.md
index 8f77e3c0..f38069dc 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -890,6 +890,13 @@ KeycloakAuthFlowSpec defines the desired state of KeycloakAuthFlow.
AuthenticationExecutions is list of authentication executions for this auth flow.
false |
+
+ childRequirement |
+ string |
+
+ ChildRequirement is requirement for child execution. Available options: REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL.
+ |
+ false |
childType |
string |
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go b/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go
index 8950b98e..e2e52db7 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go
@@ -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"`
@@ -22,6 +24,7 @@ type KeycloakAuthFlow struct {
BuiltIn bool `json:"builtIn"`
ParentName string `json:"-"`
ChildType string `json:"-"`
+ ChildRequirement string `json:"-"`
AuthenticationExecutions []AuthenticationExecution `json:"-"`
}
@@ -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")
}
@@ -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) {
@@ -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)
@@ -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) {
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go b/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go
index f60cc5a1..75e888a1 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go
@@ -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() {
@@ -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)