Skip to content

Commit 07915d1

Browse files
new resource: keycloak_ldap_custom_mapper (#863)
1 parent 1cb7d39 commit 07915d1

6 files changed

+704
-0
lines changed

docs/resources/ldap_custom_mapper.md

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
page_title: "keycloak_ldap_custom_mapper Resource"
3+
---
4+
5+
# keycloak\_ldap\_custom\_mapper Resource
6+
7+
Allows for creating and managing custom attribute mappers for Keycloak users federated via LDAP.
8+
9+
The LDAP custom mapper is implemented and deployed into Keycloak as a custom provider. This resource allows to
10+
specify the custom id and custom implementation class of the self-implemented attribute mapper as well as additional
11+
properties via config map.
12+
13+
The custom mapper should already be deployed into keycloak in order to be correctly configured.
14+
15+
## Example Usage
16+
17+
```hcl
18+
resource "keycloak_realm" "realm" {
19+
realm = "my-realm"
20+
enabled = true
21+
}
22+
23+
resource "keycloak_ldap_user_federation" "ldap_user_federation" {
24+
name = "openldap"
25+
realm_id = keycloak_realm.realm.id
26+
27+
username_ldap_attribute = "cn"
28+
rdn_ldap_attribute = "cn"
29+
uuid_ldap_attribute = "entryDN"
30+
user_object_classes = [
31+
"simpleSecurityObject",
32+
"organizationalRole"
33+
]
34+
35+
connection_url = "ldap://openldap"
36+
users_dn = "dc=example,dc=org"
37+
bind_dn = "cn=admin,dc=example,dc=org"
38+
bind_credential = "admin"
39+
}
40+
41+
resource "keycloak_ldap_custom_mapper" "custom_mapper" {
42+
name = "custom-mapper"
43+
realm_id = keycloak_ldap_user_federation.openldap.realm_id
44+
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
45+
46+
provider_id = "custom-provider-registered-in-keycloak"
47+
provider_type = "com.example.custom.ldap.mappers.CustomMapper"
48+
49+
config = {
50+
"attribute.name" = "name"
51+
"attribute.value" = "value"
52+
}
53+
}
54+
```
55+
56+
## Argument Reference
57+
58+
- `realm_id` - (Required) The realm that this LDAP mapper will exist in.
59+
- `ldap_user_federation_id` - (Required) The ID of the LDAP user federation provider to attach this mapper to.
60+
- `name` - (Required) Display name of this mapper when displayed in the console.
61+
- `provider_id` - (Required) The id of the LDAP mapper implemented in MapperFactory.
62+
- `provider_type` - (Required) The fully-qualified Java class name of the custom LDAP mapper.
63+
- `config` - (Optional) A map with key / value pairs for configuring the LDAP mapper. The supported keys depend on the protocol mapper.
64+
65+
## Import
66+
67+
LDAP mappers can be imported using the format `{{realm_id}}/{{ldap_user_federation_id}}/{{ldap_mapper_id}}`.
68+
The ID of the LDAP user federation provider and the mapper can be found within the Keycloak GUI, and they are typically GUIDs.
69+
70+
Example:
71+
72+
```bash
73+
$ terraform import keycloak_ldap_custom_mapper.custom_mapper my-realm/af2a6ca3-e4d7-49c3-b08b-1b3c70b4b860/3d923ece-1a91-4bf7-adaf-3b82f2a12b67
74+
```

example/main.tf

+23
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,29 @@ resource "keycloak_ldap_full_name_mapper" "full_name_mapper" {
426426
read_only = true
427427
}
428428

429+
resource "keycloak_ldap_custom_mapper" "custom_mapper" {
430+
name = "custom-mapper"
431+
realm_id = keycloak_ldap_user_federation.openldap.realm_id
432+
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
433+
434+
provider_id = "msad-user-account-control-mapper"
435+
provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
436+
}
437+
438+
resource "keycloak_ldap_custom_mapper" "custom_mapper_with_config" {
439+
name = "custom-mapper-with-config"
440+
realm_id = keycloak_ldap_user_federation.openldap.realm_id
441+
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
442+
443+
provider_id = "user-attribute-ldap-mapper"
444+
provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
445+
config = {
446+
"user.model.attribute" = "username"
447+
"ldap.attribute" = "cn"
448+
}
449+
}
450+
451+
429452
resource "keycloak_custom_user_federation" "custom" {
430453
name = "custom1"
431454
realm_id = "master"

keycloak/ldap_custom_mapper.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package keycloak
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
type LdapCustomMapper struct {
9+
Id string
10+
Name string
11+
RealmId string
12+
LdapUserFederationId string
13+
ProviderId string
14+
ProviderType string
15+
Config map[string]string
16+
}
17+
18+
func convertFromLdapCustomMapperToComponent(ldapCustomMapper *LdapCustomMapper) *component {
19+
return &component{
20+
Id: ldapCustomMapper.Id,
21+
Name: ldapCustomMapper.Name,
22+
ProviderId: ldapCustomMapper.ProviderId,
23+
ProviderType: ldapCustomMapper.ProviderType,
24+
ParentId: ldapCustomMapper.LdapUserFederationId,
25+
Config: convertToComponentConfig(ldapCustomMapper.Config),
26+
}
27+
}
28+
29+
func convertFromComponentToLdapCustomMapper(component *component, realmId string) (*LdapCustomMapper, error) {
30+
return &LdapCustomMapper{
31+
Id: component.Id,
32+
Name: component.Name,
33+
RealmId: realmId,
34+
LdapUserFederationId: component.ParentId,
35+
ProviderId: component.ProviderId,
36+
ProviderType: component.ProviderType,
37+
Config: convertFromComponentConfig(component.Config),
38+
}, nil
39+
}
40+
41+
func convertFromComponentConfig(originalMap map[string][]string) map[string]string {
42+
convertedMap := make(map[string]string)
43+
44+
for key, values := range originalMap {
45+
convertedMap[key] = values[0]
46+
}
47+
48+
return convertedMap
49+
}
50+
51+
func convertToComponentConfig(originalMap map[string]string) map[string][]string {
52+
convertedMap := make(map[string][]string)
53+
54+
for key, value := range originalMap {
55+
convertedMap[key] = []string{value}
56+
}
57+
58+
return convertedMap
59+
}
60+
61+
func (keycloakClient *KeycloakClient) NewLdapCustomMapper(ctx context.Context, ldapCustomMapper *LdapCustomMapper) error {
62+
_, location, err := keycloakClient.post(ctx, fmt.Sprintf("/realms/%s/components", ldapCustomMapper.RealmId), convertFromLdapCustomMapperToComponent(ldapCustomMapper))
63+
if err != nil {
64+
return err
65+
}
66+
67+
ldapCustomMapper.Id = getIdFromLocationHeader(location)
68+
69+
return nil
70+
}
71+
72+
func (keycloakClient *KeycloakClient) GetLdapCustomMapper(ctx context.Context, realmId, id string) (*LdapCustomMapper, error) {
73+
var component *component
74+
75+
err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/components/%s", realmId, id), &component, nil)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
return convertFromComponentToLdapCustomMapper(component, realmId)
81+
}
82+
83+
func (keycloakClient *KeycloakClient) UpdateLdapCustomMapper(ctx context.Context, ldapCustomMapper *LdapCustomMapper) error {
84+
return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/components/%s", ldapCustomMapper.RealmId, ldapCustomMapper.Id), convertFromLdapCustomMapperToComponent(ldapCustomMapper))
85+
}
86+
87+
func (keycloakClient *KeycloakClient) DeleteLdapCustomMapper(ctx context.Context, realmId, id string) error {
88+
return keycloakClient.DeleteComponent(ctx, realmId, id)
89+
}

provider/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
5959
"keycloak_ldap_msad_user_account_control_mapper": resourceKeycloakLdapMsadUserAccountControlMapper(),
6060
"keycloak_ldap_msad_lds_user_account_control_mapper": resourceKeycloakLdapMsadLdsUserAccountControlMapper(),
6161
"keycloak_ldap_full_name_mapper": resourceKeycloakLdapFullNameMapper(),
62+
"keycloak_ldap_custom_mapper": resourceKeycloakLdapCustomMapper(),
6263
"keycloak_custom_user_federation": resourceKeycloakCustomUserFederation(),
6364
"keycloak_openid_user_attribute_protocol_mapper": resourceKeycloakOpenIdUserAttributeProtocolMapper(),
6465
"keycloak_openid_user_property_protocol_mapper": resourceKeycloakOpenIdUserPropertyProtocolMapper(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
9+
)
10+
11+
func resourceKeycloakLdapCustomMapper() *schema.Resource {
12+
return &schema.Resource{
13+
CreateContext: resourceKeycloakLdapCustomMapperCreate,
14+
ReadContext: resourceKeycloakLdapCustomMapperRead,
15+
UpdateContext: resourceKeycloakLdapCustomMapperUpdate,
16+
DeleteContext: resourceKeycloakLdapCustomMapperDelete,
17+
// This resource can be imported using {{realm}}/{{provider_id}}/{{mapper_id}}. The Provider and Mapper IDs are displayed in the GUI
18+
Importer: &schema.ResourceImporter{
19+
StateContext: resourceKeycloakLdapGenericMapperImport,
20+
},
21+
Schema: map[string]*schema.Schema{
22+
"name": {
23+
Type: schema.TypeString,
24+
Required: true,
25+
Description: "Display name of the mapper when displayed in the console.",
26+
},
27+
"realm_id": {
28+
Type: schema.TypeString,
29+
Required: true,
30+
ForceNew: true,
31+
Description: "The realm in which the ldap user federation provider exists.",
32+
},
33+
"ldap_user_federation_id": {
34+
Type: schema.TypeString,
35+
Required: true,
36+
ForceNew: true,
37+
Description: "The ldap user federation provider to attach this mapper to.",
38+
},
39+
"provider_id": {
40+
Type: schema.TypeString,
41+
Required: true,
42+
ForceNew: true,
43+
Description: "ID of the custom LDAP mapper.",
44+
},
45+
"provider_type": {
46+
Type: schema.TypeString,
47+
Required: true,
48+
ForceNew: true,
49+
Description: "Fully-qualified name of the Java class implementing the custom LDAP mapper.",
50+
},
51+
"config": {
52+
Type: schema.TypeMap,
53+
Optional: true,
54+
},
55+
},
56+
}
57+
}
58+
59+
func getLdapCustomMapperFromData(data *schema.ResourceData) *keycloak.LdapCustomMapper {
60+
config := make(map[string]string)
61+
if v, ok := data.GetOk("config"); ok {
62+
for key, value := range v.(map[string]interface{}) {
63+
config[key] = value.(string)
64+
}
65+
}
66+
return &keycloak.LdapCustomMapper{
67+
Id: data.Id(),
68+
Name: data.Get("name").(string),
69+
RealmId: data.Get("realm_id").(string),
70+
LdapUserFederationId: data.Get("ldap_user_federation_id").(string),
71+
ProviderId: data.Get("provider_id").(string),
72+
ProviderType: data.Get("provider_type").(string),
73+
Config: config,
74+
}
75+
}
76+
77+
func setLdapCustomMapperData(data *schema.ResourceData, ldapCustomMapper *keycloak.LdapCustomMapper) {
78+
data.SetId(ldapCustomMapper.Id)
79+
80+
data.Set("name", ldapCustomMapper.Name)
81+
data.Set("realm_id", ldapCustomMapper.RealmId)
82+
data.Set("ldap_user_federation_id", ldapCustomMapper.LdapUserFederationId)
83+
84+
data.Set("provider_id", ldapCustomMapper.ProviderId)
85+
data.Set("provider_type", ldapCustomMapper.ProviderType)
86+
data.Set("config", ldapCustomMapper.Config)
87+
}
88+
89+
func resourceKeycloakLdapCustomMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
90+
keycloakClient := meta.(*keycloak.KeycloakClient)
91+
92+
ldapCustomMapper := getLdapCustomMapperFromData(data)
93+
94+
err := keycloakClient.NewLdapCustomMapper(ctx, ldapCustomMapper)
95+
if err != nil {
96+
return diag.FromErr(err)
97+
}
98+
99+
setLdapCustomMapperData(data, ldapCustomMapper)
100+
101+
return resourceKeycloakLdapCustomMapperRead(ctx, data, meta)
102+
}
103+
104+
func resourceKeycloakLdapCustomMapperRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
105+
keycloakClient := meta.(*keycloak.KeycloakClient)
106+
107+
realmId := data.Get("realm_id").(string)
108+
id := data.Id()
109+
110+
ldapCustomMapper, err := keycloakClient.GetLdapCustomMapper(ctx, realmId, id)
111+
if err != nil {
112+
return handleNotFoundError(ctx, err, data)
113+
}
114+
115+
setLdapCustomMapperData(data, ldapCustomMapper)
116+
117+
return nil
118+
}
119+
120+
func resourceKeycloakLdapCustomMapperUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
121+
keycloakClient := meta.(*keycloak.KeycloakClient)
122+
123+
ldapCustomMapper := getLdapCustomMapperFromData(data)
124+
125+
err := keycloakClient.UpdateLdapCustomMapper(ctx, ldapCustomMapper)
126+
if err != nil {
127+
return diag.FromErr(err)
128+
}
129+
130+
setLdapCustomMapperData(data, ldapCustomMapper)
131+
132+
return nil
133+
}
134+
135+
func resourceKeycloakLdapCustomMapperDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
136+
keycloakClient := meta.(*keycloak.KeycloakClient)
137+
138+
realmId := data.Get("realm_id").(string)
139+
id := data.Id()
140+
141+
return diag.FromErr(keycloakClient.DeleteLdapCustomMapper(ctx, realmId, id))
142+
}

0 commit comments

Comments
 (0)