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)