diff --git a/example/main.tf b/example/main.tf index 0284e68b4..9101763eb 100644 --- a/example/main.tf +++ b/example/main.tf @@ -643,3 +643,55 @@ resource "keycloak_openid_client_service_account_role" "read_token" { service_account_user_id = "${keycloak_openid_client.test_client_auth.service_account_user_id}" role = "read-token" } + +resource "keycloak_authentication_flow" "browser-copy-flow" { + alias = "browserCopyFlow" + realm_id = "${keycloak_realm.test.id}" + description = "browser based authentication" +} + +resource "keycloak_authentication_execution" "browser-copy-cookie" { + realm_id = "${keycloak_realm.test.id}" + parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}" + authenticator = "auth-cookie" + requirement = "ALTERNATIVE" + depends_on = ["keycloak_authentication_execution.browser-copy-kerberos"] +} + +resource "keycloak_authentication_execution" "browser-copy-kerberos" { + realm_id = "${keycloak_realm.test.id}" + parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}" + authenticator = "auth-spnego" + requirement = "DISABLED" +} + +resource "keycloak_authentication_execution" "browser-copy-idp-redirect" { + realm_id = "${keycloak_realm.test.id}" + parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}" + authenticator = "identity-provider-redirector" + requirement = "ALTERNATIVE" + depends_on = ["keycloak_authentication_execution.browser-copy-cookie"] +} + +resource "keycloak_authentication_subflow" "browser-copy-flow-forms" { + realm_id = "${keycloak_realm.test.id}" + parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}" + alias = "browser-copy-flow-forms" + requirement = "ALTERNATIVE" + depends_on = ["keycloak_authentication_execution.browser-copy-idp-redirect"] +} + +resource "keycloak_authentication_execution" "browser-copy-auth-username-password-form" { + realm_id = "${keycloak_realm.test.id}" + parent_flow_alias = "${keycloak_authentication_subflow.browser-copy-flow-forms.alias}" + authenticator = "auth-username-password-form" + requirement = "REQUIRED" +} + +resource "keycloak_authentication_execution" "browser-copy-otp" { + realm_id = "${keycloak_realm.test.id}" + parent_flow_alias = "${keycloak_authentication_subflow.browser-copy-flow-forms.alias}" + authenticator = "auth-otp-form" + requirement = "REQUIRED" + depends_on = ["keycloak_authentication_execution.browser-copy-auth-username-password-form"] +} diff --git a/keycloak/authentication_execution.go b/keycloak/authentication_execution.go new file mode 100644 index 000000000..b8b2d6b7a --- /dev/null +++ b/keycloak/authentication_execution.go @@ -0,0 +1,144 @@ +package keycloak + +import ( + "fmt" +) + +// this is only used when creating an execution on a flow. +// other fields can be provided to the API but they are ignored +// POST /realms/${realmId}/authentication/flows/${flowAlias}/executions/execution +type authenticationExecutionCreate struct { + Provider string `json:"provider"` //authenticator of the execution +} + +type authenticationExecutionRequirementUpdate struct { + RealmId string `json:"-"` + ParentFlowAlias string `json:"-"` + Id string `json:"id"` + Requirement string `json:"requirement"` +} + +// this type is returned by GET /realms/${realmId}/authentication/flows/${flowAlias}/executions +type AuthenticationExecution struct { + Id string `json:"id"` + RealmId string `json:"-"` + ParentFlowAlias string `json:"-"` + Authenticator string `json:"authenticator"` //can be any authenticator from GET realms/{realm}/authentication/authenticator-providers OR GET realms/{realm}/authentication/client-authenticator-providers OR GET realms/{realm}/authentication/form-action-providers + AuthenticationConfig string `json:"authenticationConfig"` + AuthenticationFlow bool `json:"authenticationFlow"` + FlowId string `json:"flowId"` + ParentFlowId string `json:"parentFlow"` + Priority int `json:"priority"` + Requirement string `json:"requirement"` +} + +// another model is used for GET /realms/${realmId}/authentication/executions/${executionId}, but I am going to try to avoid using this API +type AuthenticationExecutionInfo struct { + Id string `json:"id"` + RealmId string `json:"-"` + ParentFlowAlias string `json:"-"` + Alias string `json:"alias"` + AuthenticationConfig string `json:"authenticationConfig"` + AuthenticationFlow bool `json:"authenticationFlow"` + Configurable bool `json:"configurable"` + FlowId string `json:"flowId"` + Index int `json:"index"` + Level int `json:"level"` + ProviderId string `json:"providerId"` + Requirement string `json:"requirement"` +} + +type AuthenticationExecutionList []*AuthenticationExecutionInfo + +func (list AuthenticationExecutionList) Len() int { + return len(list) +} + +func (list AuthenticationExecutionList) Less(i, j int) bool { + return list[i].Index < list[j].Index +} + +func (list AuthenticationExecutionList) Swap(i, j int) { + list[i], list[j] = list[j], list[i] +} + +func (keycloakClient *KeycloakClient) ListAuthenticationExecutions(realmId, parentFlowAlias string) (AuthenticationExecutionList, error) { + var authenticationExecutions []*AuthenticationExecutionInfo + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", realmId, parentFlowAlias), &authenticationExecutions, nil) + if err != nil { + return nil, err + } + + return authenticationExecutions, err +} + +func (keycloakClient *KeycloakClient) NewAuthenticationExecution(execution *AuthenticationExecution) error { + _, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions/execution", execution.RealmId, execution.ParentFlowAlias), &authenticationExecutionCreate{Provider: execution.Authenticator}) + if err != nil { + return err + } + + execution.Id = getIdFromLocationHeader(location) + + err = keycloakClient.UpdateAuthenticationExecution(execution) + if err != nil { + return err + } + + return nil +} + +func (keycloakClient *KeycloakClient) GetAuthenticationExecution(realmId, parentFlowAlias, id string) (*AuthenticationExecution, error) { + var authenticationExecution AuthenticationExecution + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/executions/%s", realmId, id), &authenticationExecution, nil) + if err != nil { + return nil, err + } + + authenticationExecution.RealmId = realmId + authenticationExecution.ParentFlowAlias = parentFlowAlias + + return &authenticationExecution, nil +} + +func (keycloakClient *KeycloakClient) UpdateAuthenticationExecution(execution *AuthenticationExecution) error { + authenticationExecutionUpdateRequirement := &authenticationExecutionRequirementUpdate{ + RealmId: execution.RealmId, + ParentFlowAlias: execution.ParentFlowAlias, + Id: execution.Id, + Requirement: execution.Requirement, + } + return keycloakClient.UpdateAuthenticationExecutionRequirement(authenticationExecutionUpdateRequirement) +} + +func (keycloakClient *KeycloakClient) UpdateAuthenticationExecutionRequirement(executionRequirementUpdate *authenticationExecutionRequirementUpdate) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", executionRequirementUpdate.RealmId, executionRequirementUpdate.ParentFlowAlias), executionRequirementUpdate) +} + +func (keycloakClient *KeycloakClient) DeleteAuthenticationExecution(realmId, id string) error { + err := keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/executions/%s", realmId, id), nil) + if err != nil { + // For whatever reason, this fails sometimes with a 500 during acceptance tests. try again + return keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/executions/%s", realmId, id), nil) + } + + return nil +} + +func (keycloakClient *KeycloakClient) RaiseAuthenticationExecutionPriority(realmId, id string) error { + _, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/executions/%s/raise-priority", realmId, id), nil) + if err != nil { + return err + } + return nil +} + +func (keycloakClient *KeycloakClient) LowerAuthenticationExecutionPriority(realmId, id string) error { + _, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/executions/%s/lower-priority", realmId, id), nil) + if err != nil { + return err + } + return nil +} diff --git a/keycloak/authentication_flow.go b/keycloak/authentication_flow.go new file mode 100644 index 000000000..8ce3de38c --- /dev/null +++ b/keycloak/authentication_flow.go @@ -0,0 +1,55 @@ +package keycloak + +import ( + "fmt" +) + +type AuthenticationFlow struct { + Id string `json:"id,omitempty"` + RealmId string `json:"-"` + Alias string `json:"alias"` + Description string `json:"description"` + ProviderId string `json:"providerId"` // "basic-flow" or "client-flow" + TopLevel bool `json:"topLevel"` + BuiltIn bool `json:"builtIn"` +} + +func (keycloakClient *KeycloakClient) NewAuthenticationFlow(authenticationFlow *AuthenticationFlow) error { + authenticationFlow.TopLevel = true + authenticationFlow.BuiltIn = false + + _, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows", authenticationFlow.RealmId), authenticationFlow) + if err != nil { + return err + } + authenticationFlow.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) GetAuthenticationFlow(realmId, id string) (*AuthenticationFlow, error) { + var authenticationFlow AuthenticationFlow + err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), &authenticationFlow, nil) + if err != nil { + return nil, err + } + + authenticationFlow.RealmId = realmId + return &authenticationFlow, nil +} + +func (keycloakClient *KeycloakClient) UpdateAuthenticationFlow(authenticationFlow *AuthenticationFlow) error { + authenticationFlow.TopLevel = true + authenticationFlow.BuiltIn = false + + return keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/flows/%s", authenticationFlow.RealmId, authenticationFlow.Id), authenticationFlow) +} + +func (keycloakClient *KeycloakClient) DeleteAuthenticationFlow(realmId, id string) error { + err := keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), nil) + if err != nil { + // For whatever reason, this fails sometimes with a 500 during acceptance tests. try again + return keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), nil) + } + return nil +} diff --git a/keycloak/authentication_subflow.go b/keycloak/authentication_subflow.go new file mode 100644 index 000000000..1a3305a4f --- /dev/null +++ b/keycloak/authentication_subflow.go @@ -0,0 +1,157 @@ +package keycloak + +import ( + "errors" + "fmt" +) + +type AuthenticationSubFlow struct { + Id string `json:"id,omitempty"` + Alias string `json:"alias"` + RealmId string `json:"-"` + ParentFlowAlias string `json:"-"` + ProviderId string `json:"providerId"` // "basic-flow" or "client-flow" or form-flow see /keycloak/server-spi/src/main/java/org/keycloak/models/AuthenticationFlowModel.java + TopLevel bool `json:"topLevel"` // should only be false if this is a subflow + BuiltIn bool `json:"builtIn"` // this controls whether or not this flow can be edited from the console. it can be updated, but this provider will only set it to `true` + Description string `json:"description"` + //execution part + Authenticator string `json:"-"` //can be any authenticator see /auth/admin/master/console/#/server-info/providers (not limited to the authenticator spi section) for example could also be part of the form-action spi + Priority int `json:"-"` + Requirement string `json:"-"` +} + +//each subflow creates a flow and an execution under the covers +type authenticationSubFlowCreate struct { + Alias string `json:"alias"` + Type string `json:"type"` //providerId of the flow + Provider string `json:"provider"` //authenticator of the execution + Description string `json:"description"` +} + +func (keycloakClient *KeycloakClient) NewAuthenticationSubFlow(authenticationSubFlow *AuthenticationSubFlow) error { + authenticationSubFlow.TopLevel = false + authenticationSubFlow.BuiltIn = false + authenticationSubFlowCreate := &authenticationSubFlowCreate{ + Alias: authenticationSubFlow.Alias, + Type: authenticationSubFlow.ProviderId, //providerId of the flow + Provider: authenticationSubFlow.Authenticator, //seems this can be empty //authenticator of the execution + Description: authenticationSubFlow.Description, + } + + _, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions/flow", authenticationSubFlow.RealmId, authenticationSubFlow.ParentFlowAlias), authenticationSubFlowCreate) + if err != nil { + return err + } + authenticationSubFlow.Id = getIdFromLocationHeader(location) + + if authenticationSubFlow.Requirement != "DISABLED" { + return keycloakClient.UpdateAuthenticationSubFlow(authenticationSubFlow) + } + return nil +} + +func (keycloakClient *KeycloakClient) GetAuthenticationSubFlow(realmId, parentFlowAlias, id string) (*AuthenticationSubFlow, error) { + var authenticationSubFlow AuthenticationSubFlow + err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), &authenticationSubFlow, nil) + if err != nil { + return nil, err + } + authenticationSubFlow.RealmId = realmId + authenticationSubFlow.ParentFlowAlias = parentFlowAlias + + executionId, err := keycloakClient.getExecutionId(&authenticationSubFlow) + if err != nil { + return nil, err + } + + subFlowExecution, err := keycloakClient.GetAuthenticationExecution(realmId, parentFlowAlias, executionId) + if err != nil { + return nil, err + } + authenticationSubFlow.Authenticator = subFlowExecution.Authenticator + authenticationSubFlow.Requirement = subFlowExecution.Requirement + + return &authenticationSubFlow, nil +} + +func (keycloakClient *KeycloakClient) getExecutionId(authenticationSubFlow *AuthenticationSubFlow) (string, error) { + list, err := keycloakClient.ListAuthenticationExecutions(authenticationSubFlow.RealmId, authenticationSubFlow.ParentFlowAlias) + if err != nil { + return "", err + } + + for _, ex := range list { + if ex.FlowId == authenticationSubFlow.Id { + return ex.Id, nil + } + } + return "", errors.New("no execution id found for subflow") +} + +func (keycloakClient *KeycloakClient) UpdateAuthenticationSubFlow(authenticationSubFlow *AuthenticationSubFlow) error { + authenticationSubFlow.TopLevel = false + authenticationSubFlow.BuiltIn = false + + err := keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/flows/%s", authenticationSubFlow.RealmId, authenticationSubFlow.Id), authenticationSubFlow) + + if err != nil { + return err + } + + executionId, err := keycloakClient.getExecutionId(authenticationSubFlow) + if err != nil { + return err + } + + //update requirement + authenticationExecutionUpdateRequirement := &authenticationExecutionRequirementUpdate{ + RealmId: authenticationSubFlow.RealmId, + ParentFlowAlias: authenticationSubFlow.ParentFlowAlias, + Id: executionId, + Requirement: authenticationSubFlow.Requirement, + } + return keycloakClient.UpdateAuthenticationExecutionRequirement(authenticationExecutionUpdateRequirement) + +} + +func (keycloakClient *KeycloakClient) DeleteAuthenticationSubFlow(realmId, parentFlowAlias, id string) error { + authenticationSubFlow := AuthenticationSubFlow{ + Id: id, + ParentFlowAlias: parentFlowAlias, + RealmId: realmId, + } + executionId, err := keycloakClient.getExecutionId(&authenticationSubFlow) + if err != nil { + return err + } + + return keycloakClient.DeleteAuthenticationExecution(authenticationSubFlow.RealmId, executionId) +} + +func (keycloakClient *KeycloakClient) RaiseAuthenticationSubFlowPriority(realmId, parentFlowAlias, id string) error { + authenticationSubFlow := AuthenticationSubFlow{ + Id: id, + ParentFlowAlias: parentFlowAlias, + RealmId: realmId, + } + executionId, err := keycloakClient.getExecutionId(&authenticationSubFlow) + if err != nil { + return err + } + + return keycloakClient.RaiseAuthenticationExecutionPriority(authenticationSubFlow.RealmId, executionId) +} + +func (keycloakClient *KeycloakClient) LowerAuthenticationSubFlowPriority(realmId, parentFlowAlias, id string) error { + authenticationSubFlow := AuthenticationSubFlow{ + Id: id, + ParentFlowAlias: parentFlowAlias, + RealmId: realmId, + } + executionId, err := keycloakClient.getExecutionId(&authenticationSubFlow) + if err != nil { + return err + } + + return keycloakClient.LowerAuthenticationExecutionPriority(authenticationSubFlow.RealmId, executionId) +} diff --git a/provider/data_source_keycloak_realm.go b/provider/data_source_keycloak_realm.go index 8317d07c4..8a1069841 100644 --- a/provider/data_source_keycloak_realm.go +++ b/provider/data_source_keycloak_realm.go @@ -60,6 +60,10 @@ func dataSourceKeycloakRealm() *schema.Resource { Type: schema.TypeBool, Computed: true, }, + "ssl_required": { + Type: schema.TypeString, + Computed: true, + }, //Smtp server @@ -262,6 +266,43 @@ func dataSourceKeycloakRealm() *schema.Resource { }, }, }, + "brute_force_detection": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "permanent_lockout": { //Permanent Lockout + Type: schema.TypeBool, + Computed: true, + }, + "max_login_failures": { //failureFactor + Type: schema.TypeInt, + Computed: true, + }, + "wait_increment_seconds": { //Wait Increment + Type: schema.TypeInt, + Computed: true, + }, + "quick_login_check_milli_seconds": { //Quick Login Check Milli Seconds + Type: schema.TypeInt, + Computed: true, + }, + "minimum_quick_login_wait_seconds": { //Minimum Quick Login Wait + Type: schema.TypeInt, + Computed: true, + }, + "max_failure_wait_seconds": { //Max Wait + Type: schema.TypeInt, + Computed: true, + }, + "failure_reset_time_seconds": { //maxDeltaTimeSeconds + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, }, }, }, @@ -302,6 +343,11 @@ func dataSourceKeycloakRealm() *schema.Resource { Description: "Which flow should be used for DockerAuthenticationFlow", Computed: true, }, + "attributes": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + }, }, } } diff --git a/provider/provider.go b/provider/provider.go index d46efe8cf..1ad500625 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -61,6 +61,9 @@ func KeycloakProvider() *schema.Provider { "keycloak_openid_client_service_account_role": resourceKeycloakOpenidClientServiceAccountRole(), "keycloak_openid_client_service_account_realm_role": resourceKeycloakOpenidClientServiceAccountRealmRole(), "keycloak_role": resourceKeycloakRole(), + "keycloak_authentication_flow": resourceKeycloakAuthenticationFlow(), + "keycloak_authentication_subflow": resourceKeycloakAuthenticationSubFlow(), + "keycloak_authentication_execution": resourceKeycloakAuthenticationExecution(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/provider/resource_keycloak_authentication_execution.go b/provider/resource_keycloak_authentication_execution.go new file mode 100644 index 000000000..07d704394 --- /dev/null +++ b/provider/resource_keycloak_authentication_execution.go @@ -0,0 +1,135 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "strings" +) + +func resourceKeycloakAuthenticationExecution() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakAuthenticationExecutionCreate, + Read: resourceKeycloakAuthenticationExecutionRead, + Delete: resourceKeycloakAuthenticationExecutionDelete, + Update: resourceKeycloakAuthenticationExecutionUpdate, + Importer: &schema.ResourceImporter{ + State: resourceKeycloakAuthenticationExecutionImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "parent_flow_alias": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "authenticator": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "requirement": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"REQUIRED", "ALTERNATIVE", "OPTIONAL", "CONDITIONAL", "DISABLED"}, false), //OPTIONAL is removed from 8.0.0 onwards + Default: "DISABLED", + }, + }, + } +} + +func mapFromDataToAuthenticationExecution(data *schema.ResourceData) *keycloak.AuthenticationExecution { + authenticationExecution := &keycloak.AuthenticationExecution{ + Id: data.Id(), + RealmId: data.Get("realm_id").(string), + ParentFlowAlias: data.Get("parent_flow_alias").(string), + Authenticator: data.Get("authenticator").(string), + Requirement: data.Get("requirement").(string), + } + + return authenticationExecution +} + +func mapFromAuthenticationExecutionToData(data *schema.ResourceData, authenticationExecution *keycloak.AuthenticationExecution) { + data.SetId(authenticationExecution.Id) + + data.Set("realm_id", authenticationExecution.RealmId) + data.Set("parent_flow_alias", authenticationExecution.ParentFlowAlias) + data.Set("authenticator", authenticationExecution.Authenticator) + data.Set("requirement", authenticationExecution.Requirement) +} + +func resourceKeycloakAuthenticationExecutionCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + authenticationExecution := mapFromDataToAuthenticationExecution(data) + + err := keycloakClient.NewAuthenticationExecution(authenticationExecution) + if err != nil { + return err + } + + mapFromAuthenticationExecutionToData(data, authenticationExecution) + + return resourceKeycloakAuthenticationExecutionRead(data, meta) +} + +func resourceKeycloakAuthenticationExecutionRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + parentFlowAlias := data.Get("parent_flow_alias").(string) + id := data.Id() + + authenticationExecution, err := keycloakClient.GetAuthenticationExecution(realmId, parentFlowAlias, id) + if err != nil { + return handleNotFoundError(err, data) + } + + mapFromAuthenticationExecutionToData(data, authenticationExecution) + + return nil +} + +func resourceKeycloakAuthenticationExecutionUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + authenticationExecution := mapFromDataToAuthenticationExecution(data) + + err := keycloakClient.UpdateAuthenticationExecution(authenticationExecution) + if err != nil { + return err + } + + mapFromAuthenticationExecutionToData(data, authenticationExecution) + + return nil +} + +func resourceKeycloakAuthenticationExecutionDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + return keycloakClient.DeleteAuthenticationExecution(realmId, id) +} + +func resourceKeycloakAuthenticationExecutionImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 3 { + return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{parentFlowAlias}}/{{authenticationExecutionId}}") + } + + d.Set("realm_id", parts[0]) + d.Set("parent_flow_alias", parts[1]) + d.SetId(parts[2]) + + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_authentication_execution_test.go b/provider/resource_keycloak_authentication_execution_test.go new file mode 100644 index 000000000..af2f9e94c --- /dev/null +++ b/provider/resource_keycloak_authentication_execution_test.go @@ -0,0 +1,225 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakAuthenticationExecution_basic(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + parentAuthFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationExecutionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationExecution_basic(realmName, parentAuthFlowAlias), + Check: testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"), + }, + { + ResourceName: "keycloak_authentication_execution.execution", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getExecutionImportId("keycloak_authentication_execution.execution"), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationExecution_createAfterManualDestroy(t *testing.T) { + var authenticationExecution = &keycloak.AuthenticationExecution{} + + realmName := "terraform-" + acctest.RandString(10) + authParentFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationExecutionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationExecution_basic(realmName, authParentFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"), + testAccCheckKeycloakAuthenticationExecutionFetch("keycloak_authentication_execution.execution", authenticationExecution), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteAuthenticationExecution(authenticationExecution.RealmId, authenticationExecution.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakAuthenticationExecution_basic(realmName, authParentFlowAlias), + Check: testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationExecution_updateAuthenticationExecutionRequirement(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + authParentFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationSubFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationExecution_basic(realmName, authParentFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"), + resource.TestCheckResourceAttr("keycloak_authentication_execution.execution", "requirement", "DISABLED"), + ), + }, + { + Config: testKeycloakAuthenticationExecution_basicWithRequirement(realmName, authParentFlowAlias, "REQUIRED"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"), + resource.TestCheckResourceAttr("keycloak_authentication_execution.execution", "requirement", "REQUIRED"), + ), + }, + { + Config: testKeycloakAuthenticationExecution_basicWithRequirement(realmName, authParentFlowAlias, "DISABLED"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"), + resource.TestCheckResourceAttr("keycloak_authentication_execution.execution", "requirement", "DISABLED"), + ), + }, + }, + }) +} + +func testAccCheckKeycloakAuthenticationExecutionExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getAuthenticationExecutionFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakAuthenticationExecutionFetch(resourceName string, authenticationExecution *keycloak.AuthenticationExecution) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedAuthenticationExecution, err := getAuthenticationExecutionFromState(s, resourceName) + if err != nil { + return err + } + + authenticationExecution.Id = fetchedAuthenticationExecution.Id + authenticationExecution.ParentFlowAlias = fetchedAuthenticationExecution.ParentFlowAlias + authenticationExecution.RealmId = fetchedAuthenticationExecution.RealmId + + return nil + } +} + +func testAccCheckKeycloakAuthenticationExecutionDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_authentication_execution" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + authenticationExecution, _ := keycloakClient.GetAuthenticationExecution(realm, parentFlowAlias, id) + if authenticationExecution != nil { + return fmt.Errorf("authentication flow with id %s still exists", id) + } + } + + return nil + } +} + +func getAuthenticationExecutionFromState(s *terraform.State, resourceName string) (*keycloak.AuthenticationExecution, error) { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"] + + authenticationExecution, err := keycloakClient.GetAuthenticationExecution(realm, parentFlowAlias, id) + + if err != nil { + return nil, fmt.Errorf("error getting authentication execution with id %s: %s", id, err) + } + + return authenticationExecution, nil +} + +func getExecutionImportId(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"] + realmId := rs.Primary.Attributes["realm_id"] + + return fmt.Sprintf("%s/%s/%s", realmId, parentFlowAlias, id), nil + } +} + +func testKeycloakAuthenticationExecution_basic(realm, parentAlias string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + realm_id = "${keycloak_realm.realm.id}" + alias = "%s" +} + +resource "keycloak_authentication_execution" "execution" { + realm_id = "${keycloak_realm.realm.id}" + parent_flow_alias = "${keycloak_authentication_flow.flow.alias}" + authenticator = "auth-cookie" +} + `, realm, parentAlias) +} + +func testKeycloakAuthenticationExecution_basicWithRequirement(realm, parentAlias, requirement string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + realm_id = "${keycloak_realm.realm.id}" + alias = "%s" +} + +resource "keycloak_authentication_execution" "execution" { + realm_id = "${keycloak_realm.realm.id}" + parent_flow_alias = "${keycloak_authentication_flow.flow.alias}" + authenticator = "auth-cookie" + requirement = "%s" +} + `, realm, parentAlias, requirement) +} diff --git a/provider/resource_keycloak_authentication_flow.go b/provider/resource_keycloak_authentication_flow.go new file mode 100644 index 000000000..79f77ba81 --- /dev/null +++ b/provider/resource_keycloak_authentication_flow.go @@ -0,0 +1,127 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "strings" +) + +func resourceKeycloakAuthenticationFlow() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakAuthenticationFlowCreate, + Read: resourceKeycloakAuthenticationFlowRead, + Delete: resourceKeycloakAuthenticationFlowDelete, + Update: resourceKeycloakAuthenticationFlowUpdate, + Importer: &schema.ResourceImporter{ + State: resourceKeycloakAuthenticationFlowImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "alias": { + Type: schema.TypeString, + Required: true, + }, + "provider_id": { + Type: schema.TypeString, + Default: "basic-flow", + ValidateFunc: validation.StringInSlice([]string{"basic-flow", "client-flow"}, false), //it seems toplevel can only one of these and not 'form-flow' + Optional: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func mapFromDataToAuthenticationFlow(data *schema.ResourceData) *keycloak.AuthenticationFlow { + authenticationFlow := &keycloak.AuthenticationFlow{ + Id: data.Id(), + RealmId: data.Get("realm_id").(string), + Alias: data.Get("alias").(string), + ProviderId: data.Get("provider_id").(string), + Description: data.Get("description").(string), + } + + return authenticationFlow +} + +func mapFromAuthenticationFlowToData(data *schema.ResourceData, authenticationFlow *keycloak.AuthenticationFlow) { + data.SetId(authenticationFlow.Id) + data.Set("realm_id", authenticationFlow.RealmId) + data.Set("alias", authenticationFlow.Alias) + data.Set("provider_id", authenticationFlow.ProviderId) + data.Set("description", authenticationFlow.Description) +} + +func resourceKeycloakAuthenticationFlowCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + authenticationFlow := mapFromDataToAuthenticationFlow(data) + + err := keycloakClient.NewAuthenticationFlow(authenticationFlow) + if err != nil { + return err + } + + mapFromAuthenticationFlowToData(data, authenticationFlow) + return resourceKeycloakAuthenticationFlowRead(data, meta) +} + +func resourceKeycloakAuthenticationFlowRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + authenticationFlow, err := keycloakClient.GetAuthenticationFlow(realmId, id) + if err != nil { + return handleNotFoundError(err, data) + } + + mapFromAuthenticationFlowToData(data, authenticationFlow) + return nil +} + +func resourceKeycloakAuthenticationFlowUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + authenticationFlow := mapFromDataToAuthenticationFlow(data) + + err := keycloakClient.UpdateAuthenticationFlow(authenticationFlow) + if err != nil { + return err + } + + mapFromAuthenticationFlowToData(data, authenticationFlow) + return nil +} + +func resourceKeycloakAuthenticationFlowDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + return keycloakClient.DeleteAuthenticationFlow(realmId, id) +} + +func resourceKeycloakAuthenticationFlowImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{authenticationFlowId}}") + } + + d.Set("realm_id", parts[0]) + d.SetId(parts[1]) + + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_authentication_flow_test.go b/provider/resource_keycloak_authentication_flow_test.go new file mode 100644 index 000000000..80ad3133a --- /dev/null +++ b/provider/resource_keycloak_authentication_flow_test.go @@ -0,0 +1,253 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakAuthenticationFlow_basic(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + authFlowAlias := "terraform-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationFlow_basic(realmName, authFlowAlias), + Check: testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + }, + { + ResourceName: "keycloak_authentication_flow.flow", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: realmName + "/", + }, + }, + }) +} + +func TestAccKeycloakAuthenticationFlow_createAfterManualDestroy(t *testing.T) { + var authenticationFlow = &keycloak.AuthenticationFlow{} + + realmName := "terraform-" + acctest.RandString(10) + authFlowAlias := "terraform-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationFlow_basic(realmName, authFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + testAccCheckKeycloakAuthenticationFlowFetch("keycloak_authentication_flow.flow", authenticationFlow), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteAuthenticationFlow(authenticationFlow.RealmId, authenticationFlow.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakAuthenticationFlow_basic(realmName, authFlowAlias), + Check: testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationFlow_updateAuthenticationFlow(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + + authFlowAliasBefore := "terraform-flow-before-" + acctest.RandString(10) + authFlowAliasAfter := "terraform-flow-after-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationFlow_basic(realmName, authFlowAliasBefore), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + resource.TestCheckResourceAttr("keycloak_authentication_flow.flow", "alias", authFlowAliasBefore), + ), + }, + { + Config: testKeycloakAuthenticationFlow_basic(realmName, authFlowAliasAfter), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + resource.TestCheckResourceAttr("keycloak_authentication_flow.flow", "alias", authFlowAliasAfter), + ), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationFlow_updateRealm(t *testing.T) { + realmOne := "terraform-" + acctest.RandString(10) + realmTwo := "terraform-" + acctest.RandString(10) + + authFlowAlias := "terraform-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationFlow_updateRealmBefore(realmOne, realmTwo, authFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + testAccCheckKeycloakAuthenticationFlowBelongsToRealm("keycloak_authentication_flow.flow", realmOne), + ), + }, + { + Config: testKeycloakAuthenticationFlow_updateRealmAfter(realmOne, realmTwo, authFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationFlowExists("keycloak_authentication_flow.flow"), + testAccCheckKeycloakAuthenticationFlowBelongsToRealm("keycloak_authentication_flow.flow", realmTwo), + ), + }, + }, + }) +} + +func testAccCheckKeycloakAuthenticationFlowExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getAuthenticationFlowFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakAuthenticationFlowFetch(resourceName string, authenticationFlow *keycloak.AuthenticationFlow) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedAuthenticationFlow, err := getAuthenticationFlowFromState(s, resourceName) + if err != nil { + return err + } + + authenticationFlow.Id = fetchedAuthenticationFlow.Id + authenticationFlow.RealmId = fetchedAuthenticationFlow.RealmId + + return nil + } +} + +func testAccCheckKeycloakAuthenticationFlowBelongsToRealm(resourceName, realm string) resource.TestCheckFunc { + return func(s *terraform.State) error { + authenticationFlow, err := getAuthenticationFlowFromState(s, resourceName) + if err != nil { + return err + } + + if authenticationFlow.RealmId != realm { + return fmt.Errorf("expected authentication flow with id %s to have realm_id of %s, but got %s", authenticationFlow.Id, realm, authenticationFlow.RealmId) + } + + return nil + } +} + +func testAccCheckKeycloakAuthenticationFlowDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_authentication_flow" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + authenticationFlow, _ := keycloakClient.GetAuthenticationFlow(realm, id) + if authenticationFlow != nil { + return fmt.Errorf("authentication flow with id %s still exists", id) + } + } + + return nil + } +} + +func getAuthenticationFlowFromState(s *terraform.State, resourceName string) (*keycloak.AuthenticationFlow, error) { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + authenticationFlow, err := keycloakClient.GetAuthenticationFlow(realm, id) + if err != nil { + return nil, fmt.Errorf("error getting authentication flow with id %s: %s", id, err) + } + + return authenticationFlow, nil +} + +func testKeycloakAuthenticationFlow_basic(realm, alias string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + realm_id = "${keycloak_realm.realm.id}" + alias = "%s" +} + `, realm, alias) +} + +func testKeycloakAuthenticationFlow_updateRealmBefore(realmOne, realmTwo, alias string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm_1" { + realm = "%s" +} + +resource "keycloak_realm" "realm_2" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + alias = "%s" + realm_id = "${keycloak_realm.realm_1.id}" +} + `, realmOne, realmTwo, alias) +} + +func testKeycloakAuthenticationFlow_updateRealmAfter(realmOne, realmTwo, alias string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm_1" { + realm = "%s" +} + +resource "keycloak_realm" "realm_2" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + alias = "%s" + realm_id = "${keycloak_realm.realm_2.id}" +} + `, realmOne, realmTwo, alias) +} diff --git a/provider/resource_keycloak_authentication_subflow.go b/provider/resource_keycloak_authentication_subflow.go new file mode 100644 index 000000000..d282ee6ff --- /dev/null +++ b/provider/resource_keycloak_authentication_subflow.go @@ -0,0 +1,152 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "strings" +) + +func resourceKeycloakAuthenticationSubFlow() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakAuthenticationSubFlowCreate, + Read: resourceKeycloakAuthenticationSubFlowRead, + Delete: resourceKeycloakAuthenticationSubFlowDelete, + Update: resourceKeycloakAuthenticationSubFlowUpdate, + Importer: &schema.ResourceImporter{ + State: resourceKeycloakAuthenticationSubFlowImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "parent_flow_alias": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "alias": { + Type: schema.TypeString, + Required: true, + }, + "provider_id": { + Type: schema.TypeString, + Default: "basic-flow", + ValidateFunc: validation.StringInSlice([]string{"basic-flow", "form-flow", "client-flow"}, false), + Optional: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + + //execution parts of the subflow + "authenticator": { + Type: schema.TypeString, + Description: "Might be needed to be set with certain custom subflow with specific authenticator, in general this will remain empty", + Optional: true, + ForceNew: true, + }, + "requirement": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"REQUIRED", "ALTERNATIVE", "OPTIONAL", "CONDITIONAL", "DISABLED"}, false), //OPTIONAL is removed from 8.0.0 onwards + Default: "DISABLED", + }, + }, + } +} + +func mapFromDataToAuthenticationSubFlow(data *schema.ResourceData) *keycloak.AuthenticationSubFlow { + authenticationSubFlow := &keycloak.AuthenticationSubFlow{ + Id: data.Id(), + RealmId: data.Get("realm_id").(string), + ParentFlowAlias: data.Get("parent_flow_alias").(string), + Alias: data.Get("alias").(string), + ProviderId: data.Get("provider_id").(string), + Description: data.Get("description").(string), + Authenticator: data.Get("authenticator").(string), + Requirement: data.Get("requirement").(string), + } + + return authenticationSubFlow +} + +func mapFromAuthenticationSubFlowToData(data *schema.ResourceData, authenticationSubFlow *keycloak.AuthenticationSubFlow) { + data.SetId(authenticationSubFlow.Id) + data.Set("realm_id", authenticationSubFlow.RealmId) + data.Set("parent_flow_alias", authenticationSubFlow.ParentFlowAlias) + data.Set("alias", authenticationSubFlow.Alias) + data.Set("provider_id", authenticationSubFlow.ProviderId) + data.Set("description", authenticationSubFlow.Description) + data.Set("authenticator", authenticationSubFlow.Authenticator) + data.Set("requirement", authenticationSubFlow.Requirement) +} + +func resourceKeycloakAuthenticationSubFlowCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + authenticationFlow := mapFromDataToAuthenticationSubFlow(data) + + err := keycloakClient.NewAuthenticationSubFlow(authenticationFlow) + if err != nil { + return err + } + mapFromAuthenticationSubFlowToData(data, authenticationFlow) + return resourceKeycloakAuthenticationSubFlowRead(data, meta) +} + +func resourceKeycloakAuthenticationSubFlowRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + parentFlowAlias := data.Get("parent_flow_alias").(string) + id := data.Id() + + authenticationFlow, err := keycloakClient.GetAuthenticationSubFlow(realmId, parentFlowAlias, id) + if err != nil { + return handleNotFoundError(err, data) + } + mapFromAuthenticationSubFlowToData(data, authenticationFlow) + return nil +} + +func resourceKeycloakAuthenticationSubFlowUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + authenticationFlow := mapFromDataToAuthenticationSubFlow(data) + + err := keycloakClient.UpdateAuthenticationSubFlow(authenticationFlow) + if err != nil { + return err + } + mapFromAuthenticationSubFlowToData(data, authenticationFlow) + return nil +} + +func resourceKeycloakAuthenticationSubFlowDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + parentFlowAlias := data.Get("parent_flow_alias").(string) + id := data.Id() + + return keycloakClient.DeleteAuthenticationSubFlow(realmId, parentFlowAlias, id) +} + +func resourceKeycloakAuthenticationSubFlowImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 3 { + return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{parentFlowAlias}}/{{authenticationSubFlowId}}") + } + + d.Set("realm_id", parts[0]) + d.Set("parent_flow_alias", parts[1]) + d.SetId(parts[2]) + + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_authentication_subflow_test.go b/provider/resource_keycloak_authentication_subflow_test.go new file mode 100644 index 000000000..141aebcc8 --- /dev/null +++ b/provider/resource_keycloak_authentication_subflow_test.go @@ -0,0 +1,260 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakAuthenticationSubFlow_basic(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + parentAuthFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + authFlowAlias := "terraform-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationSubFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationSubFlow_basic(realmName, parentAuthFlowAlias, authFlowAlias), + Check: testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + }, + { + ResourceName: "keycloak_authentication_subflow.subflow", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getSubFlowImportId("keycloak_authentication_subflow.subflow"), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationSubFlow_createAfterManualDestroy(t *testing.T) { + var authenticationSubFlow = &keycloak.AuthenticationSubFlow{} + + realmName := "terraform-" + acctest.RandString(10) + authParentFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + authFlowAlias := "terraform-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationSubFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationSubFlow_basic(realmName, authParentFlowAlias, authFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + testAccCheckKeycloakAuthenticationSubFlowFetch("keycloak_authentication_subflow.subflow", authenticationSubFlow), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteAuthenticationSubFlow(authenticationSubFlow.RealmId, authenticationSubFlow.ParentFlowAlias, authenticationSubFlow.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakAuthenticationSubFlow_basic(realmName, authParentFlowAlias, authFlowAlias), + Check: testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationSubFlow_updateAuthenticationSubFlow(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + authParentFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + authFlowAliasBefore := "terraform-flow-before-" + acctest.RandString(10) + authFlowAliasAfter := "terraform-flow-after-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationSubFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationSubFlow_basic(realmName, authParentFlowAlias, authFlowAliasBefore), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + resource.TestCheckResourceAttr("keycloak_authentication_subflow.subflow", "alias", authFlowAliasBefore), + ), + }, + { + Config: testKeycloakAuthenticationSubFlow_basic(realmName, authParentFlowAlias, authFlowAliasAfter), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + resource.TestCheckResourceAttr("keycloak_authentication_subflow.subflow", "alias", authFlowAliasAfter), + ), + }, + }, + }) +} + +func TestAccKeycloakAuthenticationSubFlow_updateAuthenticationSubFlowRequirement(t *testing.T) { + realmName := "terraform-r-" + acctest.RandString(10) + authParentFlowAlias := "terraform-parent-flow-" + acctest.RandString(10) + authFlowAlias := "terraform-flow-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakAuthenticationSubFlowDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakAuthenticationSubFlow_basic(realmName, authParentFlowAlias, authFlowAlias), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + resource.TestCheckResourceAttr("keycloak_authentication_subflow.subflow", "requirement", "DISABLED"), + ), + }, + { + Config: testKeycloakAuthenticationSubFlow_basicWithRequirement(realmName, authParentFlowAlias, authFlowAlias, "REQUIRED"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + resource.TestCheckResourceAttr("keycloak_authentication_subflow.subflow", "requirement", "REQUIRED"), + ), + }, + { + Config: testKeycloakAuthenticationSubFlow_basicWithRequirement(realmName, authParentFlowAlias, authFlowAlias, "DISABLED"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakAuthenticationSubFlowExists("keycloak_authentication_subflow.subflow"), + resource.TestCheckResourceAttr("keycloak_authentication_subflow.subflow", "requirement", "DISABLED"), + ), + }, + }, + }) +} + +func testAccCheckKeycloakAuthenticationSubFlowExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getAuthenticationSubFlowFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakAuthenticationSubFlowFetch(resourceName string, authenticationSubFlow *keycloak.AuthenticationSubFlow) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedAuthenticationSubFlow, err := getAuthenticationSubFlowFromState(s, resourceName) + if err != nil { + return err + } + + authenticationSubFlow.Id = fetchedAuthenticationSubFlow.Id + authenticationSubFlow.ParentFlowAlias = fetchedAuthenticationSubFlow.ParentFlowAlias + authenticationSubFlow.RealmId = fetchedAuthenticationSubFlow.RealmId + authenticationSubFlow.Alias = fetchedAuthenticationSubFlow.Alias + + return nil + } +} + +func testAccCheckKeycloakAuthenticationSubFlowDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_authentication_subflow" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + authenticationSubFlow, _ := keycloakClient.GetAuthenticationSubFlow(realm, parentFlowAlias, id) + if authenticationSubFlow != nil { + return fmt.Errorf("authentication flow with id %s still exists", id) + } + } + + return nil + } +} + +func getAuthenticationSubFlowFromState(s *terraform.State, resourceName string) (*keycloak.AuthenticationSubFlow, error) { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"] + + authenticationSubFlow, err := keycloakClient.GetAuthenticationSubFlow(realm, parentFlowAlias, id) + + if err != nil { + return nil, fmt.Errorf("error getting authentication subflow with id %s: %s", id, err) + } + + return authenticationSubFlow, nil +} + +func getSubFlowImportId(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"] + realmId := rs.Primary.Attributes["realm_id"] + + return fmt.Sprintf("%s/%s/%s", realmId, parentFlowAlias, id), nil + } +} + +func testKeycloakAuthenticationSubFlow_basic(realm, parentAlias, alias string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + realm_id = "${keycloak_realm.realm.id}" + alias = "%s" +} + +resource "keycloak_authentication_subflow" "subflow" { + realm_id = "${keycloak_realm.realm.id}" + parent_flow_alias = "${keycloak_authentication_flow.flow.alias}" + alias = "%s" + provider_id = "basic-flow" +} + `, realm, parentAlias, alias) +} + +func testKeycloakAuthenticationSubFlow_basicWithRequirement(realm, parentAlias, alias, requirement string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_authentication_flow" "flow" { + realm_id = "${keycloak_realm.realm.id}" + alias = "%s" +} + +resource "keycloak_authentication_subflow" "subflow" { + realm_id = "${keycloak_realm.realm.id}" + parent_flow_alias = "${keycloak_authentication_flow.flow.alias}" + alias = "%s" + provider_id = "basic-flow" + requirement = "%s" +} + `, realm, parentAlias, alias, requirement) +}