Repository: mrparkers/terraform-provider-keycloak
Pull request #241 (2020-02-29 19:22:10)

Add support for authentication execution configuration
Building on #215, this PR adds support for configuring executions.

I doubt that this is the best implementation as I am a keycloak/terraform newbie, but it seems good enough for my use case.

mrparkers: Hi @ndrpnt, thanks for the PR!  My only thought here is that it may be more user-friendly to define `config` as a new attribute on the `keycloak_authentication_execution` resource instead of having its own resource.  What are your thoughts on this?
ndrpnt: I could see that being more elegant for the user, but slightly more complex in the code as it doesn't stick to the Keycloak API (one Terraform resource would manage two Keycloak resources). I can give it a try.
ndrpnt: Hi @mrparkers, getting back to you after trying to merge `execution` and `execution config` in a single Terraform resource. It is more complex than I anticipated, and I would rather not waste more time on it, as I don't think this is the best solution.

My unfinished attempt is [here](https://github.com/ndrpnt/terraform-provider-keycloak/tree/merge_execution_config). I may be missing something, but I feel like it makes the code more complicated than it needs to be. I ended up in some sort of typecasting/mapping hell (or is it what any provider looks like?) which makes me wonder if I am heading in the right direction.
This is to be compared with the separate resources implementation which was super simple.

Some problems I faced:
- CRUD operation can be successful on one resource and fail on the other (implies the use of partial state ?)
- Lifecycle of the resources differ: we should ideally be able to add/update/delete an `execution config` without affecting the parent `execution`. This is some logic that we delegate to the Terraform framework when splitting the resources
- The client offers two separate datastructures (`AuthenticationExecution` and `AuthenticationExecutionConfig`) that I have trouble mapping to a single `ResourceData` struct (maybe using an intermediate datastructure would help?)

What do you think?
mrparkers: Thanks for your thoughts!  I didn't think my suggestion would bring that many challenges, but I understand that it's difficult to manage this in one terraform resource with the Keycloak API presents them as two separate resources.

I'm fine with merging your PR as-is.

Thanks for the contribution!

Patch:
diff --git a/docs/resources/keycloak_authentication_execution_config.md b/docs/resources/keycloak_authentication_execution_config.md
new file mode 100644
index 0000000..ce47731
--- /dev/null
+++ b/docs/resources/keycloak_authentication_execution_config.md
@@ -0,0 +1,53 @@
+# keycloak_authentication_execution_config
+
+Allows for managing an authentication execution configuration.
+
+### Example Usage
+
+```hcl
+resource "keycloak_realm" "realm" {
+	realm   = "my-realm"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = "${keycloak_realm.realm.id}"
+	alias    = "my-flow-alias"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = "${keycloak_realm.realm.id}"
+	parent_flow_alias = "${keycloak_authentication_flow.flow.alias}"
+	authenticator     = "identity-provider-redirector"
+}
+
+resource "keycloak_authentication_execution_config" "config" {
+	realm_id     = "${keycloak_realm.realm.id}"
+	execution_id = "${keycloak_authentication_execution.execution.id}"
+	alias        = "my-config-alias"
+	config = {
+		defaultProvider = "my-config-default-idp"
+	}
+}
+```
+
+### Argument Reference
+
+The following arguments are supported:
+
+- `realm_id` - (Required) The realm the authentication execution exists in.
+- `execution_id` - (Required) The authentication execution this configuration is attached to.
+- `alias` - (Required) The name of the configuration.
+- `config` - (Optional) The configuration. Keys are specific to each configurable authentication execution and not checked when applying.
+
+### Import
+
+Configurations can be imported using the format `{{realm}}/{{authenticationExecutionId}}/{{authenticationExecutionConfigId}}`.
+If the `authenticationExecutionId` is incorrect, the import will still be successful.
+A subsequent apply will change the `authenticationExecutionId` to the correct one, which causes the configuration to be replaced.
+
+Example:
+
+```bash
+$ terraform import keycloak_authentication_execution_config.config my-realm/be081463-ddbf-4b42-9eff-9c97886f24ff/30559fcf-6fb8-45ea-8c46-2b86f46ebc17
+```
diff --git a/example/main.tf b/example/main.tf
index 5b1cefd..40a4981 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -714,3 +714,12 @@ resource "keycloak_authentication_execution" "browser-copy-otp" {
   requirement = "REQUIRED"
   depends_on = ["keycloak_authentication_execution.browser-copy-auth-username-password-form"]
 }
+
+resource "keycloak_authentication_execution_config" "config" {
+  realm_id     = "${keycloak_realm.test.id}"
+  execution_id = "${keycloak_authentication_execution.browser-copy-idp-redirect.id}"
+  alias        = "idp-XXX-config"
+  config = {
+    defaultProvider = "idp-XXX"
+  }
+}
diff --git a/keycloak/authentication_execution_config.go b/keycloak/authentication_execution_config.go
new file mode 100644
index 0000000..8966c1b
--- /dev/null
+++ b/keycloak/authentication_execution_config.go
@@ -0,0 +1,38 @@
+package keycloak
+
+import (
+	"fmt"
+)
+
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticatorconfigrepresentation
+type AuthenticationExecutionConfig struct {
+	RealmId     string            `json:"-"`
+	ExecutionId string            `json:"-"`
+	Id          string            `json:"id"`
+	Alias       string            `json:"alias"`
+	Config      map[string]string `json:"config"`
+}
+
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_newexecutionconfig
+func (keycloakClient *KeycloakClient) NewAuthenticationExecutionConfig(config *AuthenticationExecutionConfig) (string, error) {
+	_, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/executions/%s/config", config.RealmId, config.ExecutionId), config)
+	if err != nil {
+		return "", err
+	}
+	return getIdFromLocationHeader(location), nil
+}
+
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getauthenticatorconfig
+func (keycloakClient *KeycloakClient) GetAuthenticationExecutionConfig(config *AuthenticationExecutionConfig) error {
+	return keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/config/%s", config.RealmId, config.Id), config, nil)
+}
+
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_updateauthenticatorconfig
+func (keycloakClient *KeycloakClient) UpdateAuthenticationExecutionConfig(config *AuthenticationExecutionConfig) error {
+	return keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/config/%s", config.RealmId, config.Id), config)
+}
+
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_removeauthenticatorconfig
+func (keycloakClient *KeycloakClient) DeleteAuthenticationExecutionConfig(config *AuthenticationExecutionConfig) error {
+	return keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/config/%s", config.RealmId, config.Id), nil)
+}
diff --git a/provider/provider.go b/provider/provider.go
index 6ecc053..59f4160 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -66,6 +66,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_authentication_flow":                             resourceKeycloakAuthenticationFlow(),
 			"keycloak_authentication_subflow":                          resourceKeycloakAuthenticationSubFlow(),
 			"keycloak_authentication_execution":                        resourceKeycloakAuthenticationExecution(),
+			"keycloak_authentication_execution_config":                 resourceKeycloakAuthenticationExecutionConfig(),
 		},
 		Schema: map[string]*schema.Schema{
 			"client_id": {
diff --git a/provider/resource_keycloak_authentication_execution_config.go b/provider/resource_keycloak_authentication_execution_config.go
new file mode 100644
index 0000000..76d0bfe
--- /dev/null
+++ b/provider/resource_keycloak_authentication_execution_config.go
@@ -0,0 +1,137 @@
+package provider
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func resourceKeycloakAuthenticationExecutionConfig() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceKeycloakAuthenticationExecutionConfigCreate,
+		Read:   resourceKeycloakAuthenticationExecutionConfigRead,
+		Delete: resourceKeycloakAuthenticationExecutionConfigDelete,
+		Update: resourceKeycloakAuthenticationExecutionConfigUpdate,
+		Importer: &schema.ResourceImporter{
+			State: resourceKeycloakAuthenticationExecutionConfigImport,
+		},
+		Schema: map[string]*schema.Schema{
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"execution_id": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"alias": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"config": {
+				Type:     schema.TypeMap,
+				Elem:     &schema.Schema{Type: schema.TypeString},
+				Required: true,
+			},
+		},
+	}
+}
+
+func getAuthenticationExecutionConfigFromData(data *schema.ResourceData) *keycloak.AuthenticationExecutionConfig {
+	config := make(map[string]string)
+	for key, value := range data.Get("config").(map[string]interface{}) {
+		config[key] = value.(string)
+	}
+	return &keycloak.AuthenticationExecutionConfig{
+		Id:          data.Id(),
+		RealmId:     data.Get("realm_id").(string),
+		ExecutionId: data.Get("execution_id").(string),
+		Alias:       data.Get("alias").(string),
+		Config:      config,
+	}
+}
+
+func setAuthenticationExecutionConfigData(data *schema.ResourceData, config *keycloak.AuthenticationExecutionConfig) {
+	data.SetId(config.Id)
+	data.Set("realm_id", config.RealmId)
+	data.Set("execution_id", config.ExecutionId)
+	data.Set("alias", config.Alias)
+	data.Set("config", config.Config)
+}
+
+func resourceKeycloakAuthenticationExecutionConfigCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	config := getAuthenticationExecutionConfigFromData(data)
+
+	id, err := keycloakClient.NewAuthenticationExecutionConfig(config)
+	if err != nil {
+		return err
+	}
+
+	data.SetId(id)
+
+	return resourceKeycloakAuthenticationExecutionConfigRead(data, meta)
+}
+
+func resourceKeycloakAuthenticationExecutionConfigRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	config := &keycloak.AuthenticationExecutionConfig{
+		RealmId:     data.Get("realm_id").(string),
+		ExecutionId: data.Get("execution_id").(string),
+		Id:          data.Id(),
+	}
+
+	err := keycloakClient.GetAuthenticationExecutionConfig(config)
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	setAuthenticationExecutionConfigData(data, config)
+
+	return nil
+}
+
+func resourceKeycloakAuthenticationExecutionConfigUpdate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	config := getAuthenticationExecutionConfigFromData(data)
+
+	err := keycloakClient.UpdateAuthenticationExecutionConfig(config)
+	if err != nil {
+		return err
+	}
+
+	return resourceKeycloakAuthenticationExecutionConfigRead(data, meta)
+}
+
+func resourceKeycloakAuthenticationExecutionConfigDelete(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	config := &keycloak.AuthenticationExecutionConfig{
+		RealmId: data.Get("realm_id").(string),
+		Id:      data.Id(),
+	}
+
+	return keycloakClient.DeleteAuthenticationExecutionConfig(config)
+}
+
+func resourceKeycloakAuthenticationExecutionConfigImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
+	parts := strings.Split(data.Id(), "/")
+
+	if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" {
+		return nil, fmt.Errorf("invalid import. Supported import formats: {{realm}}/{{authenticationExecutionId}}/{{authenticationExecutionConfigId}}")
+	}
+
+	data.Set("realm_id", parts[0])
+	data.Set("execution_id", parts[1])
+	data.SetId(parts[2])
+
+	return []*schema.ResourceData{data}, nil
+}


Test patch:
diff --git a/provider/resource_keycloak_authentication_execution_config_test.go b/provider/resource_keycloak_authentication_execution_config_test.go
new file mode 100644
index 0000000..b0e19dc
--- /dev/null
+++ b/provider/resource_keycloak_authentication_execution_config_test.go
@@ -0,0 +1,253 @@
+package provider
+
+import (
+	"fmt"
+	"testing"
+
+	"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"
+)
+
+func TestAccKeycloakAuthenticationExecutionConfig_basic(t *testing.T) {
+	realmName := "terraform-r-" + acctest.RandString(10)
+	var config1, config2 keycloak.AuthenticationExecutionConfig
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccKeycloakAuthenticationExecutionConfig(realmName),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakAuthenticationExecutionConfigExists("keycloak_authentication_execution_config.config", &config1),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "realm_id", realmName),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "alias", "some-config-alias"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.%", "1"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.defaultProvider", "some-config-default-idp"),
+				),
+			},
+			{
+				Config: testAccKeycloakAuthenticationExecutionConfigUpdatedConfig(realmName),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakAuthenticationExecutionConfigExists("keycloak_authentication_execution_config.config", &config2),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "realm_id", realmName),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "alias", "some-config-alias"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.%", "1"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.defaultProvider", "some-updated-config-default-idp"),
+					testAccCheckKeycloakAuthenticationExecutionConfigForceNew(&config1, &config2, false),
+				),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakAuthenticationExecutionConfig_updateForcesNew(t *testing.T) {
+	realmName := "terraform-r-" + acctest.RandString(10)
+	var config1, config2 keycloak.AuthenticationExecutionConfig
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccKeycloakAuthenticationExecutionConfig(realmName),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakAuthenticationExecutionConfigExists("keycloak_authentication_execution_config.config", &config1),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "realm_id", realmName),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "alias", "some-config-alias"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.%", "1"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.defaultProvider", "some-config-default-idp"),
+				),
+			},
+			{
+				Config: testAccKeycloakAuthenticationExecutionConfigUpdatedAlias(realmName),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakAuthenticationExecutionConfigExists("keycloak_authentication_execution_config.config", &config2),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "realm_id", realmName),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "alias", "some-updated-config-alias"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.%", "1"),
+					resource.TestCheckResourceAttr("keycloak_authentication_execution_config.config", "config.defaultProvider", "some-config-default-idp"),
+					testAccCheckKeycloakAuthenticationExecutionConfigForceNew(&config1, &config2, true),
+				),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakAuthenticationExecutionConfig_import(t *testing.T) {
+	realmName := "terraform-r-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccKeycloakAuthenticationExecutionConfig(realmName),
+			},
+			{
+				ResourceName:      "keycloak_authentication_execution_config.config",
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getExecutionConfigImportId("keycloak_authentication_execution_config.config"),
+			},
+		},
+	})
+}
+
+func getExecutionConfigImportId(resourceName string) resource.ImportStateIdFunc {
+	return func(s *terraform.State) (string, error) {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return "", fmt.Errorf("resource %s not found", resourceName)
+		}
+
+		realmId := rs.Primary.Attributes["realm_id"]
+		executionId := rs.Primary.Attributes["execution_id"]
+		id := rs.Primary.ID
+
+		return fmt.Sprintf("%s/%s/%s", realmId, executionId, id), nil
+	}
+}
+
+func testAccCheckKeycloakAuthenticationExecutionConfigExists(resourceName string, config *keycloak.AuthenticationExecutionConfig) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("resource %s not found in state", resourceName)
+		}
+
+		config.RealmId = rs.Primary.Attributes["realm_id"]
+		config.ExecutionId = rs.Primary.Attributes["execution_id"]
+		config.Id = rs.Primary.ID
+
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+		if err := keycloakClient.GetAuthenticationExecutionConfig(config); err != nil {
+			return fmt.Errorf("error fetching authentication execution config: %v", err)
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakAuthenticationExecutionConfigDestroy(s *terraform.State) error {
+	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+	for _, rs := range s.RootModule().Resources {
+		if rs.Type != "keycloak_authentication_execution_config" {
+			continue
+		}
+
+		config := &keycloak.AuthenticationExecutionConfig{
+			RealmId: rs.Primary.Attributes["realm_id"],
+			Id:      rs.Primary.ID,
+		}
+		if err := keycloakClient.GetAuthenticationExecutionConfig(config); err == nil {
+			return fmt.Errorf("authentication execution config still exists")
+		} else if !keycloak.ErrorIs404(err) {
+			return fmt.Errorf("could not fetch authentication execution config: %v", err)
+		}
+	}
+
+	return nil
+}
+
+func testAccCheckKeycloakAuthenticationExecutionConfigForceNew(old, new *keycloak.AuthenticationExecutionConfig, wantNew bool) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		if wantNew {
+			if old.Id == new.Id {
+				return fmt.Errorf("expecting authentication execution config ID to differ, got %+v and %+v", old, new)
+			}
+		} else {
+			if old.Id != new.Id {
+				return fmt.Errorf("expecting authentication execution config ID to be equal, got %+v and %+v", old, new)
+			}
+		}
+		return nil
+	}
+}
+
+func testAccKeycloakAuthenticationExecutionConfig(realm string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = "${keycloak_realm.realm.id}"
+	alias    = "some-flow-alias"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = "${keycloak_realm.realm.id}"
+	parent_flow_alias = "${keycloak_authentication_flow.flow.alias}"
+	authenticator     = "identity-provider-redirector"
+}
+
+resource "keycloak_authentication_execution_config" "config" {
+	realm_id     = "${keycloak_realm.realm.id}"
+	execution_id = "${keycloak_authentication_execution.execution.id}"
+	alias        = "some-config-alias"
+	config = {
+		defaultProvider = "some-config-default-idp"
+	}
+}`, realm)
+}
+
+func testAccKeycloakAuthenticationExecutionConfigUpdatedConfig(realm string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = "${keycloak_realm.realm.id}"
+	alias    = "some-flow-alias"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = "${keycloak_realm.realm.id}"
+	parent_flow_alias = "${keycloak_authentication_flow.flow.alias}"
+	authenticator     = "identity-provider-redirector"
+}
+
+resource "keycloak_authentication_execution_config" "config" {
+	realm_id     = "${keycloak_realm.realm.id}"
+	execution_id = "${keycloak_authentication_execution.execution.id}"
+	alias        = "some-config-alias"
+	config = {
+		defaultProvider = "some-updated-config-default-idp"
+	}
+}`, realm)
+}
+
+func testAccKeycloakAuthenticationExecutionConfigUpdatedAlias(realm string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = "${keycloak_realm.realm.id}"
+	alias    = "some-flow-alias"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = "${keycloak_realm.realm.id}"
+	parent_flow_alias = "${keycloak_authentication_flow.flow.alias}"
+	authenticator     = "identity-provider-redirector"
+}
+
+resource "keycloak_authentication_execution_config" "config" {
+	realm_id     = "${keycloak_realm.realm.id}"
+	execution_id = "${keycloak_authentication_execution.execution.id}"
+	alias        = "some-updated-config-alias"
+	config = {
+		defaultProvider = "some-config-default-idp"
+	}
+}`, realm)
+}