Skip to content

Commit

Permalink
new resource: keycloak_ldap_custom_mapper (keycloak#863)
Browse files Browse the repository at this point in the history
  • Loading branch information
NataliaKhodiakova authored Jan 3, 2024
1 parent 1cb7d39 commit 07915d1
Show file tree
Hide file tree
Showing 6 changed files with 704 additions and 0 deletions.
74 changes: 74 additions & 0 deletions docs/resources/ldap_custom_mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
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 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

```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"
config = {
"attribute.name" = "name"
"attribute.value" = "value"
}
}
```

## 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.
- `config` - (Optional) A map with key / value pairs for configuring the LDAP mapper. The supported keys depend on the protocol 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
```
23 changes: 23 additions & 0 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,29 @@ 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_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"
Expand Down
89 changes: 89 additions & 0 deletions keycloak/ldap_custom_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package keycloak

import (
"context"
"fmt"
)

type LdapCustomMapper struct {
Id string
Name string
RealmId string
LdapUserFederationId string
ProviderId string
ProviderType string
Config map[string]string
}

func convertFromLdapCustomMapperToComponent(ldapCustomMapper *LdapCustomMapper) *component {
return &component{
Id: ldapCustomMapper.Id,
Name: ldapCustomMapper.Name,
ProviderId: ldapCustomMapper.ProviderId,
ProviderType: ldapCustomMapper.ProviderType,
ParentId: ldapCustomMapper.LdapUserFederationId,
Config: convertToComponentConfig(ldapCustomMapper.Config),
}
}

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,
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 {
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)
}
1 change: 1 addition & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
142 changes: 142 additions & 0 deletions provider/resource_keycloak_ldap_custom_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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.",
},
"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),
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),
Config: config,
}
}

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)
data.Set("config", ldapCustomMapper.Config)
}

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))
}
Loading

0 comments on commit 07915d1

Please sign in to comment.