Repository: mrparkers/terraform-provider-keycloak
Pull request #139 (2019-07-31 14:00:54)

Realm Password policies
Added Support for password policies.
It checks if the password policy provider is installed.
I did not change or wrap the syntax of the realm password policy string the keycloak API uses. 

tomrutsaert: Some checks were not successful
1 failing and 1 successful checks:
ci/circleci: test-password — Your tests failed on CircleCI 

--> These test seem to be flaky, they fail often but not always, the failure has nothing to with my change.
mrparkers: They are flaky, I can merge this regardless.  Thanks for the contribution!

Patch:
diff --git a/example/main.tf b/example/main.tf
index ca0d053..80fe90d 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -9,7 +9,7 @@ resource "keycloak_realm" "test" {
 	enabled      = true
 	display_name = "foo"
 
-	smtp_server {
+	smtp_server = {
 		host                  = "mysmtphost.com"
 		port                  = 25
 		from_display_name     = "Tom"
@@ -20,7 +20,7 @@ resource "keycloak_realm" "test" {
 		starttls              = true
 		envelope_from         = "nottom@myhost.com"
 
-		auth {
+		auth  = {
 			username = "tom"
 			password = "tom"
 		}
@@ -30,7 +30,7 @@ resource "keycloak_realm" "test" {
 
 	access_code_lifespan = "30m"
 
-	internationalization {
+	internationalization = {
 		supported_locales = [
 			"en",
 			"de",
@@ -39,8 +39,8 @@ resource "keycloak_realm" "test" {
 		default_locale    = "en"
 	}
 
-	security_defenses {
-		headers {
+	security_defenses = {
+		headers = {
 			x_frame_options = "DENY"
 			content_security_policy = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
 			content_security_policy_report_only = ""
@@ -50,6 +50,8 @@ resource "keycloak_realm" "test" {
 			strict_transport_security = "max-age=31536000; includeSubDomains"
 		}
 	}
+
+	password_policy = "upperCase(1) and length(8) and forceExpiredPasswordChange(365) and notUsername"
 }
 
 resource "keycloak_required_action" "custom-terms-and-conditions" {
diff --git a/keycloak/realm.go b/keycloak/realm.go
index 5e97911..73f85bc 100644
--- a/keycloak/realm.go
+++ b/keycloak/realm.go
@@ -2,6 +2,7 @@ package keycloak
 
 import (
 	"fmt"
+	"strings"
 )
 
 type Key struct {
@@ -66,6 +67,8 @@ type Realm struct {
 
 	//extra attributes of a realm, contains security defenses browser headers and brute force detection parameters(those still nee to be added)
 	Attributes Attributes `json:"attributes,omitempty"`
+
+	PasswordPolicy string `json:"passwordPolicy"`
 }
 
 type Attributes struct {
@@ -174,6 +177,16 @@ func (keycloakClient *KeycloakClient) ValidateRealm(realm *Realm) error {
 		return fmt.Errorf("validation error: DefaultLocale should be in the SupportLocales")
 	}
 
+	if realm.PasswordPolicy != "" {
+		policies := strings.Split(realm.PasswordPolicy, " and ")
+		for _, policyTypeRepresentation := range policies {
+			policy := strings.Split(policyTypeRepresentation, "(")
+			if !serverInfo.providerInstalled("password-policy", policy[0]) {
+				return fmt.Errorf("validation error: password-policy \"%s\" does not exist on the server, installed providers: %s", policy[0], serverInfo.getInstalledProvidersNames("password-policy"))
+			}
+		}
+	}
+
 	return nil
 }
 
diff --git a/provider/resource_keycloak_realm.go b/provider/resource_keycloak_realm.go
index 0e844f4..c2777e9 100644
--- a/provider/resource_keycloak_realm.go
+++ b/provider/resource_keycloak_realm.go
@@ -310,6 +310,11 @@ func resourceKeycloakRealm() *schema.Resource {
 					},
 				},
 			},
+			"password_policy": {
+				Type:        schema.TypeString,
+				Description: "String that represents the passwordPolicies that are in place. Each policy is separated with \" and \". Supported policies can be found in the server-info providers page. example: \"upperCase(1) and length(8) and forceExpiredPasswordChange(365) and notUsername(undefined)\"",
+				Optional:    true,
+			},
 		},
 	}
 }
@@ -534,6 +539,10 @@ func getRealmFromData(data *schema.ResourceData) (*keycloak.Realm, error) {
 	} else {
 		setDefaultSecuritySettings(realm)
 	}
+
+	if passwordPolicy, ok := data.GetOk("password_policy"); ok {
+		realm.PasswordPolicy = passwordPolicy.(string)
+	}
 	return realm, nil
 }
 
@@ -647,6 +656,8 @@ func setRealmData(data *schema.ResourceData, realm *keycloak.Realm) {
 			data.Set("security_defenses", []interface{}{securityDefensesSettings})
 		}
 	}
+
+	data.Set("password_policy", realm.PasswordPolicy)
 }
 
 func resourceKeycloakRealmCreate(data *schema.ResourceData, meta interface{}) error {


Test patch:
diff --git a/provider/resource_keycloak_realm_test.go b/provider/resource_keycloak_realm_test.go
index 3bd7e7d..6d1dba6 100644
--- a/provider/resource_keycloak_realm_test.go
+++ b/provider/resource_keycloak_realm_test.go
@@ -442,6 +442,74 @@ func TestAccKeycloakRealm_securityDefenses(t *testing.T) {
 	})
 }
 
+func TestAccKeycloakRealm_passwordPolicy(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	realmDisplayName := "terraform-" + acctest.RandString(10)
+	passwordPolicyStringValid1 := "upperCase(1) and length(8) and forceExpiredPasswordChange(365) and notUsername"
+	passwordPolicyStringValid2 := "upperCase(1) and length(8)"
+	passwordPolicyStringValid3 := "lowerCase(2)"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakRealmDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRealm_basic(realmName, realmDisplayName),
+				Check:  testAccCheckKeycloakRealmPasswordPolicy("keycloak_realm.realm", ""),
+			},
+			{
+				Config: testKeycloakRealm_passwordPolicy(realmName, realmDisplayName, passwordPolicyStringValid1),
+				Check:  testAccCheckKeycloakRealmPasswordPolicy("keycloak_realm.realm", passwordPolicyStringValid1),
+			},
+			{
+				Config: testKeycloakRealm_passwordPolicy(realmName, realmDisplayName, passwordPolicyStringValid2),
+				Check:  testAccCheckKeycloakRealmPasswordPolicy("keycloak_realm.realm", passwordPolicyStringValid2),
+			},
+			{
+				Config: testKeycloakRealm_passwordPolicy(realmName, realmDisplayName, passwordPolicyStringValid3),
+				Check:  testAccCheckKeycloakRealmPasswordPolicy("keycloak_realm.realm", passwordPolicyStringValid3),
+			},
+			{
+				Config: testKeycloakRealm_basic(realmName, realmDisplayName),
+				Check:  testAccCheckKeycloakRealmPasswordPolicy("keycloak_realm.realm", ""),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakRealm_passwordPolicyInvalid(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	realmDisplayName := "terraform-" + acctest.RandString(10)
+	passwordPolicyStringInvalid1 := "unknownpolicy(1) and length(8) and forceExpiredPasswordChange(365) and notUsername"
+	passwordPolicyStringInvalid2 := "lowerCase(1) and length(8) and unknownpolicy(365) and notUsername"
+	passwordPolicyStringInvalid3 := "unknownpolicy(2)"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakRealmDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRealm_basic(realmName, realmDisplayName),
+				Check:  testAccCheckKeycloakRealmPasswordPolicy("keycloak_realm.realm", ""),
+			},
+			{
+				Config:      testKeycloakRealm_passwordPolicy(realmName, realmDisplayName, passwordPolicyStringInvalid1),
+				ExpectError: regexp.MustCompile("errors during apply: validation error: password-policy .+ does not exist on the server, installed providers: .+"),
+			},
+			{
+				Config:      testKeycloakRealm_passwordPolicy(realmName, realmDisplayName, passwordPolicyStringInvalid2),
+				ExpectError: regexp.MustCompile("errors during apply: validation error: password-policy .+ does not exist on the server, installed providers: .+"),
+			},
+			{
+				Config:      testKeycloakRealm_passwordPolicy(realmName, realmDisplayName, passwordPolicyStringInvalid3),
+				ExpectError: regexp.MustCompile("errors during apply: validation error: password-policy .+ does not exist on the server, installed providers: .+"),
+			},
+		},
+	})
+}
+
 func testKeycloakRealmLoginInfo(resourceName string, realm *keycloak.Realm) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		realmFromState, err := getRealmFromState(s, resourceName)
@@ -651,6 +719,21 @@ func testAccCheckKeycloakRealmSecurityDefenses(resourceName, xFrameOptions strin
 	}
 }
 
+func testAccCheckKeycloakRealmPasswordPolicy(resourceName, passwordPolicy string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		realm, err := getRealmFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		if realm.PasswordPolicy != passwordPolicy {
+			return fmt.Errorf("expected realm %s to have passwordPolicy %s, but was %s", realm.Realm, passwordPolicy, realm.PasswordPolicy)
+		}
+
+		return nil
+	}
+}
+
 func testKeycloakRealm_basic(realm, realmDisplayName string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
@@ -904,3 +987,14 @@ resource "keycloak_realm" "realm" {
 }
 	`, realm, realmDisplayName, xFrameOptions)
 }
+
+func testKeycloakRealm_passwordPolicy(realm, realmDisplayName, passwordPolicy string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm        = "%s"
+	enabled      = true
+	display_name = "%s"
+	password_policy = "%s"
+}
+	`, realm, realmDisplayName, passwordPolicy)
+}