From df1ec803c841ee61dc902ca80a733c621a7e75dc Mon Sep 17 00:00:00 2001 From: Natalia Khodiakova Date: Tue, 18 Jul 2023 22:57:51 +0200 Subject: [PATCH 1/2] feat: add ldap custom_ mapper resource (#862) --- docs/resources/ldap_custom_mapper.md | 66 ++++ example/main.tf | 9 + keycloak/ldap_custom_mapper.go | 67 ++++ provider/provider.go | 1 + .../resource_keycloak_ldap_custom_mapper.go | 130 ++++++ ...source_keycloak_ldap_custom_mapper_test.go | 371 ++++++++++++++++++ 6 files changed, 644 insertions(+) create mode 100644 docs/resources/ldap_custom_mapper.md create mode 100644 keycloak/ldap_custom_mapper.go create mode 100644 provider/resource_keycloak_ldap_custom_mapper.go create mode 100644 provider/resource_keycloak_ldap_custom_mapper_test.go diff --git a/docs/resources/ldap_custom_mapper.md b/docs/resources/ldap_custom_mapper.md new file mode 100644 index 000000000..8efb06e27 --- /dev/null +++ b/docs/resources/ldap_custom_mapper.md @@ -0,0 +1,66 @@ +--- +page_title: "keycloak_ldap_custom_mapper Resource" +--- + +# keycloak\_ldap\_custom\_mapper Resource + +Allows for creating and managing custom attribute mappers for Keycloak users federated via LDAP. + +The LDAP custom mapper is implemented and deployed into Keycloak as a custom provider. This resource allows to +specify the custom id and custom implementation class of the self-implemented attribute mapper. +The custom mapper should already be deployed into keycloak in order to be correctly configured. + +## Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_ldap_user_federation" "ldap_user_federation" { + name = "openldap" + realm_id = keycloak_realm.realm.id + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_custom_mapper" "custom_mapper" { + name = "custom-mapper" + realm_id = keycloak_ldap_user_federation.openldap.realm_id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + + provider_id = "custom-provider-registered-in-keycloak" + provider_type = "com.example.custom.ldap.mappers.CustomMapper" +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm that this LDAP mapper will exist in. +- `ldap_user_federation_id` - (Required) The ID of the LDAP user federation provider to attach this mapper to. +- `name` - (Required) Display name of this mapper when displayed in the console. +- `provider_id` - (Required) The id of the LDAP mapper implemented in MapperFactory. +- `provider_type` - (Required) The fully-qualified Java class name of the custom LDAP mapper. + +## Import + +LDAP mappers can be imported using the format `{{realm_id}}/{{ldap_user_federation_id}}/{{ldap_mapper_id}}`. +The ID of the LDAP user federation provider and the mapper can be found within the Keycloak GUI, and they are typically GUIDs. + +Example: + +```bash +$ terraform import keycloak_ldap_custom_mapper.custom_mapper my-realm/af2a6ca3-e4d7-49c3-b08b-1b3c70b4b860/3d923ece-1a91-4bf7-adaf-3b82f2a12b67 +``` diff --git a/example/main.tf b/example/main.tf index 78cc274a0..d113b4b96 100644 --- a/example/main.tf +++ b/example/main.tf @@ -426,6 +426,15 @@ resource "keycloak_ldap_full_name_mapper" "full_name_mapper" { read_only = true } +resource "keycloak_ldap_custom_mapper" "custom_mapper" { + name = "custom-mapper" + realm_id = keycloak_ldap_user_federation.openldap.realm_id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + + provider_id = "msad-user-account-control-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" +} + resource "keycloak_custom_user_federation" "custom" { name = "custom1" realm_id = "master" diff --git a/keycloak/ldap_custom_mapper.go b/keycloak/ldap_custom_mapper.go new file mode 100644 index 000000000..0150bf401 --- /dev/null +++ b/keycloak/ldap_custom_mapper.go @@ -0,0 +1,67 @@ +package keycloak + +import ( + "context" + "fmt" +) + +type LdapCustomMapper struct { + Id string + Name string + RealmId string + LdapUserFederationId string + ProviderId string + ProviderType string +} + +func convertFromLdapCustomMapperToComponent(ldapCustomMapper *LdapCustomMapper) *component { + return &component{ + Id: ldapCustomMapper.Id, + Name: ldapCustomMapper.Name, + ProviderId: ldapCustomMapper.ProviderId, + ProviderType: ldapCustomMapper.ProviderType, + ParentId: ldapCustomMapper.LdapUserFederationId, + Config: map[string][]string{}, + } +} + +func convertFromComponentToLdapCustomMapper(component *component, realmId string) (*LdapCustomMapper, error) { + return &LdapCustomMapper{ + Id: component.Id, + Name: component.Name, + RealmId: realmId, + LdapUserFederationId: component.ParentId, + ProviderId: component.ProviderId, + ProviderType: component.ProviderType, + }, nil +} + +func (keycloakClient *KeycloakClient) NewLdapCustomMapper(ctx context.Context, ldapCustomMapper *LdapCustomMapper) error { + _, location, err := keycloakClient.post(ctx, fmt.Sprintf("/realms/%s/components", ldapCustomMapper.RealmId), convertFromLdapCustomMapperToComponent(ldapCustomMapper)) + if err != nil { + return err + } + + ldapCustomMapper.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) GetLdapCustomMapper(ctx context.Context, realmId, id string) (*LdapCustomMapper, error) { + var component *component + + err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/components/%s", realmId, id), &component, nil) + if err != nil { + return nil, err + } + + return convertFromComponentToLdapCustomMapper(component, realmId) +} + +func (keycloakClient *KeycloakClient) UpdateLdapCustomMapper(ctx context.Context, ldapCustomMapper *LdapCustomMapper) error { + return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/components/%s", ldapCustomMapper.RealmId, ldapCustomMapper.Id), convertFromLdapCustomMapperToComponent(ldapCustomMapper)) +} + +func (keycloakClient *KeycloakClient) DeleteLdapCustomMapper(ctx context.Context, realmId, id string) error { + return keycloakClient.DeleteComponent(ctx, realmId, id) +} diff --git a/provider/provider.go b/provider/provider.go index 941be2bdb..00e08e889 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -59,6 +59,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_ldap_msad_user_account_control_mapper": resourceKeycloakLdapMsadUserAccountControlMapper(), "keycloak_ldap_msad_lds_user_account_control_mapper": resourceKeycloakLdapMsadLdsUserAccountControlMapper(), "keycloak_ldap_full_name_mapper": resourceKeycloakLdapFullNameMapper(), + "keycloak_ldap_custom_mapper": resourceKeycloakLdapCustomMapper(), "keycloak_custom_user_federation": resourceKeycloakCustomUserFederation(), "keycloak_openid_user_attribute_protocol_mapper": resourceKeycloakOpenIdUserAttributeProtocolMapper(), "keycloak_openid_user_property_protocol_mapper": resourceKeycloakOpenIdUserPropertyProtocolMapper(), diff --git a/provider/resource_keycloak_ldap_custom_mapper.go b/provider/resource_keycloak_ldap_custom_mapper.go new file mode 100644 index 000000000..7d065f5d4 --- /dev/null +++ b/provider/resource_keycloak_ldap_custom_mapper.go @@ -0,0 +1,130 @@ +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" +) + +func resourceKeycloakLdapCustomMapper() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeycloakLdapCustomMapperCreate, + ReadContext: resourceKeycloakLdapCustomMapperRead, + UpdateContext: resourceKeycloakLdapCustomMapperUpdate, + DeleteContext: resourceKeycloakLdapCustomMapperDelete, + // This resource can be imported using {{realm}}/{{provider_id}}/{{mapper_id}}. The Provider and Mapper IDs are displayed in the GUI + Importer: &schema.ResourceImporter{ + StateContext: resourceKeycloakLdapGenericMapperImport, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Display name of the mapper when displayed in the console.", + }, + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The realm in which the ldap user federation provider exists.", + }, + "ldap_user_federation_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The ldap user federation provider to attach this mapper to.", + }, + "provider_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the custom LDAP mapper.", + }, + "provider_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Fully-qualified name of the Java class implementing the custom LDAP mapper.", + }, + }, + } +} + +func getLdapCustomMapperFromData(data *schema.ResourceData) *keycloak.LdapCustomMapper { + return &keycloak.LdapCustomMapper{ + Id: data.Id(), + Name: data.Get("name").(string), + RealmId: data.Get("realm_id").(string), + LdapUserFederationId: data.Get("ldap_user_federation_id").(string), + ProviderId: data.Get("provider_id").(string), + ProviderType: data.Get("provider_type").(string), + } +} + +func setLdapCustomMapperData(data *schema.ResourceData, ldapCustomMapper *keycloak.LdapCustomMapper) { + data.SetId(ldapCustomMapper.Id) + + data.Set("name", ldapCustomMapper.Name) + data.Set("realm_id", ldapCustomMapper.RealmId) + data.Set("ldap_user_federation_id", ldapCustomMapper.LdapUserFederationId) + + data.Set("provider_id", ldapCustomMapper.ProviderId) + data.Set("provider_type", ldapCustomMapper.ProviderType) +} + +func resourceKeycloakLdapCustomMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + ldapCustomMapper := getLdapCustomMapperFromData(data) + + err := keycloakClient.NewLdapCustomMapper(ctx, ldapCustomMapper) + if err != nil { + return diag.FromErr(err) + } + + setLdapCustomMapperData(data, ldapCustomMapper) + + return resourceKeycloakLdapCustomMapperRead(ctx, data, meta) +} + +func resourceKeycloakLdapCustomMapperRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + ldapCustomMapper, err := keycloakClient.GetLdapCustomMapper(ctx, realmId, id) + if err != nil { + return handleNotFoundError(ctx, err, data) + } + + setLdapCustomMapperData(data, ldapCustomMapper) + + return nil +} + +func resourceKeycloakLdapCustomMapperUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + ldapCustomMapper := getLdapCustomMapperFromData(data) + + err := keycloakClient.UpdateLdapCustomMapper(ctx, ldapCustomMapper) + if err != nil { + return diag.FromErr(err) + } + + setLdapCustomMapperData(data, ldapCustomMapper) + + return nil +} + +func resourceKeycloakLdapCustomMapperDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + return diag.FromErr(keycloakClient.DeleteLdapCustomMapper(ctx, realmId, id)) +} diff --git a/provider/resource_keycloak_ldap_custom_mapper_test.go b/provider/resource_keycloak_ldap_custom_mapper_test.go new file mode 100644 index 000000000..9d42fc98c --- /dev/null +++ b/provider/resource_keycloak_ldap_custom_mapper_test.go @@ -0,0 +1,371 @@ +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" +) + +func TestAccKeycloakLdapCustomMapper_basic(t *testing.T) { + t.Parallel() + + customMapperName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapCustomMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapCustomMapper_basic(customMapperName), + Check: testAccCheckKeycloakLdapCustomMapperExists("keycloak_ldap_custom_mapper.sample_mapper"), + }, + { + ResourceName: "keycloak_ldap_custom_mapper.sample_mapper", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getLdapGenericMapperImportId("keycloak_ldap_custom_mapper.sample_mapper"), + }, + }, + }) +} + +func TestAccKeycloakLdapCustomMapper_createAfterManualDestroy(t *testing.T) { + t.Parallel() + + var mapper = &keycloak.LdapCustomMapper{} + + customMapperName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapCustomMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapCustomMapper_basic(customMapperName), + Check: testAccCheckKeycloakLdapCustomMapperFetch("keycloak_ldap_custom_mapper.sample_mapper", mapper), + }, + { + PreConfig: func() { + err := keycloakClient.DeleteLdapCustomMapper(testCtx, mapper.RealmId, mapper.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakLdapCustomMapper_basic(customMapperName), + Check: testAccCheckKeycloakLdapCustomMapperExists("keycloak_ldap_custom_mapper.sample_mapper"), + }, + }, + }) +} + +func TestAccKeycloakLdapCustomMapper_updateLdapUserFederation(t *testing.T) { + t.Parallel() + + customMapperName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapCustomMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapCustomMapper_updateLdapUserFederationBefore(customMapperName), + Check: testAccCheckKeycloakLdapCustomMapperExists("keycloak_ldap_custom_mapper.sample_mapper"), + }, + { + Config: testKeycloakLdapCustomMapper_updateLdapUserFederationAfter(customMapperName), + Check: testAccCheckKeycloakLdapCustomMapperExists("keycloak_ldap_custom_mapper.sample_mapper"), + }, + }, + }) +} + +func TestAccKeycloakLdapCustomMapper_updateInPlace(t *testing.T) { + t.Parallel() + + customMapperBefore := &keycloak.LdapCustomMapper{ + Name: acctest.RandString(10), + ProviderId: "msad-user-account-control-mapper", + ProviderType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + } + customMapperAfter := &keycloak.LdapCustomMapper{ + Name: acctest.RandString(10), + ProviderId: "msad-user-account-control-mapper", + ProviderType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapCustomMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapCustomMapper_basicFromInterface(customMapperBefore), + Check: testAccCheckKeycloakLdapCustomMapperExists("keycloak_ldap_custom_mapper.sample_mapper"), + }, + { + Config: testKeycloakLdapCustomMapper_basicFromInterface(customMapperAfter), + Check: testAccCheckKeycloakLdapCustomMapperExists("keycloak_ldap_custom_mapper.sample_mapper"), + }, + }, + }) +} + +func testAccCheckKeycloakLdapCustomMapperExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getLdapCustomMapperFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakLdapCustomMapperFetch(resourceName string, mapper *keycloak.LdapCustomMapper) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedMapper, err := getLdapCustomMapperFromState(s, resourceName) + if err != nil { + return err + } + + mapper.Id = fetchedMapper.Id + mapper.RealmId = fetchedMapper.RealmId + + return nil + } +} + +func testAccCheckKeycloakLdapCustomMapperDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_ldap_custom_mapper" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + ldapCustomMapper, _ := keycloakClient.GetLdapCustomMapper(testCtx, realm, id) + if ldapCustomMapper != nil { + return fmt.Errorf("ldap user attribute mapper with id %s still exists", id) + } + } + + return nil + } +} + +func getLdapCustomMapperFromState(s *terraform.State, resourceName string) (*keycloak.LdapCustomMapper, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + ldapCustomMapper, err := keycloakClient.GetLdapCustomMapper(testCtx, realm, id) + if err != nil { + return nil, fmt.Errorf("error getting ldap user attribute mapper with id %s: %s", id, err) + } + + return ldapCustomMapper, nil +} + +func testKeycloakLdapCustomMapper_basic(customMapperName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_ldap_user_federation" "openldap" { + name = "openldap" + realm_id = data.keycloak_realm.realm.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_custom_mapper" "sample_mapper" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}" + + provider_id = "msad-user-account-control-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" +} + `, testAccRealmUserFederation.Realm, customMapperName) +} + +func testKeycloakLdapCustomMapper_basicFromInterface(mapper *keycloak.LdapCustomMapper) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_ldap_user_federation" "openldap" { + name = "openldap" + realm_id = data.keycloak_realm.realm.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_custom_mapper" "sample_mapper" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}" + + provider_id = "%s" + provider_type = "%s" + +} + `, testAccRealmUserFederation.Realm, mapper.Name, mapper.ProviderId, mapper.ProviderType) +} + +func testKeycloakLdapCustomMapper_updateLdapUserFederationBefore(customMapperName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm_one" { + realm = "%s" +} + +data "keycloak_realm" "realm_two" { + realm = "%s" +} + +resource "keycloak_ldap_user_federation" "openldap_one" { + name = "openldap" + realm_id = data.keycloak_realm.realm_one.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_user_federation" "openldap_two" { + name = "openldap" + realm_id = data.keycloak_realm.realm_two.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_custom_mapper" "sample_mapper" { + name = "%s" + realm_id = data.keycloak_realm.realm_one.id + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap_one.id}" + + provider_id = "msad-user-account-control-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" +} + `, testAccRealmUserFederation.Realm, testAccRealmTwo.Realm, customMapperName) +} + +func testKeycloakLdapCustomMapper_updateLdapUserFederationAfter(customMapperName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm_one" { + realm = "%s" +} + +data "keycloak_realm" "realm_two" { + realm = "%s" +} + +resource "keycloak_ldap_user_federation" "openldap_one" { + name = "openldap" + realm_id = data.keycloak_realm.realm_one.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_user_federation" "openldap_two" { + name = "openldap" + realm_id = data.keycloak_realm.realm_two.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_custom_mapper" "sample_mapper" { + name = "%s" + realm_id = data.keycloak_realm.realm_two.id + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap_two.id}" + + provider_id = "msad-user-account-control-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" +} + `, testAccRealmUserFederation.Realm, testAccRealmTwo.Realm, customMapperName) +} From 982955c3243e6c010397e68f40d9db16b08cdd8b Mon Sep 17 00:00:00 2001 From: Natalia Khodiakova Date: Mon, 24 Jul 2023 22:43:21 +0200 Subject: [PATCH 2/2] feat: add optional config for ldap custom mapper resource (#862) --- docs/resources/ldap_custom_mapper.md | 10 +++++++- example/main.tf | 14 +++++++++++ keycloak/ldap_custom_mapper.go | 24 ++++++++++++++++++- .../resource_keycloak_ldap_custom_mapper.go | 12 ++++++++++ ...source_keycloak_ldap_custom_mapper_test.go | 6 ++++- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/docs/resources/ldap_custom_mapper.md b/docs/resources/ldap_custom_mapper.md index 8efb06e27..20a84f95f 100644 --- a/docs/resources/ldap_custom_mapper.md +++ b/docs/resources/ldap_custom_mapper.md @@ -7,7 +7,9 @@ page_title: "keycloak_ldap_custom_mapper Resource" Allows for creating and managing custom attribute mappers for Keycloak users federated via LDAP. The LDAP custom mapper is implemented and deployed into Keycloak as a custom provider. This resource allows to -specify the custom id and custom implementation class of the self-implemented attribute mapper. +specify the custom id and custom implementation class of the self-implemented attribute mapper as well as additional +properties via config map. + The custom mapper should already be deployed into keycloak in order to be correctly configured. ## Example Usage @@ -43,6 +45,11 @@ resource "keycloak_ldap_custom_mapper" "custom_mapper" { provider_id = "custom-provider-registered-in-keycloak" provider_type = "com.example.custom.ldap.mappers.CustomMapper" + + config = { + "attribute.name" = "name" + "attribute.value" = "value" + } } ``` @@ -53,6 +60,7 @@ resource "keycloak_ldap_custom_mapper" "custom_mapper" { - `name` - (Required) Display name of this mapper when displayed in the console. - `provider_id` - (Required) The id of the LDAP mapper implemented in MapperFactory. - `provider_type` - (Required) The fully-qualified Java class name of the custom LDAP mapper. +- `config` - (Optional) A map with key / value pairs for configuring the LDAP mapper. The supported keys depend on the protocol mapper. ## Import diff --git a/example/main.tf b/example/main.tf index d113b4b96..ceb8796b0 100644 --- a/example/main.tf +++ b/example/main.tf @@ -435,6 +435,20 @@ resource "keycloak_ldap_custom_mapper" "custom_mapper" { provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" } +resource "keycloak_ldap_custom_mapper" "custom_mapper_with_config" { + name = "custom-mapper-with-config" + realm_id = keycloak_ldap_user_federation.openldap.realm_id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + + provider_id = "user-attribute-ldap-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" + config = { + "user.model.attribute" = "username" + "ldap.attribute" = "cn" + } +} + + resource "keycloak_custom_user_federation" "custom" { name = "custom1" realm_id = "master" diff --git a/keycloak/ldap_custom_mapper.go b/keycloak/ldap_custom_mapper.go index 0150bf401..82725e1e7 100644 --- a/keycloak/ldap_custom_mapper.go +++ b/keycloak/ldap_custom_mapper.go @@ -12,6 +12,7 @@ type LdapCustomMapper struct { LdapUserFederationId string ProviderId string ProviderType string + Config map[string]string } func convertFromLdapCustomMapperToComponent(ldapCustomMapper *LdapCustomMapper) *component { @@ -21,7 +22,7 @@ func convertFromLdapCustomMapperToComponent(ldapCustomMapper *LdapCustomMapper) ProviderId: ldapCustomMapper.ProviderId, ProviderType: ldapCustomMapper.ProviderType, ParentId: ldapCustomMapper.LdapUserFederationId, - Config: map[string][]string{}, + Config: convertToComponentConfig(ldapCustomMapper.Config), } } @@ -33,9 +34,30 @@ func convertFromComponentToLdapCustomMapper(component *component, realmId string LdapUserFederationId: component.ParentId, ProviderId: component.ProviderId, ProviderType: component.ProviderType, + Config: convertFromComponentConfig(component.Config), }, nil } +func convertFromComponentConfig(originalMap map[string][]string) map[string]string { + convertedMap := make(map[string]string) + + for key, values := range originalMap { + convertedMap[key] = values[0] + } + + return convertedMap +} + +func convertToComponentConfig(originalMap map[string]string) map[string][]string { + convertedMap := make(map[string][]string) + + for key, value := range originalMap { + convertedMap[key] = []string{value} + } + + return convertedMap +} + func (keycloakClient *KeycloakClient) NewLdapCustomMapper(ctx context.Context, ldapCustomMapper *LdapCustomMapper) error { _, location, err := keycloakClient.post(ctx, fmt.Sprintf("/realms/%s/components", ldapCustomMapper.RealmId), convertFromLdapCustomMapperToComponent(ldapCustomMapper)) if err != nil { diff --git a/provider/resource_keycloak_ldap_custom_mapper.go b/provider/resource_keycloak_ldap_custom_mapper.go index 7d065f5d4..a1a275053 100644 --- a/provider/resource_keycloak_ldap_custom_mapper.go +++ b/provider/resource_keycloak_ldap_custom_mapper.go @@ -48,11 +48,21 @@ func resourceKeycloakLdapCustomMapper() *schema.Resource { ForceNew: true, Description: "Fully-qualified name of the Java class implementing the custom LDAP mapper.", }, + "config": { + Type: schema.TypeMap, + Optional: true, + }, }, } } func getLdapCustomMapperFromData(data *schema.ResourceData) *keycloak.LdapCustomMapper { + config := make(map[string]string) + if v, ok := data.GetOk("config"); ok { + for key, value := range v.(map[string]interface{}) { + config[key] = value.(string) + } + } return &keycloak.LdapCustomMapper{ Id: data.Id(), Name: data.Get("name").(string), @@ -60,6 +70,7 @@ func getLdapCustomMapperFromData(data *schema.ResourceData) *keycloak.LdapCustom LdapUserFederationId: data.Get("ldap_user_federation_id").(string), ProviderId: data.Get("provider_id").(string), ProviderType: data.Get("provider_type").(string), + Config: config, } } @@ -72,6 +83,7 @@ func setLdapCustomMapperData(data *schema.ResourceData, ldapCustomMapper *keyclo data.Set("provider_id", ldapCustomMapper.ProviderId) data.Set("provider_type", ldapCustomMapper.ProviderType) + data.Set("config", ldapCustomMapper.Config) } func resourceKeycloakLdapCustomMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/provider/resource_keycloak_ldap_custom_mapper_test.go b/provider/resource_keycloak_ldap_custom_mapper_test.go index 9d42fc98c..a72fa1dd0 100644 --- a/provider/resource_keycloak_ldap_custom_mapper_test.go +++ b/provider/resource_keycloak_ldap_custom_mapper_test.go @@ -209,8 +209,12 @@ resource "keycloak_ldap_custom_mapper" "sample_mapper" { realm_id = data.keycloak_realm.realm.id ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}" - provider_id = "msad-user-account-control-mapper" + provider_id = "user-attribute-ldap-mapper" provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" + config = { + "user.model.attribute" = "username" + "ldap.attribute" = "cn" + } } `, testAccRealmUserFederation.Realm, customMapperName) }