From 669ce051fa12ee78b77521a69bbb80afac0ec4a7 Mon Sep 17 00:00:00 2001 From: tomrutsaert Date: Wed, 31 Jul 2019 20:15:33 +0200 Subject: [PATCH] adds password policy support for keycloak_realm (#139) * added support for Realm passwordPolicy * added support for Realm passwordPolicy --- example/main.tf | 12 +-- keycloak/realm.go | 13 ++++ provider/resource_keycloak_realm.go | 11 +++ provider/resource_keycloak_realm_test.go | 94 ++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/example/main.tf b/example/main.tf index ca0d053f5..80fe90de1 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 5e9791185..73f85bcce 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 0e844f49f..c2777e96b 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 { diff --git a/provider/resource_keycloak_realm_test.go b/provider/resource_keycloak_realm_test.go index 3bd7e7d48..6d1dba628 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) +}