diff --git a/docs/resources/openid_client.md b/docs/resources/openid_client.md index e9dbb7741..0a9831192 100644 --- a/docs/resources/openid_client.md +++ b/docs/resources/openid_client.md @@ -31,11 +31,11 @@ resource "keycloak_openid_client" "openid_client" { ] login_theme = "keycloak" - + extra_config = { - "key1" = "value1" - "key2" = "value2" - } + "key1" = "value1" + "key2" = "value2" + } } ``` @@ -86,7 +86,7 @@ is set to `true`. - `backchannel_logout_url` - (Optional) The URL that will cause the client to log itself out when a logout request is sent to this realm. If omitted, no logout request will be sent to the client is this case. - `backchannel_logout_session_required` - (Optional) When `true`, a sid (session ID) claim will be included in the logout token when the backchannel logout URL is used. Defaults to `true`. - `backchannel_logout_revoke_offline_sessions` - (Optional) Specifying whether a "revoke_offline_access" event is included in the Logout Token when the Backchannel Logout URL is used. Keycloak will revoke offline sessions when receiving a Logout Token with this event. -- `extra_config` - (Optional) A map of key/value pairs to add extra configuration attributes to this client. This can be used for custom attributes, or to add configuration attributes that is not yet supported by this Terraform provider. Use this attribute at your own risk, as s may conflict with top-level configuration attributes in future provider updates. +- `extra_config` - (Optional) A map of key/value pairs to add extra configuration attributes to this client. This can be used for custom attributes, or to add configuration attributes that are not yet supported by this Terraform provider. Use this attribute at your own risk, as it may conflict with top-level configuration attributes in future provider updates. ## Attributes Reference diff --git a/keycloak/extra_config.go b/keycloak/extra_config.go new file mode 100644 index 000000000..56c484a41 --- /dev/null +++ b/keycloak/extra_config.go @@ -0,0 +1,62 @@ +package keycloak + +import ( + "encoding/json" + "reflect" + "strconv" + "strings" +) + +func unmarshalExtraConfig(data []byte, reflectValue reflect.Value, extraConfig *map[string]interface{}) error { + err := json.Unmarshal(data, extraConfig) + if err != nil { + return err + } + + for i := 0; i < reflectValue.NumField(); i++ { + structField := reflectValue.Type().Field(i) + jsonKey := strings.Split(structField.Tag.Get("json"), ",")[0] + if jsonKey != "-" { + configValue, ok := (*extraConfig)[jsonKey] + if ok { + field := reflectValue.FieldByName(structField.Name) + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.String { + field.SetString(configValue.(string)) + } else if field.Kind() == reflect.Bool { + boolVal, err := strconv.ParseBool(configValue.(string)) + if err == nil { + field.Set(reflect.ValueOf(KeycloakBoolQuoted(boolVal))) + } + } + delete(*extraConfig, jsonKey) + } + } + } + } + + return nil +} + +func marshalExtraConfig(reflectValue reflect.Value, extraConfig map[string]interface{}) ([]byte, error) { + out := map[string]interface{}{} + + for k, v := range extraConfig { + out[k] = v + } + + for i := 0; i < reflectValue.NumField(); i++ { + jsonKey := strings.Split(reflectValue.Type().Field(i).Tag.Get("json"), ",")[0] + if jsonKey != "-" { + field := reflectValue.Field(i) + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.String { + out[jsonKey] = field.String() + } else if field.Kind() == reflect.Bool { + out[jsonKey] = KeycloakBoolQuoted(field.Bool()) + } + } + } + } + return json.Marshal(out) +} diff --git a/keycloak/identity_provider.go b/keycloak/identity_provider.go index ef220cfe8..24827a037 100644 --- a/keycloak/identity_provider.go +++ b/keycloak/identity_provider.go @@ -1,12 +1,9 @@ package keycloak import ( - "encoding/json" "fmt" "log" "reflect" - "strconv" - "strings" ) type IdentityProviderConfig struct { @@ -69,60 +66,6 @@ type IdentityProvider struct { Config *IdentityProviderConfig `json:"config"` } -func (f *IdentityProviderConfig) UnmarshalJSON(data []byte) error { - f.ExtraConfig = map[string]interface{}{} - err := json.Unmarshal(data, &f.ExtraConfig) - if err != nil { - return err - } - v := reflect.ValueOf(f).Elem() - for i := 0; i < v.NumField(); i++ { - structField := v.Type().Field(i) - jsonKey := strings.Split(structField.Tag.Get("json"), ",")[0] - if jsonKey != "-" { - value, ok := f.ExtraConfig[jsonKey] - if ok { - field := v.FieldByName(structField.Name) - if field.IsValid() && field.CanSet() { - if field.Kind() == reflect.String { - field.SetString(value.(string)) - } else if field.Kind() == reflect.Bool { - boolVal, err := strconv.ParseBool(value.(string)) - if err == nil { - field.Set(reflect.ValueOf(KeycloakBoolQuoted(boolVal))) - } - } - delete(f.ExtraConfig, jsonKey) - } - } - } - } - return nil -} - -func (f *IdentityProviderConfig) MarshalJSON() ([]byte, error) { - out := map[string]interface{}{} - - for k, v := range f.ExtraConfig { - out[k] = v - } - v := reflect.ValueOf(f).Elem() - for i := 0; i < v.NumField(); i++ { - jsonKey := strings.Split(v.Type().Field(i).Tag.Get("json"), ",")[0] - if jsonKey != "-" { - field := v.Field(i) - if field.IsValid() && field.CanSet() { - if field.Kind() == reflect.String { - out[jsonKey] = field.String() - } else if field.Kind() == reflect.Bool { - out[jsonKey] = KeycloakBoolQuoted(field.Bool()) - } - } - } - } - return json.Marshal(out) -} - func (keycloakClient *KeycloakClient) NewIdentityProvider(identityProvider *IdentityProvider) error { log.Printf("[WARN] Realm: %s", identityProvider.Realm) _, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/identity-provider/instances", identityProvider.Realm), identityProvider) @@ -152,3 +95,11 @@ func (keycloakClient *KeycloakClient) UpdateIdentityProvider(identityProvider *I func (keycloakClient *KeycloakClient) DeleteIdentityProvider(realm, alias string) error { return keycloakClient.delete(fmt.Sprintf("/realms/%s/identity-provider/instances/%s", realm, alias), nil) } + +func (f *IdentityProviderConfig) UnmarshalJSON(data []byte) error { + return unmarshalExtraConfig(data, reflect.ValueOf(f).Elem(), &f.ExtraConfig) +} + +func (f *IdentityProviderConfig) MarshalJSON() ([]byte, error) { + return marshalExtraConfig(reflect.ValueOf(f).Elem(), f.ExtraConfig) +} diff --git a/keycloak/identity_provider_mapper.go b/keycloak/identity_provider_mapper.go index 37e40205e..ae83057cb 100644 --- a/keycloak/identity_provider_mapper.go +++ b/keycloak/identity_provider_mapper.go @@ -1,12 +1,9 @@ package keycloak import ( - "encoding/json" "fmt" "log" "reflect" - "strconv" - "strings" ) type IdentityProviderMapperConfig struct { @@ -68,55 +65,9 @@ func (keycloakClient *KeycloakClient) DeleteIdentityProviderMapper(realm, alias, } func (f *IdentityProviderMapperConfig) UnmarshalJSON(data []byte) error { - f.ExtraConfig = map[string]interface{}{} - err := json.Unmarshal(data, &f.ExtraConfig) - if err != nil { - return err - } - v := reflect.ValueOf(f).Elem() - for i := 0; i < v.NumField(); i++ { - structField := v.Type().Field(i) - jsonKey := strings.Split(structField.Tag.Get("json"), ",")[0] - if jsonKey != "-" { - value, ok := f.ExtraConfig[jsonKey] - if ok { - field := v.FieldByName(structField.Name) - if field.IsValid() && field.CanSet() { - if field.Kind() == reflect.String { - field.SetString(value.(string)) - } else if field.Kind() == reflect.Bool { - boolVal, err := strconv.ParseBool(value.(string)) - if err == nil { - field.Set(reflect.ValueOf(KeycloakBoolQuoted(boolVal))) - } - } - delete(f.ExtraConfig, jsonKey) - } - } - } - } - return nil + return unmarshalExtraConfig(data, reflect.ValueOf(f).Elem(), &f.ExtraConfig) } func (f *IdentityProviderMapperConfig) MarshalJSON() ([]byte, error) { - out := map[string]interface{}{} - - for k, v := range f.ExtraConfig { - out[k] = v - } - v := reflect.ValueOf(f).Elem() - for i := 0; i < v.NumField(); i++ { - jsonKey := strings.Split(v.Type().Field(i).Tag.Get("json"), ",")[0] - if jsonKey != "-" { - field := v.Field(i) - if field.IsValid() && field.CanSet() { - if field.Kind() == reflect.String { - out[jsonKey] = field.String() - } else if field.Kind() == reflect.Bool { - out[jsonKey] = KeycloakBoolQuoted(field.Bool()) - } - } - } - } - return json.Marshal(out) + return marshalExtraConfig(reflect.ValueOf(f).Elem(), f.ExtraConfig) } diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go index 5c9a82185..3ad996975 100644 --- a/keycloak/keycloak_client.go +++ b/keycloak/keycloak_client.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/cookiejar" "net/url" + "os" "strings" "time" @@ -28,6 +29,7 @@ type KeycloakClient struct { userAgent string version *version.Version additionalHeaders map[string]string + debug bool } type ClientCredentials struct { @@ -109,6 +111,12 @@ func NewKeycloakClient(url, basePath, clientId, clientSecret, realm, username, p } } + if tfLog, ok := os.LookupEnv("TF_LOG"); ok { + if tfLog == "DEBUG" { + keycloakClient.debug = true + } + } + return &keycloakClient, nil } @@ -391,7 +399,7 @@ func (keycloakClient *KeycloakClient) sendRaw(path string, requestBody []byte) ( func (keycloakClient *KeycloakClient) post(path string, requestBody interface{}) ([]byte, string, error) { resourceUrl := keycloakClient.baseUrl + apiUrl + path - payload, err := json.Marshal(requestBody) + payload, err := keycloakClient.marshal(requestBody) if err != nil { return nil, "", err } @@ -409,7 +417,7 @@ func (keycloakClient *KeycloakClient) post(path string, requestBody interface{}) func (keycloakClient *KeycloakClient) put(path string, requestBody interface{}) error { resourceUrl := keycloakClient.baseUrl + apiUrl + path - payload, err := json.Marshal(requestBody) + payload, err := keycloakClient.marshal(requestBody) if err != nil { return err } @@ -433,7 +441,7 @@ func (keycloakClient *KeycloakClient) delete(path string, requestBody interface{ ) if requestBody != nil { - payload, err = json.Marshal(requestBody) + payload, err = keycloakClient.marshal(requestBody) if err != nil { return err } @@ -448,3 +456,11 @@ func (keycloakClient *KeycloakClient) delete(path string, requestBody interface{ return err } + +func (keycloakClient *KeycloakClient) marshal(requestBody interface{}) ([]byte, error) { + if keycloakClient.debug { + return json.MarshalIndent(requestBody, "", " ") + } + + return json.Marshal(requestBody) +} diff --git a/keycloak/openid_client.go b/keycloak/openid_client.go index 9fb77c9c0..9502412cd 100644 --- a/keycloak/openid_client.go +++ b/keycloak/openid_client.go @@ -1,11 +1,8 @@ package keycloak import ( - "encoding/json" "fmt" "reflect" - "strconv" - "strings" ) type OpenidClientRole struct { @@ -354,55 +351,9 @@ func (keycloakClient *KeycloakClient) DetachOpenidClientOptionalScopes(realmId, } func (f *OpenidClientAttributes) UnmarshalJSON(data []byte) error { - f.ExtraConfig = map[string]interface{}{} - err := json.Unmarshal(data, &f.ExtraConfig) - if err != nil { - return err - } - v := reflect.ValueOf(f).Elem() - for i := 0; i < v.NumField(); i++ { - structField := v.Type().Field(i) - jsonKey := strings.Split(structField.Tag.Get("json"), ",")[0] - if jsonKey != "-" { - value, ok := f.ExtraConfig[jsonKey] - if ok { - field := v.FieldByName(structField.Name) - if field.IsValid() && field.CanSet() { - if field.Kind() == reflect.String { - field.SetString(value.(string)) - } else if field.Kind() == reflect.Bool { - boolVal, err := strconv.ParseBool(value.(string)) - if err == nil { - field.Set(reflect.ValueOf(KeycloakBoolQuoted(boolVal))) - } - } - delete(f.ExtraConfig, jsonKey) - } - } - } - } - return nil + return unmarshalExtraConfig(data, reflect.ValueOf(f).Elem(), &f.ExtraConfig) } func (f *OpenidClientAttributes) MarshalJSON() ([]byte, error) { - out := map[string]interface{}{} - - for k, v := range f.ExtraConfig { - out[k] = v - } - v := reflect.ValueOf(f).Elem() - for i := 0; i < v.NumField(); i++ { - jsonKey := strings.Split(v.Type().Field(i).Tag.Get("json"), ",")[0] - if jsonKey != "-" { - field := v.Field(i) - if field.IsValid() && field.CanSet() { - if field.Kind() == reflect.String { - out[jsonKey] = field.String() - } else if field.Kind() == reflect.Bool { - out[jsonKey] = KeycloakBoolQuoted(field.Bool()) - } - } - } - } - return json.Marshal(out) + return marshalExtraConfig(reflect.ValueOf(f).Elem(), f.ExtraConfig) } diff --git a/provider/data_source_keycloak_openid_client_test.go b/provider/data_source_keycloak_openid_client_test.go index 2890a77e1..76f8878ff 100644 --- a/provider/data_source_keycloak_openid_client_test.go +++ b/provider/data_source_keycloak_openid_client_test.go @@ -40,6 +40,26 @@ func TestAccKeycloakDataSourceOpenidClient_basic(t *testing.T) { }) } +func TestAccKeycloakDataSourceOpenidClient_extraConfig(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc-test-extra-config") + dataSourceName := "data.keycloak_openid_client.test_extra_config" + resourceName := "keycloak_openid_client.test_extra_config" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccKeycloakOpenidClientConfig_extraConfig(clientId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "key1", resourceName, "value1"), + ), + }, + }, + }) +} + func testAccKeycloakOpenidClientConfig(clientId string) string { return fmt.Sprintf(` data "keycloak_realm" "realm" { @@ -78,33 +98,13 @@ data "keycloak_openid_client" "test" { `, testAccRealm.Realm, clientId, clientId) } -func TestAccKeycloakDataSourceOpenidClient_extraConfig(t *testing.T) { - t.Parallel() - clientId := acctest.RandomWithPrefix("tf-acc-test-extra-config") - dataSourceName := "data.keycloak_openid_client.test-extra-config" - resourceName := "keycloak_openid_client.test-extra-config" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - Steps: []resource.TestStep{ - { - Config: testAccKeycloakOpenidClientConfig_extraConfig(clientId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrPair(dataSourceName, "key1", resourceName, "value1"), - ), - }, - }, - }) -} - func testAccKeycloakOpenidClientConfig_extraConfig(clientId string) string { return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" } -resource "keycloak_openid_client" "test-extra-config" { +resource "keycloak_openid_client" "test_extra_config" { name = "%s" client_id = "%s" realm_id = data.keycloak_realm.realm.id @@ -115,12 +115,12 @@ resource "keycloak_openid_client" "test-extra-config" { } } -data "keycloak_openid_client" "test-extra-config" { +data "keycloak_openid_client" "test_extra_config" { realm_id = data.keycloak_realm.realm.id - client_id = keycloak_openid_client.test-extra-config.client_id + client_id = keycloak_openid_client.test_extra_config.client_id depends_on = [ - keycloak_openid_client.test-extra-config, + keycloak_openid_client.test_extra_config, ] } `, testAccRealm.Realm, clientId, clientId) diff --git a/provider/extra_config.go b/provider/extra_config.go new file mode 100644 index 000000000..46d461006 --- /dev/null +++ b/provider/extra_config.go @@ -0,0 +1,80 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "reflect" + "strings" +) + +func getExtraConfigFromData(data *schema.ResourceData) map[string]interface{} { + extraConfig := map[string]interface{}{} + if v, ok := data.GetOk("extra_config"); ok { + for key, value := range v.(map[string]interface{}) { + extraConfig[key] = value + } + + // check if extra config attribute has been deleted. + // it's not enough to simply unset the attribute - we have to explicitly set + // it to empty string in order to remove this on the Keycloak side + if data.HasChange("extra_config") && !data.IsNewResource() { + oldConfig, newConfig := data.GetChange("extra_config") + newConfigMap := newConfig.(map[string]interface{}) + + for oldKey := range oldConfig.(map[string]interface{}) { + if _, ok := newConfigMap[oldKey]; !ok { + extraConfig[oldKey] = "" + } + } + } + } + + return extraConfig +} + +func setExtraConfigData(data *schema.ResourceData, extraConfig map[string]interface{}) { + c := map[string]interface{}{} + + // when saving back to state, don't persist empty attributes that we're trying to remove from Keycloak + for k, v := range extraConfig { + if s, ok := v.(string); ok && s == "" { + continue + } + + c[k] = v + } + + data.Set("extra_config", c) +} + +// validateExtraConfig takes a reflect value type to check its JSON schema in order to validate that extra_config +// doesn't contain any attributes that could have been specified within the official schema +func validateExtraConfig(reflectValue reflect.Value) schema.SchemaValidateDiagFunc { + return func(v interface{}, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + + extraConfig := v.(map[string]interface{}) + + for i := 0; i < reflectValue.NumField(); i++ { + field := reflectValue.Field(i) + jsonKey := strings.Split(reflectValue.Type().Field(i).Tag.Get("json"), ",")[0] + + if jsonKey != "-" && field.CanSet() { + if _, ok := extraConfig[jsonKey]; ok { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid extra_config key", + Detail: fmt.Sprintf(`extra_config key "%s" is not allowed, as it conflicts with a top-level schema attribute`, jsonKey), + AttributePath: append(path, cty.IndexStep{ + Key: cty.StringVal(jsonKey), + }), + }) + } + } + } + + return diags + } +} diff --git a/provider/generic_keycloak_identity_provider.go b/provider/generic_keycloak_identity_provider.go index 0bb217b21..424c26212 100644 --- a/provider/generic_keycloak_identity_provider.go +++ b/provider/generic_keycloak_identity_provider.go @@ -2,8 +2,6 @@ package provider import ( "fmt" - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/mrparkers/terraform-provider-keycloak/keycloak" @@ -101,35 +99,9 @@ func resourceKeycloakIdentityProvider() *schema.Resource { }, // all schema values below this point will be configuration values that are shared among all identity providers "extra_config": { - Type: schema.TypeMap, - Optional: true, - // you aren't allowed to specify any keys in extra_config that could be defined as top level attributes - ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics { - var diags diag.Diagnostics - - extraConfig := v.(map[string]interface{}) - value := reflect.ValueOf(&keycloak.IdentityProviderConfig{}).Elem() - - for i := 0; i < value.NumField(); i++ { - field := value.Field(i) - jsonKey := strings.Split(value.Type().Field(i).Tag.Get("json"), ",")[0] - - if jsonKey != "-" && field.CanSet() { - if _, ok := extraConfig[jsonKey]; ok { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Invalid extra_config key", - Detail: fmt.Sprintf(`extra_config key "%s" is not allowed, as it conflicts with a top-level schema attribute`, jsonKey), - AttributePath: append(path, cty.IndexStep{ - Key: cty.StringVal(jsonKey), - }), - }) - } - } - } - - return diags - }, + Type: schema.TypeMap, + Optional: true, + ValidateDiagFunc: validateExtraConfig(reflect.ValueOf(&keycloak.IdentityProviderConfig{}).Elem()), }, "gui_order": { Type: schema.TypeString, @@ -151,19 +123,11 @@ func resourceKeycloakIdentityProvider() *schema.Resource { func getIdentityProviderFromData(data *schema.ResourceData) (*keycloak.IdentityProvider, *keycloak.IdentityProviderConfig) { // some identity provider config is shared among all identity providers, so this default config will be used as a base to merge extra config into defaultIdentityProviderConfig := &keycloak.IdentityProviderConfig{ - GuiOrder: data.Get("gui_order").(string), - SyncMode: data.Get("sync_mode").(string), + GuiOrder: data.Get("gui_order").(string), + SyncMode: data.Get("sync_mode").(string), + ExtraConfig: getExtraConfigFromData(data), } - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } - - defaultIdentityProviderConfig.ExtraConfig = extraConfig - return &keycloak.IdentityProvider{ Realm: data.Get("realm").(string), Alias: data.Get("alias").(string), @@ -197,9 +161,9 @@ func setIdentityProviderData(data *schema.ResourceData, identityProvider *keyclo data.Set("post_broker_login_flow_alias", identityProvider.PostBrokerLoginFlowAlias) // identity provider config - data.Set("extra_config", identityProvider.Config.ExtraConfig) data.Set("gui_order", identityProvider.Config.GuiOrder) data.Set("sync_mode", identityProvider.Config.SyncMode) + setExtraConfigData(data, identityProvider.Config.ExtraConfig) } func resourceKeycloakIdentityProviderDelete(data *schema.ResourceData, meta interface{}) error { diff --git a/provider/generic_keycloak_identity_provider_mapper.go b/provider/generic_keycloak_identity_provider_mapper.go index c1b27b328..9aa1f345f 100644 --- a/provider/generic_keycloak_identity_provider_mapper.go +++ b/provider/generic_keycloak_identity_provider_mapper.go @@ -50,6 +50,9 @@ func getIdentityProviderMapperFromData(data *schema.ResourceData) (*keycloak.Ide Realm: data.Get("realm").(string), Name: data.Get("name").(string), IdentityProviderAlias: data.Get("identity_provider_alias").(string), + Config: &keycloak.IdentityProviderMapperConfig{ + ExtraConfig: getExtraConfigFromData(data), + }, } return rec, nil } @@ -59,6 +62,8 @@ func setIdentityProviderMapperData(data *schema.ResourceData, identityProviderMa data.Set("realm", identityProviderMapper.Realm) data.Set("name", identityProviderMapper.Name) data.Set("identity_provider_alias", identityProviderMapper.IdentityProviderAlias) + setExtraConfigData(data, identityProviderMapper.Config.ExtraConfig) + return nil } diff --git a/provider/resource_keycloak_attribute_importer_identity_provider_mapper.go b/provider/resource_keycloak_attribute_importer_identity_provider_mapper.go index 4633fb622..da221f8af 100644 --- a/provider/resource_keycloak_attribute_importer_identity_provider_mapper.go +++ b/provider/resource_keycloak_attribute_importer_identity_provider_mapper.go @@ -42,22 +42,16 @@ func resourceKeycloakAttributeImporterIdentityProviderMapper() *schema.Resource func getAttributeImporterIdentityProviderMapperFromData(data *schema.ResourceData, meta interface{}) (*keycloak.IdentityProviderMapper, error) { keycloakClient := meta.(*keycloak.KeycloakClient) + rec, _ := getIdentityProviderMapperFromData(data) - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } identityProvider, err := keycloakClient.GetIdentityProvider(rec.Realm, rec.IdentityProviderAlias) if err != nil { return nil, handleNotFoundError(err, data) } + rec.IdentityProviderMapper = fmt.Sprintf("%s-user-attribute-idp-mapper", identityProvider.ProviderId) - rec.Config = &keycloak.IdentityProviderMapperConfig{ - UserAttribute: data.Get("user_attribute").(string), - ExtraConfig: extraConfig, - } + rec.Config.UserAttribute = data.Get("user_attribute").(string) + if identityProvider.ProviderId == "saml" { if attr, ok := data.GetOk("attribute_friendly_name"); ok { rec.Config.AttributeFriendlyName = attr.(string) @@ -79,6 +73,7 @@ func getAttributeImporterIdentityProviderMapperFromData(data *schema.ResourceDat } else { return nil, fmt.Errorf(`provider.keycloak: keycloak_attribute_importer_identity_provider_mapper: %s: "%s" identity provider is not supported yet`, data.Get("name").(string), identityProvider.ProviderId) } + return rec, nil } @@ -94,6 +89,6 @@ func setAttributeImporterIdentityProviderMapperData(data *schema.ResourceData, i data.Set("user_attribute", identityProviderMapper.Config.UserAttribute) data.Set("attribute_friendly_name", identityProviderMapper.Config.AttributeFriendlyName) data.Set("claim_name", claimName) - data.Set("extra_config", identityProviderMapper.Config.ExtraConfig) + return nil } diff --git a/provider/resource_keycloak_attribute_to_role_identity_provider_mapper.go b/provider/resource_keycloak_attribute_to_role_identity_provider_mapper.go index 79d350298..82620b44b 100644 --- a/provider/resource_keycloak_attribute_to_role_identity_provider_mapper.go +++ b/provider/resource_keycloak_attribute_to_role_identity_provider_mapper.go @@ -52,22 +52,16 @@ func resourceKeycloakAttributeToRoleIdentityProviderMapper() *schema.Resource { func getAttributeToRoleIdentityProviderMapperFromData(data *schema.ResourceData, meta interface{}) (*keycloak.IdentityProviderMapper, error) { keycloakClient := meta.(*keycloak.KeycloakClient) + rec, _ := getIdentityProviderMapperFromData(data) - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } identityProvider, err := keycloakClient.GetIdentityProvider(rec.Realm, rec.IdentityProviderAlias) if err != nil { return nil, handleNotFoundError(err, data) } + rec.IdentityProviderMapper = fmt.Sprintf("%s-role-idp-mapper", identityProvider.ProviderId) - rec.Config = &keycloak.IdentityProviderMapperConfig{ - Role: data.Get("role").(string), - ExtraConfig: extraConfig, - } + rec.Config.Role = data.Get("role").(string) + if identityProvider.ProviderId == "saml" { if attr, ok := data.GetOk("attribute_friendly_name"); ok { rec.Config.AttributeFriendlyName = attr.(string) @@ -92,6 +86,7 @@ func getAttributeToRoleIdentityProviderMapperFromData(data *schema.ResourceData, } else { return nil, fmt.Errorf(`provider.keycloak: keycloak_attribute_to_role_identity_provider_mapper: %s: "%s" identity provider is not supported yet`, data.Get("name").(string), identityProvider.ProviderId) } + return rec, nil } @@ -103,6 +98,6 @@ func setAttributeToRoleIdentityProviderMapperData(data *schema.ResourceData, ide data.Set("claim_name", identityProviderMapper.Config.Claim) data.Set("claim_value", identityProviderMapper.Config.ClaimValue) data.Set("attribute_friendly_name", identityProviderMapper.Config.AttributeFriendlyName) - data.Set("extra_config", identityProviderMapper.Config.ExtraConfig) + return nil } diff --git a/provider/resource_keycloak_custom_identity_provider_mapper.go b/provider/resource_keycloak_custom_identity_provider_mapper.go index ea74fd725..6acf0d3a6 100644 --- a/provider/resource_keycloak_custom_identity_provider_mapper.go +++ b/provider/resource_keycloak_custom_identity_provider_mapper.go @@ -26,32 +26,27 @@ func resourceKeycloakCustomIdentityProviderMapper() *schema.Resource { func getCustomIdentityProviderMapperFromData(data *schema.ResourceData, meta interface{}) (*keycloak.IdentityProviderMapper, error) { keycloakClient := meta.(*keycloak.KeycloakClient) + rec, _ := getIdentityProviderMapperFromData(data) - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } identityProvider, err := keycloakClient.GetIdentityProvider(rec.Realm, rec.IdentityProviderAlias) if err != nil { return nil, handleNotFoundError(err, data) } + identityProviderMapper := data.Get("identity_provider_mapper").(string) if strings.Contains(identityProviderMapper, "%s") { rec.IdentityProviderMapper = fmt.Sprintf(identityProviderMapper, identityProvider.ProviderId) } else { rec.IdentityProviderMapper = identityProviderMapper } - rec.Config = &keycloak.IdentityProviderMapperConfig{ - ExtraConfig: extraConfig, - } + return rec, nil } func setCustomIdentityProviderMapperData(data *schema.ResourceData, identityProviderMapper *keycloak.IdentityProviderMapper) error { setIdentityProviderMapperData(data, identityProviderMapper) + setExtraConfigData(data, identityProviderMapper.Config.ExtraConfig) data.Set("identity_provider_mapper", identityProviderMapper.IdentityProviderMapper) - data.Set("extra_config", identityProviderMapper.Config.ExtraConfig) + return nil } diff --git a/provider/resource_keycloak_hardcoded_attribute_identity_provider_mapper.go b/provider/resource_keycloak_hardcoded_attribute_identity_provider_mapper.go index 1effb1269..383e7ab7c 100644 --- a/provider/resource_keycloak_hardcoded_attribute_identity_provider_mapper.go +++ b/provider/resource_keycloak_hardcoded_attribute_identity_provider_mapper.go @@ -36,18 +36,11 @@ func resourceKeycloakHardcodedAttributeIdentityProviderMapper() *schema.Resource func getHardcodedAttributeIdentityProviderMapperFromData(data *schema.ResourceData, _ interface{}) (*keycloak.IdentityProviderMapper, error) { rec, _ := getIdentityProviderMapperFromData(data) - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } + rec.IdentityProviderMapper = getHardcodedAttributeIdentityProviderMapperType(data.Get("user_session").(bool)) - rec.Config = &keycloak.IdentityProviderMapperConfig{ - HardcodedAttribute: data.Get("attribute_name").(string), - AttributeValue: data.Get("attribute_value").(string), - ExtraConfig: extraConfig, - } + rec.Config.HardcodedAttribute = data.Get("attribute_name").(string) + rec.Config.AttributeValue = data.Get("attribute_value").(string) + return rec, nil } @@ -55,12 +48,14 @@ func setHardcodedAttributeIdentityProviderMapperData(data *schema.ResourceData, setIdentityProviderMapperData(data, identityProviderMapper) data.Set("attribute_name", identityProviderMapper.Config.HardcodedAttribute) data.Set("attribute_value", identityProviderMapper.Config.AttributeValue) - data.Set("extra_config", identityProviderMapper.Config.ExtraConfig) + mapperType, err := getUserSessionFromHardcodedAttributeIdentityProviderMapperType(identityProviderMapper.IdentityProviderMapper) if err != nil { return err } + data.Set("user_session", mapperType) + return nil } diff --git a/provider/resource_keycloak_hardcoded_role_identity_provider_mapper.go b/provider/resource_keycloak_hardcoded_role_identity_provider_mapper.go index c38c32822..4bdef9eaa 100644 --- a/provider/resource_keycloak_hardcoded_role_identity_provider_mapper.go +++ b/provider/resource_keycloak_hardcoded_role_identity_provider_mapper.go @@ -23,23 +23,16 @@ func resourceKeycloakHardcodedRoleIdentityProviderMapper() *schema.Resource { func getHardcodedRoleIdentityProviderMapperFromData(data *schema.ResourceData, _ interface{}) (*keycloak.IdentityProviderMapper, error) { rec, _ := getIdentityProviderMapperFromData(data) - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } + rec.IdentityProviderMapper = "oidc-hardcoded-role-idp-mapper" - rec.Config = &keycloak.IdentityProviderMapperConfig{ - Role: data.Get("role").(string), - ExtraConfig: extraConfig, - } + rec.Config.Role = data.Get("role").(string) + return rec, nil } func setHardcodedRoleIdentityProviderMapperData(data *schema.ResourceData, identityProviderMapper *keycloak.IdentityProviderMapper) error { setIdentityProviderMapperData(data, identityProviderMapper) data.Set("role", identityProviderMapper.Config.Role) - data.Set("extra_config", identityProviderMapper.Config.ExtraConfig) + return nil } diff --git a/provider/resource_keycloak_openid_client.go b/provider/resource_keycloak_openid_client.go index b2f0ddfe1..3028b7026 100644 --- a/provider/resource_keycloak_openid_client.go +++ b/provider/resource_keycloak_openid_client.go @@ -7,8 +7,6 @@ import ( "reflect" "strings" - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -228,35 +226,9 @@ func resourceKeycloakOpenidClient() *schema.Resource { Optional: true, }, "extra_config": { - Type: schema.TypeMap, - Optional: true, - // you aren't allowed to specify any keys in extra_config that could be defined as top level attributes - ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics { - var diags diag.Diagnostics - - extraConfig := v.(map[string]interface{}) - value := reflect.ValueOf(&keycloak.OpenidClientAttributes{}).Elem() - - for i := 0; i < value.NumField(); i++ { - field := value.Field(i) - jsonKey := strings.Split(value.Type().Field(i).Tag.Get("json"), ",")[0] - - if jsonKey != "-" && field.CanSet() { - if _, ok := extraConfig[jsonKey]; ok { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Invalid extra_config key", - Detail: fmt.Sprintf(`extra_config key "%s" is not allowed, as it conflicts with a top-level schema attribute`, jsonKey), - AttributePath: append(path, cty.IndexStep{ - Key: cty.StringVal(jsonKey), - }), - }) - } - } - } - - return diags - }, + Type: schema.TypeMap, + Optional: true, + ValidateDiagFunc: validateExtraConfig(reflect.ValueOf(&keycloak.OpenidClientAttributes{}).Elem()), }, }, CustomizeDiff: customdiff.ComputedIf("service_account_user_id", func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { @@ -300,13 +272,6 @@ func getOpenidClientFromData(data *schema.ResourceData) (*keycloak.OpenidClient, } } - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } - openidClient := &keycloak.OpenidClient{ Id: data.Id(), ClientId: data.Get("client_id").(string), @@ -333,7 +298,7 @@ func getOpenidClientFromData(data *schema.ResourceData) (*keycloak.OpenidClient, BackchannelLogoutUrl: data.Get("backchannel_logout_url").(string), BackchannelLogoutRevokeOfflineTokens: keycloak.KeycloakBoolQuoted(data.Get("backchannel_logout_session_required").(bool)), BackchannelLogoutSessionRequired: keycloak.KeycloakBoolQuoted(data.Get("backchannel_logout_revoke_offline_sessions").(bool)), - ExtraConfig: extraConfig, + ExtraConfig: getExtraConfigFromData(data), }, ValidRedirectUris: validRedirectUris, WebOrigins: webOrigins, @@ -429,7 +394,7 @@ func setOpenidClientData(keycloakClient *keycloak.KeycloakClient, data *schema.R data.Set("backchannel_logout_url", client.Attributes.BackchannelLogoutUrl) data.Set("backchannel_logout_session_required", client.Attributes.BackchannelLogoutRevokeOfflineTokens) data.Set("backchannel_logout_revoke_offline_sessions", client.Attributes.BackchannelLogoutSessionRequired) - data.Set("extra_config", client.Attributes.ExtraConfig) + setExtraConfigData(data, client.Attributes.ExtraConfig) if client.AuthorizationServicesEnabled { data.Set("resource_server_id", client.Id) diff --git a/provider/resource_keycloak_openid_client_test.go b/provider/resource_keycloak_openid_client_test.go index 193d4882f..25ee059c7 100644 --- a/provider/resource_keycloak_openid_client_test.go +++ b/provider/resource_keycloak_openid_client_test.go @@ -3,6 +3,7 @@ package provider import ( "fmt" "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -532,8 +533,40 @@ func TestAccKeycloakOpenidClient_extraConfig(t *testing.T) { CheckDestroy: testAccCheckKeycloakOpenidClientDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakOpenidClient_extraConfig(clientId, "key1", "value1"), - Check: testAccCheckKeycloakOpenidClientExtraConfig("keycloak_openid_client.client", "key1", "value1"), + Config: testKeycloakOpenidClient_extraConfig(clientId, map[string]string{ + "key1": "value1", + "key2": "value2", + }), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakOpenidClientExtraConfig("keycloak_openid_client.client", "key1", "value1"), + testAccCheckKeycloakOpenidClientExtraConfig("keycloak_openid_client.client", "key2", "value2"), + ), + }, + { + Config: testKeycloakOpenidClient_extraConfig(clientId, map[string]string{ + "key2": "value2", + }), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakOpenidClientExtraConfig("keycloak_openid_client.client", "key2", "value2"), + testAccCheckKeycloakOpenidClientExtraConfigMissing("keycloak_openid_client.client", "key1"), + ), + }, + }, + }) +} + +func TestAccKeycloakOpenidClient_extraConfigInvalid(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakOpenidClientDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenidClient_extraConfig(clientId, map[string]string{"login_theme": "keycloak"}), + ExpectError: regexp.MustCompile(`extra_config key "login_theme" is not allowed`), }, }, }) @@ -806,6 +839,31 @@ func testAccCheckKeycloakOpenidClientExtraConfig(resourceName string, key string } } +// check that a particular extra config key is missing +func testAccCheckKeycloakOpenidClientExtraConfigMissing(resourceName string, key string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client, err := getOpenidClientFromState(s, resourceName) + if err != nil { + return err + } + + if val, ok := client.Attributes.ExtraConfig[key]; ok { + // keycloak 13+ will remove attributes if set to empty string. on older versions, we'll just check if this value is empty + if versionOk, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(keycloak.Version_13); !versionOk { + if val != "" { + return fmt.Errorf("expected openid client to have empty attribute %v", key) + } + + return nil + } + + return fmt.Errorf("expected openid client to not have attribute %v", key) + } + + return nil + } +} + func getOpenidClientFromState(s *terraform.State, resourceName string) (*keycloak.OpenidClient, error) { rs, ok := s.RootModule().Resources[resourceName] if !ok { @@ -1162,7 +1220,13 @@ resource "keycloak_openid_client" "client" { `, testAccRealm.Realm, clientId, useRefreshTokens) } -func testKeycloakOpenidClient_extraConfig(clientId string, key string, value string) string { +func testKeycloakOpenidClient_extraConfig(clientId string, extraConfig map[string]string) string { + var sb strings.Builder + sb.WriteString("{\n") + for k, v := range extraConfig { + sb.WriteString(fmt.Sprintf("\t\t\"%s\" = \"%s\"\n", k, v)) + } + sb.WriteString("}") return fmt.Sprintf(` data "keycloak_realm" "realm" { @@ -1173,9 +1237,7 @@ resource "keycloak_openid_client" "client" { client_id = "%s" realm_id = data.keycloak_realm.realm.id access_type = "CONFIDENTIAL" - extra_config = { - "%s" = "%s" - } + extra_config = %s } - `, testAccRealm.Realm, clientId, key, value) + `, testAccRealm.Realm, clientId, sb.String()) } diff --git a/provider/resource_keycloak_user_template_importer_identity_provider_mapper.go b/provider/resource_keycloak_user_template_importer_identity_provider_mapper.go index 455933358..48a5309ba 100644 --- a/provider/resource_keycloak_user_template_importer_identity_provider_mapper.go +++ b/provider/resource_keycloak_user_template_importer_identity_provider_mapper.go @@ -25,13 +25,8 @@ func resourceKeycloakUserTemplateImporterIdentityProviderMapper() *schema.Resour func getUserTemplateImporterIdentityProviderMapperFromData(data *schema.ResourceData, meta interface{}) (*keycloak.IdentityProviderMapper, error) { keycloakClient := meta.(*keycloak.KeycloakClient) + rec, _ := getIdentityProviderMapperFromData(data) - extraConfig := map[string]interface{}{} - if v, ok := data.GetOk("extra_config"); ok { - for key, value := range v.(map[string]interface{}) { - extraConfig[key] = value - } - } identityProvider, err := keycloakClient.GetIdentityProvider(rec.Realm, rec.IdentityProviderAlias) if err != nil { return nil, handleNotFoundError(err, data) @@ -43,16 +38,14 @@ func getUserTemplateImporterIdentityProviderMapperFromData(data *schema.Resource rec.IdentityProviderMapper = fmt.Sprintf("%s-username-idp-mapper", identityProvider.ProviderId) } - rec.Config = &keycloak.IdentityProviderMapperConfig{ - Template: data.Get("template").(string), - ExtraConfig: extraConfig, - } + rec.Config.Template = data.Get("template").(string) + return rec, nil } func setUserTemplateImporterIdentityProviderMapperData(data *schema.ResourceData, identityProviderMapper *keycloak.IdentityProviderMapper) error { setIdentityProviderMapperData(data, identityProviderMapper) data.Set("template", identityProviderMapper.Config.Template) - data.Set("extra_config", identityProviderMapper.Config.ExtraConfig) + return nil }