Repository: mrparkers/terraform-provider-keycloak
Pull request #130 (2019-06-25 08:27:13)

Security defenses browser headers
Added Security Defenses Browser Headers fields on Realm

tomrutsaert: @mrparkers , do you have any comments or remarks on this pull request?
It is quite straightforward. The only strange thing is that you need to set the defaults when the headers are missing.
 

Patch:
diff --git a/example/main.tf b/example/main.tf
index d9ed45c..3031a4f 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -26,7 +26,7 @@ resource "keycloak_realm" "test" {
 		}
 	}
 
-	account_theme = "base"
+  	account_theme = "base"
 
 	access_code_lifespan = "30m"
 
@@ -38,6 +38,18 @@ resource "keycloak_realm" "test" {
 		]
 		default_locale    = "en"
 	}
+
+	security_defenses {
+		headers {
+			x_frame_options = "DENY"
+			content_security_policy = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
+			content_security_policy_report_only = ""
+			x_content_type_options = "nosniff"
+			x_robots_tag = "none"
+			x_xss_protection = "1; mode=block"
+			strict_transport_security = "max-age=31536000; includeSubDomains"
+		}
+	}
 }
 
 resource "keycloak_required_action" "custom-terms-and-conditions" {
diff --git a/keycloak/realm.go b/keycloak/realm.go
index f23666a..5e97911 100644
--- a/keycloak/realm.go
+++ b/keycloak/realm.go
@@ -63,6 +63,19 @@ type Realm struct {
 	InternationalizationEnabled bool     `json:"internationalizationEnabled"`
 	SupportLocales              []string `json:"supportedLocales"`
 	DefaultLocale               string   `json:"defaultLocale"`
+
+	//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"`
+}
+
+type Attributes struct {
+	BrowserHeaderContentSecurityPolicy           string `json:"_browser_header.contentSecurityPolicy,omitempty"`
+	BrowserHeaderContentSecurityPolicyReportOnly string `json:"_browser_header.contentSecurityPolicyReportOnly,omitempty"`
+	BrowserHeaderStrictTransportSecurity         string `json:"_browser_header.strictTransportSecurity,omitempty"`
+	BrowserHeaderXContentTypeOptions             string `json:"_browser_header.xContentTypeOptions,omitempty"`
+	BrowserHeaderXFrameOptions                   string `json:"_browser_header.xFrameOptions,omitempty"`
+	BrowserHeaderXRobotsTag                      string `json:"_browser_header.xRobotsTag,omitempty"`
+	BrowserHeaderXXSSProtection                  string `json:"_browser_header.xXSSProtection,omitempty"`
 }
 
 type SmtpServer struct {
diff --git a/provider/resource_keycloak_realm.go b/provider/resource_keycloak_realm.go
index 5c9de80..0e844f4 100644
--- a/provider/resource_keycloak_realm.go
+++ b/provider/resource_keycloak_realm.go
@@ -255,6 +255,61 @@ func resourceKeycloakRealm() *schema.Resource {
 					},
 				},
 			},
+
+			//Security Defenses
+			"security_defenses": {
+				Type:     schema.TypeList,
+				Optional: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"headers": {
+							Type:     schema.TypeList,
+							Optional: true,
+							MaxItems: 1,
+							Elem: &schema.Resource{
+								Schema: map[string]*schema.Schema{
+									"x_frame_options": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "SAMEORIGIN",
+									},
+									"content_security_policy": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+									},
+									"content_security_policy_report_only": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "",
+									},
+									"x_content_type_options": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "nosniff",
+									},
+									"x_robots_tag": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "none",
+									},
+									"x_xss_protection": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "1; mode=block",
+									},
+									"strict_transport_security": {
+										Type:     schema.TypeString,
+										Optional: true,
+										Default:  "max-age=31536000; includeSubDomains",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
 		},
 	}
 }
@@ -456,9 +511,44 @@ func getRealmFromData(data *schema.ResourceData) (*keycloak.Realm, error) {
 		realm.ActionTokenGeneratedByAdminLifespan = actionTokenGeneratedByAdminLifespanDurationString
 	}
 
+	//security defenses
+	if v, ok := data.GetOk("security_defenses"); ok {
+		securityDefensesSettings := v.([]interface{})[0].(map[string]interface{})
+
+		headersConfig := securityDefensesSettings["headers"].([]interface{})
+		if len(headersConfig) == 1 {
+			headerSettings := headersConfig[0].(map[string]interface{})
+
+			realm.Attributes = keycloak.Attributes{
+				BrowserHeaderContentSecurityPolicy:           headerSettings["content_security_policy"].(string),
+				BrowserHeaderContentSecurityPolicyReportOnly: headerSettings["content_security_policy_report_only"].(string),
+				BrowserHeaderStrictTransportSecurity:         headerSettings["strict_transport_security"].(string),
+				BrowserHeaderXContentTypeOptions:             headerSettings["x_content_type_options"].(string),
+				BrowserHeaderXFrameOptions:                   headerSettings["x_frame_options"].(string),
+				BrowserHeaderXRobotsTag:                      headerSettings["x_robots_tag"].(string),
+				BrowserHeaderXXSSProtection:                  headerSettings["x_xss_protection"].(string),
+			}
+		} else {
+			setDefaultSecuritySettings(realm)
+		}
+	} else {
+		setDefaultSecuritySettings(realm)
+	}
 	return realm, nil
 }
 
+func setDefaultSecuritySettings(realm *keycloak.Realm) {
+	realm.Attributes = keycloak.Attributes{
+		BrowserHeaderContentSecurityPolicy:           "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+		BrowserHeaderContentSecurityPolicyReportOnly: "",
+		BrowserHeaderStrictTransportSecurity:         "max-age=31536000; includeSubDomains",
+		BrowserHeaderXContentTypeOptions:             "nosniff",
+		BrowserHeaderXFrameOptions:                   "SAMEORIGIN",
+		BrowserHeaderXRobotsTag:                      "none",
+		BrowserHeaderXXSSProtection:                  "1; mode=block",
+	}
+}
+
 func setRealmData(data *schema.ResourceData, realm *keycloak.Realm) {
 	data.SetId(realm.Realm)
 
@@ -534,6 +624,29 @@ func setRealmData(data *schema.ResourceData, realm *keycloak.Realm) {
 	} else {
 		data.Set("internationalization", nil)
 	}
+
+	if _, ok := data.GetOk("security_defenses"); ok {
+
+		if (keycloak.Attributes{}) == realm.Attributes {
+			data.Set("security_defenses", nil)
+		} else {
+			securityDefensesSettings := make(map[string]interface{})
+
+			headersSettings := make(map[string]interface{})
+
+			headersSettings["content_security_policy"] = realm.Attributes.BrowserHeaderContentSecurityPolicy
+			headersSettings["content_security_policy_report_only"] = realm.Attributes.BrowserHeaderContentSecurityPolicyReportOnly
+			headersSettings["strict_transport_security"] = realm.Attributes.BrowserHeaderStrictTransportSecurity
+			headersSettings["x_content_type_options"] = realm.Attributes.BrowserHeaderXContentTypeOptions
+			headersSettings["x_frame_options"] = realm.Attributes.BrowserHeaderXFrameOptions
+			headersSettings["x_robots_tag"] = realm.Attributes.BrowserHeaderXRobotsTag
+			headersSettings["x_xss_protection"] = realm.Attributes.BrowserHeaderXXSSProtection
+
+			securityDefensesSettings["headers"] = []interface{}{headersSettings}
+
+			data.Set("security_defenses", []interface{}{securityDefensesSettings})
+		}
+	}
 }
 
 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 97e41cf..3bd7e7d 100644
--- a/provider/resource_keycloak_realm_test.go
+++ b/provider/resource_keycloak_realm_test.go
@@ -413,6 +413,35 @@ func TestAccKeycloakRealm_computedTokenSettings(t *testing.T) {
 	})
 }
 
+func TestAccKeycloakRealm_securityDefenses(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	realmDisplayName := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakRealmDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRealm_basic(realmName, realmDisplayName),
+				Check:  testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "SAMEORIGIN"),
+			},
+			{
+				Config: testKeycloakRealm_securityDefenses(realmName, realmDisplayName, "SAMEORIGIN"),
+				Check:  testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "SAMEORIGIN"),
+			},
+			{
+				Config: testKeycloakRealm_securityDefenses(realmName, realmDisplayName, "DENY"),
+				Check:  testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "DENY"),
+			},
+			{
+				Config: testKeycloakRealm_basic(realmName, realmDisplayName),
+				Check:  testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "SAMEORIGIN"),
+			},
+		},
+	})
+}
+
 func testKeycloakRealmLoginInfo(resourceName string, realm *keycloak.Realm) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		realmFromState, err := getRealmFromState(s, resourceName)
@@ -607,6 +636,21 @@ func getRealmFromState(s *terraform.State, resourceName string) (*keycloak.Realm
 	return realm, nil
 }
 
+func testAccCheckKeycloakRealmSecurityDefenses(resourceName, xFrameOptions string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		realm, err := getRealmFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		if realm.Attributes.BrowserHeaderXFrameOptions != xFrameOptions {
+			return fmt.Errorf("expected realm %s to have attribute _browser_header.xFrameOptions set to %s, but was %s", realm.Realm, xFrameOptions, realm.Attributes.BrowserHeaderXFrameOptions)
+		}
+
+		return nil
+	}
+}
+
 func testKeycloakRealm_basic(realm, realmDisplayName string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
@@ -839,3 +883,24 @@ resource "keycloak_realm" "realm" {
 }
 	`, realm, realm, ssoSessionIdleTimeout, ssoSessionMaxLifespan, offlineSessionIdleTimeout, offlineSessionMaxLifespan, accessTokenLifespan, accessTokenLifespanForImplicitFlow, accessCodeLifespan, accessCodeLifespanLogin, accessCodeLifespanUserAction, actionTokenGeneratedByUserLifespan, actionTokenGeneratedByAdminLifespan)
 }
+
+func testKeycloakRealm_securityDefenses(realm, realmDisplayName, xFrameOptions string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm        = "%s"
+	enabled      = true
+	display_name = "%s"
+	security_defenses {
+    	headers {
+			x_frame_options = "%s"
+			content_security_policy = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
+			content_security_policy_report_only = ""
+			x_content_type_options = "nosniff"
+			x_robots_tag = "none"
+			x_xss_protection = "1; mode=block"
+			strict_transport_security = "max-age=31536000; includeSubDomains"
+		}
+	}
+}
+	`, realm, realmDisplayName, xFrameOptions)
+}