diff --git a/.gitignore b/.gitignore index b8ac9250..5f0714fa 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ terraform-provider-keycloak .idea/ .terraform/ +terraform.d/ +.terraform.lock.hcl terraform.tfstate* .gradle/ diff --git a/docs/resources/ldap_user_attribute_mapper.md b/docs/resources/ldap_user_attribute_mapper.md index 88ee3502..34e47ef1 100644 --- a/docs/resources/ldap_user_attribute_mapper.md +++ b/docs/resources/ldap_user_attribute_mapper.md @@ -56,6 +56,8 @@ resource "keycloak_ldap_user_attribute_mapper" "ldap_user_attribute_mapper" { - `read_only` - (Optional) When `true`, this attribute is not saved back to LDAP when the user attribute is updated in Keycloak. Defaults to `false`. - `always_read_value_from_ldap` - (Optional) When `true`, the value fetched from LDAP will override the value stored in Keycloak. Defaults to `false`. - `is_mandatory_in_ldap` - (Optional) When `true`, this attribute must exist in LDAP. Defaults to `false`. +- `attribute_default_value` - (Optional) Default value to set in LDAP if `is_mandatory_in_ldap` is true and the value is empty. +- `is_binary_attribute` - (Optional) Should be true for binary LDAP attributes. ## Import diff --git a/example/main.tf b/example/main.tf index 915e06f5..a783ff8f 100644 --- a/example/main.tf +++ b/example/main.tf @@ -8,9 +8,9 @@ terraform { } provider "keycloak" { - client_id = "terraform" - client_secret = "884e0f95-0f42-4a63-9b1f-94274655669e" - url = "http://localhost:8080" + client_id = "terraform" + client_secret = "884e0f95-0f42-4a63-9b1f-94274655669e" + url = "http://localhost:8080" additional_headers = { foo = "bar" } @@ -85,7 +85,7 @@ resource "keycloak_realm" "test" { web_authn_policy { relying_party_entity_name = "Example" relying_party_id = "keycloak.example.com" - signature_algorithms = [ + signature_algorithms = [ "ES256", "RS256" ] @@ -94,7 +94,7 @@ resource "keycloak_realm" "test" { web_authn_passwordless_policy { relying_party_entity_name = "Example" relying_party_id = "keycloak.example.com" - signature_algorithms = [ + signature_algorithms = [ "ES256", "RS256" ] @@ -189,7 +189,7 @@ resource "keycloak_group" "baz" { } resource "keycloak_default_groups" "default" { - realm_id = keycloak_realm.test.id + realm_id = keycloak_realm.test.id group_ids = [ keycloak_group.baz.id ] @@ -310,7 +310,7 @@ resource "keycloak_ldap_role_mapper" "ldap_role_mapper" { ldap_roles_dn = "dc=example,dc=org" role_name_ldap_attribute = "cn" - role_object_classes = [ + role_object_classes = [ "groupOfNames" ] membership_attribute_type = "DN" @@ -331,6 +331,19 @@ resource "keycloak_ldap_user_attribute_mapper" "description_attr_mapper" { always_read_value_from_ldap = false } +resource "keycloak_ldap_user_attribute_mapper" "default_attr_mapper" { + name = "defaultval-mapper" + realm_id = keycloak_ldap_user_federation.openldap.realm_id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + + user_model_attribute = "defaultval" + ldap_attribute = "defaultval" + + always_read_value_from_ldap = false + is_mandatory_in_ldap = true + attribute_default_value = "testing" +} + resource "keycloak_ldap_group_mapper" "group_mapper" { name = "group mapper" realm_id = keycloak_ldap_user_federation.openldap.realm_id @@ -603,7 +616,7 @@ resource "keycloak_saml_user_property_protocol_mapper" "saml_user_property_mappe saml_attribute_name_format = "Unspecified" } -resource keycloak_oidc_identity_provider oidc { +resource "keycloak_oidc_identity_provider" "oidc" { realm = keycloak_realm.test.id alias = "oidc" authorization_url = "https://example.com/auth" @@ -615,7 +628,7 @@ resource keycloak_oidc_identity_provider oidc { gui_order = 1 } -resource keycloak_oidc_google_identity_provider google { +resource "keycloak_oidc_google_identity_provider" "google" { realm = keycloak_realm.test.id client_id = "myclientid.apps.googleusercontent.com" client_secret = "myclientsecret" @@ -643,7 +656,7 @@ resource keycloak_oidc_google_identity_provider google { // } //} -resource keycloak_attribute_importer_identity_provider_mapper oidc { +resource "keycloak_attribute_importer_identity_provider_mapper" "oidc" { realm = keycloak_realm.test.id name = "attributeImporter" claim_name = "upn" @@ -656,7 +669,7 @@ resource keycloak_attribute_importer_identity_provider_mapper oidc { } } -resource keycloak_attribute_to_role_identity_provider_mapper oidc { +resource "keycloak_attribute_to_role_identity_provider_mapper" "oidc" { realm = keycloak_realm.test.id name = "attributeToRole" claim_name = "upn" @@ -670,7 +683,7 @@ resource keycloak_attribute_to_role_identity_provider_mapper oidc { } } -resource keycloak_user_template_importer_identity_provider_mapper oidc { +resource "keycloak_user_template_importer_identity_provider_mapper" "oidc" { realm = keycloak_realm.test.id name = "userTemplate" identity_provider_alias = keycloak_oidc_identity_provider.oidc.alias @@ -682,7 +695,7 @@ resource keycloak_user_template_importer_identity_provider_mapper oidc { } } -resource keycloak_hardcoded_role_identity_provider_mapper oidc { +resource "keycloak_hardcoded_role_identity_provider_mapper" "oidc" { realm = keycloak_realm.test.id name = "hardcodedRole" identity_provider_alias = keycloak_oidc_identity_provider.oidc.alias @@ -694,7 +707,7 @@ resource keycloak_hardcoded_role_identity_provider_mapper oidc { } } -resource keycloak_hardcoded_attribute_identity_provider_mapper oidc { +resource "keycloak_hardcoded_attribute_identity_provider_mapper" "oidc" { realm = keycloak_realm.test.id name = "hardcodedUserSessionAttribute" identity_provider_alias = keycloak_oidc_identity_provider.oidc.alias @@ -708,7 +721,7 @@ resource keycloak_hardcoded_attribute_identity_provider_mapper oidc { } } -resource keycloak_saml_identity_provider saml { +resource "keycloak_saml_identity_provider" "saml" { realm = keycloak_realm.test.id alias = "saml" entity_id = "https://example.com/entity_id" @@ -717,7 +730,7 @@ resource keycloak_saml_identity_provider saml { gui_order = 3 } -resource keycloak_attribute_importer_identity_provider_mapper saml { +resource "keycloak_attribute_importer_identity_provider_mapper" "saml" { realm = keycloak_realm.test.id name = "Attribute: email" attribute_name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" @@ -730,7 +743,7 @@ resource keycloak_attribute_importer_identity_provider_mapper saml { } } -resource keycloak_attribute_to_role_identity_provider_mapper saml { +resource "keycloak_attribute_to_role_identity_provider_mapper" "saml" { realm = keycloak_realm.test.id name = "attributeToRole" attribute_name = "upn" @@ -744,7 +757,7 @@ resource keycloak_attribute_to_role_identity_provider_mapper saml { } } -resource keycloak_user_template_importer_identity_provider_mapper saml { +resource "keycloak_user_template_importer_identity_provider_mapper" "saml" { realm = keycloak_realm.test.id name = "userTemplate" identity_provider_alias = keycloak_saml_identity_provider.saml.alias @@ -756,7 +769,7 @@ resource keycloak_user_template_importer_identity_provider_mapper saml { } } -resource keycloak_hardcoded_role_identity_provider_mapper saml { +resource "keycloak_hardcoded_role_identity_provider_mapper" "saml" { realm = keycloak_realm.test.id name = "hardcodedRole" identity_provider_alias = keycloak_saml_identity_provider.saml.alias @@ -768,7 +781,7 @@ resource keycloak_hardcoded_role_identity_provider_mapper saml { } } -resource keycloak_hardcoded_attribute_identity_provider_mapper saml { +resource "keycloak_hardcoded_attribute_identity_provider_mapper" "saml" { realm = keycloak_realm.test.id name = "hardcodedAttribute" identity_provider_alias = keycloak_saml_identity_provider.saml.alias @@ -782,7 +795,7 @@ resource keycloak_hardcoded_attribute_identity_provider_mapper saml { } } -resource keycloak_saml_identity_provider saml_custom { +resource "keycloak_saml_identity_provider" "saml_custom" { realm = keycloak_realm.test.id alias = "custom_saml" provider_id = "saml" @@ -790,7 +803,7 @@ resource keycloak_saml_identity_provider saml_custom { single_sign_on_service_url = "https://example.com/auth" sync_mode = "FORCE" gui_order = 4 - extra_config = { + extra_config = { mycustomAttribute = "aValue" } } @@ -828,7 +841,7 @@ resource "keycloak_openid_client" "test_client_auth" { client_secret = "secret" } -resource keycloak_openid_client test_open_id_client_with_consent_text { +resource "keycloak_openid_client" "test_open_id_client_with_consent_text" { client_id = "test_open_id_client_with_consent_text" name = "test_open_id_client_with_consent_text" realm_id = keycloak_realm.test.id @@ -941,7 +954,7 @@ resource "keycloak_authentication_execution" "browser-copy-cookie" { parent_flow_alias = keycloak_authentication_flow.browser-copy-flow.alias authenticator = "auth-cookie" requirement = "ALTERNATIVE" - depends_on = [ + depends_on = [ keycloak_authentication_execution.browser-copy-kerberos ] } @@ -958,7 +971,7 @@ resource "keycloak_authentication_execution" "browser-copy-idp-redirect" { parent_flow_alias = keycloak_authentication_flow.browser-copy-flow.alias authenticator = "identity-provider-redirector" requirement = "ALTERNATIVE" - depends_on = [ + depends_on = [ keycloak_authentication_execution.browser-copy-cookie ] } @@ -968,7 +981,7 @@ resource "keycloak_authentication_subflow" "browser-copy-flow-forms" { parent_flow_alias = keycloak_authentication_flow.browser-copy-flow.alias alias = "browser-copy-flow-forms" requirement = "ALTERNATIVE" - depends_on = [ + depends_on = [ keycloak_authentication_execution.browser-copy-idp-redirect ] } @@ -985,7 +998,7 @@ resource "keycloak_authentication_execution" "browser-copy-otp" { parent_flow_alias = keycloak_authentication_subflow.browser-copy-flow-forms.alias authenticator = "auth-otp-form" requirement = "REQUIRED" - depends_on = [ + depends_on = [ keycloak_authentication_execution.browser-copy-auth-username-password-form ] } @@ -994,7 +1007,7 @@ resource "keycloak_authentication_execution_config" "config" { realm_id = keycloak_realm.test.id execution_id = keycloak_authentication_execution.browser-copy-idp-redirect.id alias = "idp-XXX-config" - config = { + config = { defaultProvider = "idp-XXX" } } @@ -1036,7 +1049,7 @@ resource "keycloak_realm_user_profile" "userprofile" { } validator { - name = "pattern" + name = "pattern" config = { pattern = "^[a-z]+$" error_message = "Nope" diff --git a/keycloak/authentication_subflow.go b/keycloak/authentication_subflow.go index f6b9ee0d..593facb9 100644 --- a/keycloak/authentication_subflow.go +++ b/keycloak/authentication_subflow.go @@ -21,7 +21,7 @@ type AuthenticationSubFlow struct { Requirement string `json:"-"` } -//each subflow creates a flow and an execution under the covers +// each subflow creates a flow and an execution under the covers type authenticationSubFlowCreate struct { Alias string `json:"alias"` Type string `json:"type"` //providerId of the flow diff --git a/keycloak/group.go b/keycloak/group.go index b8af96b2..30d4170a 100644 --- a/keycloak/group.go +++ b/keycloak/group.go @@ -167,8 +167,8 @@ func (keycloakClient *KeycloakClient) GetGroupByName(ctx context.Context, realmI } /* - Find group by name in groups returned by /groups?search=${group_name} - If there are multiple groups match the name, it will return the first one it found, using DFS algorithm +Find group by name in groups returned by /groups?search=${group_name} +If there are multiple groups match the name, it will return the first one it found, using DFS algorithm */ func getGroupByDFS(groupName string, groups []*Group) *Group { for _, group := range groups { diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go index 75e13553..98546957 100644 --- a/keycloak/keycloak_client.go +++ b/keycloak/keycloak_client.go @@ -284,7 +284,8 @@ func (keycloakClient *KeycloakClient) addRequestHeaders(request *http.Request) { } } -/** +/* +* Sends an HTTP request and refreshes credentials on 403 or 401 errors */ func (keycloakClient *KeycloakClient) sendRequest(ctx context.Context, request *http.Request, body []byte) ([]byte, string, error) { diff --git a/keycloak/ldap_user_attribute_mapper.go b/keycloak/ldap_user_attribute_mapper.go index f44f8fa6..b5db317b 100644 --- a/keycloak/ldap_user_attribute_mapper.go +++ b/keycloak/ldap_user_attribute_mapper.go @@ -17,6 +17,8 @@ type LdapUserAttributeMapper struct { ReadOnly bool AlwaysReadValueFromLdap bool UserModelAttribute string + AttributeDefaultValue string + IsBinaryAttribute bool } func convertFromLdapUserAttributeMapperToComponent(ldapUserAttributeMapper *LdapUserAttributeMapper) *component { @@ -42,6 +44,12 @@ func convertFromLdapUserAttributeMapperToComponent(ldapUserAttributeMapper *Ldap "user.model.attribute": { ldapUserAttributeMapper.UserModelAttribute, }, + "attribute.default.value": { + ldapUserAttributeMapper.AttributeDefaultValue, + }, + "is.binary.attribute": { + strconv.FormatBool(ldapUserAttributeMapper.IsBinaryAttribute), + }, }, } } @@ -62,6 +70,11 @@ func convertFromComponentToLdapUserAttributeMapper(component *component, realmId return nil, err } + isBinaryAttribute, err := parseBoolAndTreatEmptyStringAsFalse(component.getConfig("is.binary.attribute")) + if err != nil { + return nil, err + } + return &LdapUserAttributeMapper{ Id: component.Id, Name: component.Name, @@ -73,6 +86,8 @@ func convertFromComponentToLdapUserAttributeMapper(component *component, realmId ReadOnly: readOnly, AlwaysReadValueFromLdap: alwaysReadValueFromLdap, UserModelAttribute: component.getConfig("user.model.attribute"), + AttributeDefaultValue: component.getConfig("attribute.default.value"), + IsBinaryAttribute: isBinaryAttribute, }, nil } diff --git a/makefile b/makefile index fadf63b3..5ca84a10 100644 --- a/makefile +++ b/makefile @@ -1,18 +1,18 @@ GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) - -MAKEFLAGS += --silent +GOOS?=darwin +GOARCH?=amd64 build: go build -o terraform-provider-keycloak build-example: build - mkdir -p example/.terraform/plugins/terraform.local/mrparkers/keycloak/3.0.0/darwin_amd64 - mkdir -p example/terraform.d/plugins/terraform.local/mrparkers/keycloak/3.0.0/darwin_amd64 - cp terraform-provider-keycloak example/.terraform/plugins/terraform.local/mrparkers/keycloak/3.0.0/darwin_amd64/ - cp terraform-provider-keycloak example/terraform.d/plugins/terraform.local/mrparkers/keycloak/3.0.0/darwin_amd64/ + mkdir -p example/.terraform/plugins/terraform.local/mrparkers/keycloak/3.0.0/$(GOOS)_$(GOARCH) + mkdir -p example/terraform.d/plugins/terraform.local/mrparkers/keycloak/3.0.0/$(GOOS)_$(GOARCH) + cp terraform-provider-keycloak example/.terraform/plugins/terraform.local/mrparkers/keycloak/3.0.0/$(GOOS)_$(GOARCH)/ + cp terraform-provider-keycloak example/terraform.d/plugins/terraform.local/mrparkers/keycloak/3.0.0/$(GOOS)_$(GOARCH)/ local: deps - docker-compose up --build -d + docker compose up --build -d ./scripts/wait-for-local-keycloak.sh ./scripts/create-terraform-client.sh diff --git a/provider/resource_keycloak_ldap_user_attribute_mapper.go b/provider/resource_keycloak_ldap_user_attribute_mapper.go index 49625ca7..ea22d343 100644 --- a/provider/resource_keycloak_ldap_user_attribute_mapper.go +++ b/provider/resource_keycloak_ldap_user_attribute_mapper.go @@ -2,6 +2,7 @@ package provider import ( "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mrparkers/terraform-provider-keycloak/keycloak" @@ -63,6 +64,18 @@ func resourceKeycloakLdapUserAttributeMapper() *schema.Resource { Default: false, Description: "When true, this attribute must exist in LDAP.", }, + "attribute_default_value": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Default value to set in LDAP if is_mandatory_in_ldap and the value is empty", + }, + "is_binary_attribute": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Should be true for binary LDAP attributes", + }, }, } } @@ -80,6 +93,8 @@ func getLdapUserAttributeMapperFromData(data *schema.ResourceData) *keycloak.Lda ReadOnly: data.Get("read_only").(bool), AlwaysReadValueFromLdap: data.Get("always_read_value_from_ldap").(bool), IsMandatoryInLdap: data.Get("is_mandatory_in_ldap").(bool), + AttributeDefaultValue: data.Get("attribute_default_value").(string), + IsBinaryAttribute: data.Get("is_binary_attribute").(bool), } } @@ -96,6 +111,8 @@ func setLdapUserAttributeMapperData(data *schema.ResourceData, ldapUserAttribute data.Set("read_only", ldapUserAttributeMapper.ReadOnly) data.Set("always_read_value_from_ldap", ldapUserAttributeMapper.AlwaysReadValueFromLdap) data.Set("is_mandatory_in_ldap", ldapUserAttributeMapper.IsMandatoryInLdap) + data.Set("attribute_default_value", ldapUserAttributeMapper.AttributeDefaultValue) + data.Set("is_binary_attribute", ldapUserAttributeMapper.IsBinaryAttribute) } func resourceKeycloakLdapUserAttributeMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/provider/resource_keycloak_ldap_user_attribute_mapper_test.go b/provider/resource_keycloak_ldap_user_attribute_mapper_test.go index f87b0cfa..4e5ec0b7 100644 --- a/provider/resource_keycloak_ldap_user_attribute_mapper_test.go +++ b/provider/resource_keycloak_ldap_user_attribute_mapper_test.go @@ -2,11 +2,12 @@ package provider import ( "fmt" + "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/mrparkers/terraform-provider-keycloak/keycloak" - "testing" ) func TestAccKeycloakLdapUserAttributeMapper_basic(t *testing.T) { @@ -95,6 +96,8 @@ func TestAccKeycloakLdapUserAttributeMapper_updateInPlace(t *testing.T) { IsMandatoryInLdap: randomBool(), ReadOnly: randomBool(), AlwaysReadValueFromLdap: randomBool(), + AttributeDefaultValue: acctest.RandString(10), + IsBinaryAttribute: randomBool(), } userAttributeMapperAfter := &keycloak.LdapUserAttributeMapper{ Name: acctest.RandString(10), @@ -103,6 +106,8 @@ func TestAccKeycloakLdapUserAttributeMapper_updateInPlace(t *testing.T) { IsMandatoryInLdap: randomBool(), ReadOnly: randomBool(), AlwaysReadValueFromLdap: randomBool(), + AttributeDefaultValue: acctest.RandString(10), + IsBinaryAttribute: randomBool(), } resource.Test(t, resource.TestCase{ @@ -256,8 +261,11 @@ resource "keycloak_ldap_user_attribute_mapper" "username" { read_only = %t always_read_value_from_ldap = %t is_mandatory_in_ldap = %t + attribute_default_value = "%s" + is_binary_attribute = %t } - `, testAccRealmUserFederation.Realm, mapper.Name, mapper.UserModelAttribute, mapper.LdapAttribute, mapper.ReadOnly, mapper.AlwaysReadValueFromLdap, mapper.IsMandatoryInLdap) + `, testAccRealmUserFederation.Realm, mapper.Name, mapper.UserModelAttribute, mapper.LdapAttribute, mapper.ReadOnly, mapper.AlwaysReadValueFromLdap, mapper.IsMandatoryInLdap, + mapper.AttributeDefaultValue, mapper.IsBinaryAttribute) } func testKeycloakLdapUserAttributeMapper_updateLdapUserFederationBefore(userAttributeMapperName string) string {