Repository: mrparkers/terraform-provider-keycloak
Pull request #131 (2019-06-25 14:13:21)

Required Actions (squashed relevant commits from pr 123)
squashed relevant commits from pr https://github.com/mrparkers/terraform-provider-keycloak/pull/123

mrparkers: Running `make fmt` should help with the failing CI build.

Patch:
diff --git a/example/main.tf b/example/main.tf
index 17efd77..d9ed45c 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -40,6 +40,23 @@ resource "keycloak_realm" "test" {
 	}
 }
 
+resource "keycloak_required_action" "custom-terms-and-conditions" {
+	realm_id		= "${keycloak_realm.test.realm}"
+	alias			= "terms_and_conditions"
+	default_action 	= true
+	enabled			= true
+	name			= "Custom Terms and Conditions"
+}
+
+resource "keycloak_required_action" "custom-configured_totp" {
+	realm_id		= "${keycloak_realm.test.realm}"
+	alias			= "CONFIGURE_TOTP"
+	default_action 	= true
+	enabled			= true
+	name			= "Custom configure totp"
+	priority		= "${keycloak_required_action.custom-terms-and-conditions.priority+15}"
+}
+
 resource "keycloak_group" "foo" {
 	realm_id = "${keycloak_realm.test.id}"
 	name     = "foo"
diff --git a/keycloak/required_action.go b/keycloak/required_action.go
new file mode 100644
index 0000000..3971962
--- /dev/null
+++ b/keycloak/required_action.go
@@ -0,0 +1,81 @@
+package keycloak
+
+import "fmt"
+
+type RequiredAction struct {
+	Id            string              `json:"-"`
+	RealmId       string              `json:"-"`
+	Alias         string              `json:"alias"`
+	Name          string              `json:"name"`
+	Enabled       bool                `json:"enabled"`
+	DefaultAction bool                `json:"defaultAction"`
+	Priority      int                 `json:"priority"`
+	Config        map[string][]string `json:"config"`
+}
+
+func (requiredActions *RequiredAction) getConfig(val string) string {
+	if len(requiredActions.Config[val]) == 0 {
+		return ""
+	}
+	return requiredActions.Config[val][0]
+}
+
+func (requiredActions *RequiredAction) getConfigOk(val string) (string, bool) {
+	if v, ok := requiredActions.Config[val]; ok {
+		return v[0], true
+	}
+	return "", false
+}
+
+func (keycloakClient *KeycloakClient) GetRequiredAction(realmId string, alias string) (*RequiredAction, error) {
+	var requiredAction RequiredAction
+
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/required-actions/%s", realmId, alias), &requiredAction, nil)
+	if err != nil {
+		return nil, err
+	}
+	requiredAction.RealmId = realmId
+	return &requiredAction, nil
+}
+
+func (keycloakClient *KeycloakClient) CreateRequiredAction(requiredAction *RequiredAction) error {
+	requiredAction.Id = fmt.Sprintf("%s/%s", requiredAction.RealmId, requiredAction.Alias)
+	return keycloakClient.UpdateRequiredAction(requiredAction)
+}
+
+func (keycloakClient *KeycloakClient) UpdateRequiredAction(requiredAction *RequiredAction) error {
+
+	err := keycloakClient.ValidateRequiredAction(requiredAction)
+	if err != nil {
+		return err
+	}
+
+	return keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/required-actions/%s", requiredAction.RealmId, requiredAction.Alias), requiredAction)
+}
+
+func (keycloakClient *KeycloakClient) DeleteRequiredAction(realmName string, alias string) error {
+	err := keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/required-actions/%s", realmName, alias), 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/required-actions/%s", realmName, alias), nil)
+	}
+
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) ValidateRequiredAction(requiredAction *RequiredAction) error {
+	serverInfo, err := keycloakClient.GetServerInfo()
+	if err != nil {
+		return err
+	}
+
+	if requiredAction.DefaultAction && !requiredAction.Enabled {
+		return fmt.Errorf("validation error: a 'default' required action should be enabled, set 'defaultAction' to 'false' or set 'enabled' to 'true'")
+	}
+
+	if !serverInfo.providerInstalled("required-action", requiredAction.Alias) {
+		return fmt.Errorf("validation error: required action \"%s\" does not exist on the server, installed providers: %s", requiredAction.Alias, serverInfo.getInstalledProvidersNames("required-action"))
+	}
+
+	return nil
+}
diff --git a/keycloak/server_info.go b/keycloak/server_info.go
index 48b7533..8d238a7 100644
--- a/keycloak/server_info.go
+++ b/keycloak/server_info.go
@@ -4,14 +4,23 @@ type ComponentType struct {
 	Id string `json:"id"`
 }
 
-type Theme struct {
-	Name    string   `json:"name"`
-	Locales []string `json:"locales,omitempty"`
+type Provider struct {
+}
+
+type ProviderType struct {
+	Internal  bool                `json:"internal"`
+	Providers map[string]Provider `json:"providers"`
 }
 
 type ServerInfo struct {
-	Themes         map[string][]Theme         `json:"themes"`
 	ComponentTypes map[string][]ComponentType `json:"componentTypes"`
+	ProviderTypes  map[string]ProviderType    `json:"providers"`
+	Themes         map[string][]Theme         `json:"themes"`
+}
+
+type Theme struct {
+	Name    string   `json:"name"`
+	Locales []string `json:"locales,omitempty"`
 }
 
 func (serverInfo *ServerInfo) ThemeIsInstalled(t, themeName string) bool {
@@ -38,6 +47,25 @@ func (serverInfo *ServerInfo) ComponentTypeIsInstalled(componentType, componentT
 	return false
 }
 
+func (serverInfo *ServerInfo) getInstalledProvidersNames(providerType string) []string {
+	providers := serverInfo.ProviderTypes[providerType].Providers
+	keys := make([]string, 0, len(providers))
+	for p := range providers {
+		keys = append(keys, p)
+	}
+	return keys
+}
+
+func (serverInfo *ServerInfo) providerInstalled(providerType, providerName string) bool {
+	providers := serverInfo.ProviderTypes[providerType].Providers
+	for p := range providers {
+		if p == providerName {
+			return true
+		}
+	}
+	return false
+}
+
 func (keycloakClient *KeycloakClient) GetServerInfo() (*ServerInfo, error) {
 	var serverInfo ServerInfo
 
diff --git a/provider/provider.go b/provider/provider.go
index 814911f..bfd519a 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -13,6 +13,7 @@ func KeycloakProvider() *schema.Provider {
 		},
 		ResourcesMap: map[string]*schema.Resource{
 			"keycloak_realm":                                           resourceKeycloakRealm(),
+			"keycloak_required_action":                                 resourceKeycloakRequiredAction(),
 			"keycloak_group":                                           resourceKeycloakGroup(),
 			"keycloak_group_memberships":                               resourceKeycloakGroupMemberships(),
 			"keycloak_user":                                            resourceKeycloakUser(),
diff --git a/provider/resource_keycloak_required_action.go b/provider/resource_keycloak_required_action.go
new file mode 100644
index 0000000..782a96d
--- /dev/null
+++ b/provider/resource_keycloak_required_action.go
@@ -0,0 +1,144 @@
+package provider
+
+import (
+	"fmt"
+	"github.com/hashicorp/terraform/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"strings"
+)
+
+func resourceKeycloakRequiredAction() *schema.Resource {
+
+	return &schema.Resource{
+		Create: resourceKeycloakRequiredActionsCreate,
+		Read:   resourceKeycloakRequiredActionsRead,
+		Delete: resourceKeycloakRequiredActionsDelete,
+		Update: resourceKeycloakRequiredActionsUpdate,
+		Importer: &schema.ResourceImporter{
+			// This resource can be imported using {{realm}}/{{alias}}. The required action aliases are displayed in the server info or GET realms/{{realm}}/authentication/required-actions
+			State: resourceKeycloakRequiredActionsImport,
+		},
+		Schema: map[string]*schema.Schema{
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"alias": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"name": {
+				Type:     schema.TypeString,
+				Optional: true,
+			},
+			"enabled": {
+				Type:     schema.TypeBool,
+				Optional: true,
+			},
+			"default_action": {
+				Type:     schema.TypeBool,
+				Optional: true,
+			},
+			"priority": {
+				Type:     schema.TypeInt,
+				Optional: true,
+				Computed: true,
+			},
+		},
+	}
+}
+
+func getRequiredActionFromData(data *schema.ResourceData) (*keycloak.RequiredAction, error) {
+	action := &keycloak.RequiredAction{
+		Id:            fmt.Sprintf("%s/%s", data.Get("realm_id").(string), data.Get("alias").(string)),
+		RealmId:       data.Get("realm_id").(string),
+		Alias:         data.Get("alias").(string),
+		Name:          data.Get("name").(string),
+		Enabled:       data.Get("enabled").(bool),
+		DefaultAction: data.Get("default_action").(bool),
+		Priority:      data.Get("priority").(int),
+		Config:        make(map[string][]string),
+	}
+
+	return action, nil
+}
+
+func setRequiredActionData(data *schema.ResourceData, action *keycloak.RequiredAction) {
+	data.SetId(fmt.Sprintf("%s/%s", action.RealmId, action.Alias))
+	data.Set("realm_id", action.RealmId)
+	data.Set("alias", action.Alias)
+	data.Set("name", action.Name)
+	data.Set("enabled", action.Enabled)
+	data.Set("default_action", action.DefaultAction)
+	data.Set("priority", action.Priority)
+}
+
+func resourceKeycloakRequiredActionsCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	action, err := getRequiredActionFromData(data)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.CreateRequiredAction(action)
+	if err != nil {
+		return err
+	}
+
+	setRequiredActionData(data, action)
+
+	return resourceKeycloakRequiredActionsRead(data, meta)
+}
+
+func resourceKeycloakRequiredActionsRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	action, err := keycloakClient.GetRequiredAction(data.Get("realm_id").(string), data.Get("alias").(string))
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	setRequiredActionData(data, action)
+
+	return nil
+}
+
+func resourceKeycloakRequiredActionsUpdate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	action, err := getRequiredActionFromData(data)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.UpdateRequiredAction(action)
+	if err != nil {
+		return err
+	}
+
+	setRequiredActionData(data, action)
+
+	return nil
+}
+
+func resourceKeycloakRequiredActionsDelete(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmName := data.Get("realm_id").(string)
+	alias := data.Get("alias").(string)
+
+	return keycloakClient.DeleteRequiredAction(realmName, alias)
+}
+
+func resourceKeycloakRequiredActionsImport(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}}/{{alias}}")
+	}
+	d.Set("realm_id", parts[0])
+	d.Set("alias", parts[1])
+	d.SetId(fmt.Sprintf("%s/%s", parts[0], parts[1]))
+
+	return []*schema.ResourceData{d}, nil
+}


Test patch:
diff --git a/provider/resource_keycloak_required_action_test.go b/provider/resource_keycloak_required_action_test.go
new file mode 100644
index 0000000..355ff14
--- /dev/null
+++ b/provider/resource_keycloak_required_action_test.go
@@ -0,0 +1,193 @@
+package provider
+
+import (
+	"fmt"
+	"github.com/hashicorp/terraform/helper/acctest"
+	"github.com/hashicorp/terraform/helper/resource"
+	"github.com/hashicorp/terraform/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"regexp"
+	"testing"
+)
+
+func TestAccKeycloakRequiredAction_basic(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	requiredActionAlias := "CONFIGURE_TOTP"
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRequiredAction_basic(realmName, requiredActionAlias, 37),
+				Check:  testAccCheckKeycloakRequiresActionExistsWithCorrectPriority(realmName, requiredActionAlias, 37),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakRequiredAction_invalidAlias(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	randomReqActionAlias := "randomRequiredAction-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config:      testKeycloakRequiredAction_basic(realmName, randomReqActionAlias, 37),
+				ExpectError: regexp.MustCompile("errors during apply: validation error: required action .+ does not exist on the server, installed providers: .+"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakRequiredAction_import(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	requiredActionAlias := "terms_and_conditions"
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRequiredAction_import(realmName, requiredActionAlias),
+				Check:  testAccCheckKeycloakRequiresActionExists(realmName, requiredActionAlias),
+			},
+			{
+				ResourceName:      "keycloak_required_action.required_action",
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateId:     realmName + "/" + requiredActionAlias,
+			},
+		},
+	})
+}
+
+func TestAccKeycloakRequiredAction_disabledDefault(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	requiredActionAlias := "terms_and_conditions"
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config:      testKeycloakRequiredAction_disabledDefault(realmName, requiredActionAlias),
+				ExpectError: regexp.MustCompile("errors during apply: validation error: a 'default' required action should be enabled, set 'defaultAction' to 'false' or set 'enabled' to 'true'"),
+			},
+		},
+	})
+}
+func TestAccKeycloakRequiredAction_computedPriority(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	requiredActionAlias := "terms_and_conditions"
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRequiredAction_computedPriority(realmName, requiredActionAlias, 37, 14),
+				Check:  testAccCheckKeycloakRequiresActionExistsWithCorrectPriority(realmName, requiredActionAlias, 51),
+			},
+		},
+	})
+}
+
+func testKeycloakRequiredAction_basic(realm, requiredActionAlias string, priority int) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_required_action" "required_action" {
+	realm_id		= "${keycloak_realm.realm.realm}"
+	alias			= "%s"
+	default_action 	= true
+	enabled			= true
+	name			= "My required Action"
+	priority		= %d
+}
+	`, realm, requiredActionAlias, priority)
+}
+
+func testKeycloakRequiredAction_import(realm, requiredActionAlias string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_required_action" "required_action" {
+	realm_id		= "${keycloak_realm.realm.realm}"
+	alias			= "%s"
+}
+	`, realm, requiredActionAlias)
+}
+
+func testKeycloakRequiredAction_disabledDefault(realm, requiredActionAlias string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_required_action" "required_action" {
+	realm_id		= "${keycloak_realm.realm.realm}"
+	alias			= "%s"
+	default_action 	= true
+	enabled			= false
+	name			= "My required Action"
+	priority		= 56
+}
+	`, realm, requiredActionAlias)
+}
+
+func testKeycloakRequiredAction_computedPriority(realm, requiredActionAlias string, priority1, priorityPlus int) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_required_action" "required_action" {
+	realm_id		= "${keycloak_realm.realm.realm}"
+	alias			= "VERIFY_EMAIL"
+	name			= "My required Action"
+	priority		= %d
+}
+
+resource "keycloak_required_action" "required_action2" {
+	realm_id		= "${keycloak_realm.realm.realm}"
+	alias			= "%s"
+	name			= "My required Action 2"
+	priority		= "${keycloak_required_action.required_action.priority+%d}"
+}
+	`, realm, priority1, requiredActionAlias, priorityPlus)
+}
+
+func testAccCheckKeycloakRequiresActionExistsWithCorrectPriority(realm, requiredActionAlias string, priority int) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+		action, err := keycloakClient.GetRequiredAction(realm, requiredActionAlias)
+		if err != nil {
+			return fmt.Errorf("required action not found: %s", requiredActionAlias)
+		}
+
+		if action.Priority != priority {
+			return fmt.Errorf("expected required action to have priority %d, but got %d", priority, action.Priority)
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakRequiresActionExists(realm, requiredActionAlias string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+		_, err := keycloakClient.GetRequiredAction(realm, requiredActionAlias)
+		if err != nil {
+			return fmt.Errorf("required action not found: %s", requiredActionAlias)
+		}
+
+		return nil
+	}
+}