Skip to content

Commit

Permalink
new resource: keycloak_saml_user_property_protocol_mapper (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrparkers authored Jan 28, 2019
1 parent c7cdada commit b07251b
Show file tree
Hide file tree
Showing 7 changed files with 652 additions and 0 deletions.
61 changes: 61 additions & 0 deletions docs/resources/keycloak_saml_user_property_protocol_mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# keycloak_saml_user_property_protocol_mapper

Allows for creating and managing user property protocol mappers for
SAML clients within Keycloak.

SAML user property protocol mappers allow you to map properties of the Keycloak
user model to an attribute in a SAML assertion. Protocol mappers
can be defined for a single client, or they can be defined for a client scope which
can be shared between multiple different clients.

### Example Usage (Client)

```hcl
resource "keycloak_realm" "realm" {
realm = "my-realm"
enabled = true
}
resource "keycloak_saml_client" "saml_client" {
realm_id = "${keycloak_realm.test.id}"
client_id = "test-saml-client"
name = "test-saml-client"
}
resource "keycloak_saml_user_property_protocol_mapper" "saml_user_property_mapper" {
realm_id = "${keycloak_realm.test.id}"
client_id = "${keycloak_saml_client.saml_client.id}"
name = "email-user-property-mapper"
user_property = "email"
saml_attribute_name = "email"
saml_attribute_name_format = "Unspecified"
}
```

### Argument Reference

The following arguments are supported:

- `realm_id` - (Required) The realm this protocol mapper exists within.
- One of the following arguments is required:
- `client_id` - The SAML client this protocol mapper is attached to.
- `client_scope_id` - The SAML client scope this protocol mapper is attached to.
- `name` - (Required) The display name of this protocol mapper in the GUI.
- `user_property` - (Required) The property of the Keycloak user model to map.
- `friendly_name` - (Optional) An optional human-friendly name for this attribute.
- `saml_attribute_name` - (Required) The name of the SAML attribute.
- `saml_attribute_name_format` - (Required) The SAML attribute Name Format. Can be one of `Unspecified`, `Basic`, or `URI Reference`.

### Import

Protocol mappers can be imported using one of the following formats:
- Client: `{{realm_id}}/client/{{client_keycloak_id}}/{{protocol_mapper_id}}`
- Client Scope: `{{realm_id}}/client-scope/{{client_scope_keycloak_id}}/{{protocol_mapper_id}}`

Example:

```bash
$ terraform import keycloak_saml_user_property_protocol_mapper.saml_user_property_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4
$ terraform import keycloak_saml_user_property_protocol_mapper.saml_user_property_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4
```
10 changes: 10 additions & 0 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,13 @@ resource "keycloak_saml_user_attribute_protocol_mapper" "saml_user_attribute_map
saml_attribute_name = "saml-attribute-name"
saml_attribute_name_format = "Unspecified"
}

resource "keycloak_saml_user_property_protocol_mapper" "saml_user_property_mapper" {
realm_id = "${keycloak_realm.test.id}"
client_id = "${keycloak_saml_client.saml_client.id}"
name = "test-saml-user-property-mapper"

user_property = "email"
saml_attribute_name = "email"
saml_attribute_name_format = "Unspecified"
}
101 changes: 101 additions & 0 deletions keycloak/saml_user_property_protocol_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package keycloak

import (
"fmt"
)

type SamlUserPropertyProtocolMapper struct {
Id string
Name string
RealmId string
ClientId string
ClientScopeId string

UserProperty string
FriendlyName string
SamlAttributeName string
SamlAttributeNameFormat string
}

func (mapper *SamlUserPropertyProtocolMapper) convertToGenericProtocolMapper() *protocolMapper {
return &protocolMapper{
Id: mapper.Id,
Name: mapper.Name,
Protocol: "saml",
ProtocolMapper: "saml-user-property-mapper",
Config: map[string]string{
attributeNameField: mapper.SamlAttributeName,
attributeNameFormatField: mapper.SamlAttributeNameFormat,
friendlyNameField: mapper.FriendlyName,
userAttributeField: mapper.UserProperty,
},
}
}

func (protocolMapper *protocolMapper) convertToSamlUserPropertyProtocolMapper(realmId, clientId, clientScopeId string) *SamlUserPropertyProtocolMapper {
return &SamlUserPropertyProtocolMapper{
Id: protocolMapper.Id,
Name: protocolMapper.Name,
RealmId: realmId,
ClientId: clientId,
ClientScopeId: clientScopeId,

UserProperty: protocolMapper.Config[userAttributeField],
FriendlyName: protocolMapper.Config[friendlyNameField],
SamlAttributeName: protocolMapper.Config[attributeNameField],
SamlAttributeNameFormat: protocolMapper.Config[attributeNameFormatField],
}
}

func (keycloakClient *KeycloakClient) GetSamlUserPropertyProtocolMapper(realmId, clientId, clientScopeId, mapperId string) (*SamlUserPropertyProtocolMapper, error) {
var protocolMapper *protocolMapper

err := keycloakClient.get(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper)
if err != nil {
return nil, err
}

return protocolMapper.convertToSamlUserPropertyProtocolMapper(realmId, clientId, clientScopeId), nil
}

func (keycloakClient *KeycloakClient) DeleteSamlUserPropertyProtocolMapper(realmId, clientId, clientScopeId, mapperId string) error {
return keycloakClient.delete(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId))
}

func (keycloakClient *KeycloakClient) NewSamlUserPropertyProtocolMapper(mapper *SamlUserPropertyProtocolMapper) error {
path := protocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId)

location, err := keycloakClient.post(path, mapper.convertToGenericProtocolMapper())
if err != nil {
return err
}

mapper.Id = getIdFromLocationHeader(location)

return nil
}

func (keycloakClient *KeycloakClient) UpdateSamlUserPropertyProtocolMapper(mapper *SamlUserPropertyProtocolMapper) error {
path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id)

return keycloakClient.put(path, mapper.convertToGenericProtocolMapper())
}

func (keycloakClient *KeycloakClient) ValidateSamlUserPropertyProtocolMapper(mapper *SamlUserPropertyProtocolMapper) error {
if mapper.ClientId == "" && mapper.ClientScopeId == "" {
return fmt.Errorf("validation error: one of ClientId or ClientScopeId must be set")
}

protocolMappers, err := keycloakClient.listGenericProtocolMappers(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId)
if err != nil {
return err
}

for _, protocolMapper := range protocolMappers {
if protocolMapper.Name == mapper.Name && protocolMapper.Id != mapper.Id {
return fmt.Errorf("validation error: a protocol mapper with name %s already exists for this client", mapper.Name)
}
}

return nil
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ nav:
- keycloak_openid_full_name_protocol_mapper: resources/keycloak_openid_full_name_protocol_mapper.md
- keycloak_saml_client: resources/keycloak_saml_client.md
- keycloak_saml_user_attribute_protocol_mapper: resources/keycloak_saml_user_attribute_protocol_mapper.md
- keycloak_saml_user_property_protocol_mapper: resources/keycloak_saml_user_property_protocol_mapper.md
- keycloak_ldap_user_federation: resources/keycloak_ldap_user_federation.md
- keycloak_ldap_full_name_mapper: resources/keycloak_ldap_full_name_mapper.md
- keycloak_ldap_group_mapper: resources/keycloak_ldap_group_mapper.md
Expand Down
160 changes: 160 additions & 0 deletions provider/keycloak_saml_user_property_protocol_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package provider

import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
)

func resourceKeycloakSamlUserPropertyProtocolMapper() *schema.Resource {
return &schema.Resource{
Create: resourceKeycloakSamlUserPropertyProtocolMapperCreate,
Read: resourceKeycloakSamlUserPropertyProtocolMapperRead,
Update: resourceKeycloakSamlUserPropertyProtocolMapperUpdate,
Delete: resourceKeycloakSamlUserPropertyProtocolMapperDelete,
Importer: &schema.ResourceImporter{
// import a mapper tied to a client:
// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
// or a client scope:
// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
State: genericProtocolMapperImport,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"realm_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"client_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"client_scope_id"},
},
"client_scope_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"client_id"},
},
"user_property": {
Type: schema.TypeString,
Required: true,
},
"friendly_name": {
Type: schema.TypeString,
Optional: true,
},
"saml_attribute_name": {
Type: schema.TypeString,
Required: true,
},
"saml_attribute_name_format": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice(keycloakSamlUserAttributeProtocolMapperNameFormats, false),
},
},
}
}

func mapFromDataToSamlUserPropertyProtocolMapper(data *schema.ResourceData) *keycloak.SamlUserPropertyProtocolMapper {
return &keycloak.SamlUserPropertyProtocolMapper{
Id: data.Id(),
Name: data.Get("name").(string),
RealmId: data.Get("realm_id").(string),
ClientId: data.Get("client_id").(string),
ClientScopeId: data.Get("client_scope_id").(string),

UserProperty: data.Get("user_property").(string),
FriendlyName: data.Get("friendly_name").(string),
SamlAttributeName: data.Get("saml_attribute_name").(string),
SamlAttributeNameFormat: data.Get("saml_attribute_name_format").(string),
}
}

func mapFromSamlUserPropertyProtocolMapperToData(mapper *keycloak.SamlUserPropertyProtocolMapper, data *schema.ResourceData) {
data.SetId(mapper.Id)
data.Set("name", mapper.Name)
data.Set("realm_id", mapper.RealmId)

if mapper.ClientId != "" {
data.Set("client_id", mapper.ClientId)
} else {
data.Set("client_scope_id", mapper.ClientScopeId)
}

data.Set("user_property", mapper.UserProperty)
data.Set("friendly_name", mapper.FriendlyName)
data.Set("saml_attribute_name", mapper.SamlAttributeName)
data.Set("saml_attribute_name_format", mapper.SamlAttributeNameFormat)
}

func resourceKeycloakSamlUserPropertyProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

samlUserPropertyMapper := mapFromDataToSamlUserPropertyProtocolMapper(data)

err := keycloakClient.ValidateSamlUserPropertyProtocolMapper(samlUserPropertyMapper)
if err != nil {
return err
}

err = keycloakClient.NewSamlUserPropertyProtocolMapper(samlUserPropertyMapper)
if err != nil {
return err
}

mapFromSamlUserPropertyProtocolMapperToData(samlUserPropertyMapper, data)

return resourceKeycloakSamlUserPropertyProtocolMapperRead(data, meta)
}

func resourceKeycloakSamlUserPropertyProtocolMapperRead(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
clientId := data.Get("client_id").(string)
clientScopeId := data.Get("client_scope_id").(string)

samlUserPropertyMapper, err := keycloakClient.GetSamlUserPropertyProtocolMapper(realmId, clientId, clientScopeId, data.Id())
if err != nil {
return handleNotFoundError(err, data)
}

mapFromSamlUserPropertyProtocolMapperToData(samlUserPropertyMapper, data)

return nil
}

func resourceKeycloakSamlUserPropertyProtocolMapperUpdate(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

samlUserPropertyMapper := mapFromDataToSamlUserPropertyProtocolMapper(data)

err := keycloakClient.ValidateSamlUserPropertyProtocolMapper(samlUserPropertyMapper)
if err != nil {
return err
}

err = keycloakClient.UpdateSamlUserPropertyProtocolMapper(samlUserPropertyMapper)
if err != nil {
return err
}

return resourceKeycloakSamlUserPropertyProtocolMapperRead(data, meta)
}

func resourceKeycloakSamlUserPropertyProtocolMapperDelete(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
clientId := data.Get("client_id").(string)
clientScopeId := data.Get("client_scope_id").(string)

return keycloakClient.DeleteSamlUserPropertyProtocolMapper(realmId, clientId, clientScopeId, data.Id())
}
Loading

0 comments on commit b07251b

Please sign in to comment.