diff --git a/keycloak/realm.go b/keycloak/realm.go index aa635187c..76999dee8 100644 --- a/keycloak/realm.go +++ b/keycloak/realm.go @@ -3,8 +3,9 @@ package keycloak import ( "context" "fmt" - "github.com/keycloak/terraform-provider-keycloak/keycloak/types" "strings" + + "github.com/keycloak/terraform-provider-keycloak/keycloak/types" ) type Key struct { @@ -86,7 +87,7 @@ type Realm struct { DefaultDefaultClientScopes []string `json:"defaultDefaultClientScopes,omitempty"` DefaultOptionalClientScopes []string `json:"defaultOptionalClientScopes,omitempty"` - BrowserSecurityHeaders BrowserSecurityHeaders `json:"browserSecurityHeaders"` + BrowserSecurityHeaders *BrowserSecurityHeaders `json:"browserSecurityHeaders,omitempty"` BruteForceProtected bool `json:"bruteForceProtected"` PermanentLockout bool `json:"permanentLockout"` diff --git a/provider/resource_keycloak_realm.go b/provider/resource_keycloak_realm.go index 240782c61..532cac1d8 100644 --- a/provider/resource_keycloak_realm.go +++ b/provider/resource_keycloak_realm.go @@ -475,6 +475,7 @@ func resourceKeycloakRealm() *schema.Resource { "security_defenses": { Type: schema.TypeList, Optional: true, + Computed: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -998,7 +999,7 @@ func getRealmFromData(data *schema.ResourceData, keycloakVersion *version.Versio if len(headersConfig) == 1 { headerSettings := headersConfig[0].(map[string]interface{}) - realm.BrowserSecurityHeaders = keycloak.BrowserSecurityHeaders{ + realm.BrowserSecurityHeaders = &keycloak.BrowserSecurityHeaders{ ContentSecurityPolicy: headerSettings["content_security_policy"].(string), ContentSecurityPolicyReportOnly: headerSettings["content_security_policy_report_only"].(string), StrictTransportSecurity: headerSettings["strict_transport_security"].(string), @@ -1008,8 +1009,6 @@ func getRealmFromData(data *schema.ResourceData, keycloakVersion *version.Versio XXSSProtection: headerSettings["x_xss_protection"].(string), ReferrerPolicy: headerSettings["referrer_policy"].(string), } - } else { - setDefaultSecuritySettingHeaders(realm) } bruteForceDetectionConfig := securityDefensesSettings["brute_force_detection"].([]interface{}) @@ -1027,7 +1026,6 @@ func getRealmFromData(data *schema.ResourceData, keycloakVersion *version.Versio setDefaultSecuritySettingsBruteForceDetection(realm) } } else { - setDefaultSecuritySettingHeaders(realm) setDefaultSecuritySettingsBruteForceDetection(realm) } @@ -1178,7 +1176,7 @@ func getRealmFromData(data *schema.ResourceData, keycloakVersion *version.Versio } func setDefaultSecuritySettingHeaders(realm *keycloak.Realm) { - realm.BrowserSecurityHeaders = keycloak.BrowserSecurityHeaders{ + realm.BrowserSecurityHeaders = &keycloak.BrowserSecurityHeaders{ ContentSecurityPolicy: "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyReportOnly: "", StrictTransportSecurity: "max-age=31536000; includeSubDomains", @@ -1309,6 +1307,14 @@ func setRealmData(data *schema.ResourceData, realm *keycloak.Realm, keycloakVers securityDefensesSettings["brute_force_detection"] = []interface{}{getBruteForceDetectionSettings(realm)} data.Set("security_defenses", []interface{}{securityDefensesSettings}) } + } else { + // if we do not have any configuration in the realm config, use the default config generated by Keycloak + securityDefensesSettings := make(map[string]interface{}) + securityDefensesSettings["headers"] = []interface{}{getHeaderSettings(realm)} + if realm.BruteForceProtected { + securityDefensesSettings["brute_force_detection"] = []interface{}{getBruteForceDetectionSettings(realm)} + } + data.Set("security_defenses", []interface{}{securityDefensesSettings}) } data.Set("password_policy", realm.PasswordPolicy) @@ -1392,6 +1398,11 @@ func getBruteForceDetectionSettings(realm *keycloak.Realm) map[string]interface{ } func getHeaderSettings(realm *keycloak.Realm) map[string]interface{} { + + if realm.BrowserSecurityHeaders == nil { + return nil + } + headersSettings := make(map[string]interface{}) headersSettings["content_security_policy"] = realm.BrowserSecurityHeaders.ContentSecurityPolicy headersSettings["content_security_policy_report_only"] = realm.BrowserSecurityHeaders.ContentSecurityPolicyReportOnly @@ -1431,7 +1442,7 @@ func resourceKeycloakRealmCreate(ctx context.Context, data *schema.ResourceData, return diag.FromErr(err) } - setRealmData(data, realm, keycloakVersion) + data.SetId(realm.Realm) return resourceKeycloakRealmRead(ctx, data, meta) } diff --git a/provider/resource_keycloak_realm_test.go b/provider/resource_keycloak_realm_test.go index 454ad8e51..40c932fb1 100644 --- a/provider/resource_keycloak_realm_test.go +++ b/provider/resource_keycloak_realm_test.go @@ -2,12 +2,13 @@ package provider import ( "fmt" + "regexp" + "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/keycloak/terraform-provider-keycloak/keycloak" - "regexp" - "testing" ) func TestAccKeycloakRealm_basic(t *testing.T) { @@ -517,19 +518,33 @@ func TestAccKeycloakRealm_securityDefensesHeaders(t *testing.T) { CheckDestroy: testAccCheckKeycloakRealmDestroy(), Steps: []resource.TestStep{ { + // current keycloak default for xFrameOptions is SAMEORIGIN Config: testKeycloakRealm_basic(realmName, realmDisplayName, realmDisplayNameHtml), Check: testAccCheckKeycloakRealmSecurityDefensesHeaders("keycloak_realm.realm", "SAMEORIGIN"), }, { + // set explicit non default value for xFrameOptions + Config: testKeycloakRealm_securityDefensesHeaders(realmName, realmDisplayName, "DENY"), + Check: testAccCheckKeycloakRealmSecurityDefensesHeaders("keycloak_realm.realm", "DENY"), + }, + { + // set explicit default value for xFrameOptions Config: testKeycloakRealm_securityDefensesHeaders(realmName, realmDisplayName, "SAMEORIGIN"), Check: testAccCheckKeycloakRealmSecurityDefensesHeaders("keycloak_realm.realm", "SAMEORIGIN"), }, { - Config: testKeycloakRealm_securityDefensesHeaders(realmName, realmDisplayName, "DENY"), + // previously set xFrameOptions value should still be set + Config: testKeycloakRealm_basic(realmName, realmDisplayName, realmDisplayNameHtml), Check: testAccCheckKeycloakRealmSecurityDefensesHeaders("keycloak_realm.realm", "DENY"), }, { - Config: testKeycloakRealm_basic(realmName, realmDisplayName, realmDisplayNameHtml), + // set explicit empty value for xFrameOptions + Config: testKeycloakRealm_securityDefensesHeadersOnlyXFrameOptions(realmName, realmDisplayName, ""), + Check: testAccCheckKeycloakRealmSecurityDefensesHeaders("keycloak_realm.realm", ""), + }, + { + // set explicit non default value for xFrameOptions again + Config: testKeycloakRealm_securityDefensesHeaders(realmName, realmDisplayName, "SAMEORIGIN"), Check: testAccCheckKeycloakRealmSecurityDefensesHeaders("keycloak_realm.realm", "SAMEORIGIN"), }, }, @@ -1604,6 +1619,21 @@ resource "keycloak_realm" "realm" { `, realm, realmDisplayName, xFrameOptions) } +func testKeycloakRealm_securityDefensesHeadersOnlyXFrameOptions(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" + } + } +} + `, realm, realmDisplayName, xFrameOptions) +} + func testKeycloakRealm_securityDefensesBruteForceDetection(realm, realmDisplayName string, maxLoginFailures int) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" {