Repository: mrparkers/terraform-provider-keycloak
Pull request #84 (2019-01-25 19:55:53)

new resource: keycloak_saml_user_attribute_protocol_mapper


Patch:
diff --git a/docs/keycloak_saml_user_attribute_protocol_mapper.md b/docs/keycloak_saml_user_attribute_protocol_mapper.md
new file mode 100644
index 0000000..a5d47f2
--- /dev/null
+++ b/docs/keycloak_saml_user_attribute_protocol_mapper.md
@@ -0,0 +1,61 @@
+# keycloak_saml_user_attribute_protocol_mapper
+
+Allows for creating and managing user attribute protocol mappers for
+SAML clients within Keycloak.
+
+SAML user attribute protocol mappers allow you to map custom attributes defined
+for a user within Keycloak 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_attribute_protocol_mapper" "saml_user_attribute_mapper" {
+    realm_id                   = "${keycloak_realm.test.id}"
+    client_id                  = "${keycloak_saml_client.saml_client.id}"
+    name                       = "displayname-user-attribute-mapper"
+
+    user_attribute             = "displayName"
+    saml_attribute_name        = "displayName"
+    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_attribute` - (Required) The custom user attribute 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_attribute_protocol_mapper.saml_user_attribute_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4
+$ terraform import keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4
+```
diff --git a/example/main.tf b/example/main.tf
index 9d0c74a..02678d8 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -85,7 +85,7 @@ resource "keycloak_openid_client" "test_client" {
 		"http://localhost:5555/callback"
 	]
 
-	client_secret = "secret"
+	client_secret         = "secret"
 }
 
 resource "keycloak_openid_client_scope" "test_client_scope" {
@@ -133,12 +133,12 @@ resource "keycloak_ldap_user_federation" "openldap" {
 }
 
 resource "keycloak_ldap_user_attribute_mapper" "description_attr_mapper" {
-	name                    = "description-mapper"
-	realm_id                = "${keycloak_ldap_user_federation.openldap.realm_id}"
-	ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}"
+	name                        = "description-mapper"
+	realm_id                    = "${keycloak_ldap_user_federation.openldap.realm_id}"
+	ldap_user_federation_id     = "${keycloak_ldap_user_federation.openldap.id}"
 
-	user_model_attribute    = "description"
-	ldap_attribute          = "description"
+	user_model_attribute        = "description"
+	ldap_attribute              = "description"
 
 	always_read_value_from_ldap = false
 }
@@ -267,6 +267,17 @@ resource "keycloak_saml_client" "saml_client" {
 	sign_assertions         = true
 	include_authn_statement = true
 
-	signing_certificate = "${file("../provider/misc/saml-cert.pem")}"
-	signing_private_key = "${file("../provider/misc/saml-key.pem")}"
+	signing_certificate     = "${file("../provider/misc/saml-cert.pem")}"
+	signing_private_key     = "${file("../provider/misc/saml-key.pem")}"
+}
+
+resource "keycloak_saml_user_attribute_protocol_mapper" "saml_user_attribute_mapper" {
+	realm_id                   = "${keycloak_realm.test.id}"
+	client_id                  = "${keycloak_saml_client.saml_client.id}"
+	name                       = "test-saml-user-attribute-mapper"
+
+	user_attribute             = "user-attribute"
+	friendly_name              = "friendly-name"
+	saml_attribute_name        = "saml-attribute-name"
+	saml_attribute_name_format = "Unspecified"
 }
diff --git a/keycloak/protocol_mapper.go b/keycloak/protocol_mapper.go
index 177b8d5..561687c 100644
--- a/keycloak/protocol_mapper.go
+++ b/keycloak/protocol_mapper.go
@@ -12,16 +12,19 @@ type protocolMapper struct {
 }
 
 var (
-	addToAccessTokenField = "access.token.claim"
-	addToIdTokenField     = "id.token.claim"
-	addToUserInfoField    = "userinfo.token.claim"
-	claimNameField        = "claim.name"
-	claimValueField       = "claim.value"
-	claimValueTypeField   = "jsonType.label"
-	fullPathField         = "full.path"
-	multivaluedField      = "multivalued"
-	userAttributeField    = "user.attribute"
-	userPropertyField     = "user.attribute"
+	addToAccessTokenField    = "access.token.claim"
+	addToIdTokenField        = "id.token.claim"
+	addToUserInfoField       = "userinfo.token.claim"
+	attributeNameField       = "attribute.name"
+	attributeNameFormatField = "attribute.nameformat"
+	claimNameField           = "claim.name"
+	claimValueField          = "claim.value"
+	claimValueTypeField      = "jsonType.label"
+	friendlyNameField        = "friendly.name"
+	fullPathField            = "full.path"
+	multivaluedField         = "multivalued"
+	userAttributeField       = "user.attribute"
+	userPropertyField        = "user.attribute"
 )
 
 func protocolMapperPath(realmId, clientId, clientScopeId string) string {
diff --git a/keycloak/saml_user_attribute_protocol_mapper.go b/keycloak/saml_user_attribute_protocol_mapper.go
new file mode 100644
index 0000000..0cbee86
--- /dev/null
+++ b/keycloak/saml_user_attribute_protocol_mapper.go
@@ -0,0 +1,101 @@
+package keycloak
+
+import (
+	"fmt"
+)
+
+type SamlUserAttributeProtocolMapper struct {
+	Id            string
+	Name          string
+	RealmId       string
+	ClientId      string
+	ClientScopeId string
+
+	UserAttribute           string
+	FriendlyName            string
+	SamlAttributeName       string
+	SamlAttributeNameFormat string
+}
+
+func (mapper *SamlUserAttributeProtocolMapper) convertToGenericProtocolMapper() *protocolMapper {
+	return &protocolMapper{
+		Id:             mapper.Id,
+		Name:           mapper.Name,
+		Protocol:       "saml",
+		ProtocolMapper: "saml-user-attribute-mapper",
+		Config: map[string]string{
+			attributeNameField:       mapper.SamlAttributeName,
+			attributeNameFormatField: mapper.SamlAttributeNameFormat,
+			friendlyNameField:        mapper.FriendlyName,
+			userAttributeField:       mapper.UserAttribute,
+		},
+	}
+}
+
+func (protocolMapper *protocolMapper) convertToSamlUserAttributeProtocolMapper(realmId, clientId, clientScopeId string) *SamlUserAttributeProtocolMapper {
+	return &SamlUserAttributeProtocolMapper{
+		Id:            protocolMapper.Id,
+		Name:          protocolMapper.Name,
+		RealmId:       realmId,
+		ClientId:      clientId,
+		ClientScopeId: clientScopeId,
+
+		UserAttribute:           protocolMapper.Config[userAttributeField],
+		FriendlyName:            protocolMapper.Config[friendlyNameField],
+		SamlAttributeName:       protocolMapper.Config[attributeNameField],
+		SamlAttributeNameFormat: protocolMapper.Config[attributeNameFormatField],
+	}
+}
+
+func (keycloakClient *KeycloakClient) GetSamlUserAttributeProtocolMapper(realmId, clientId, clientScopeId, mapperId string) (*SamlUserAttributeProtocolMapper, error) {
+	var protocolMapper *protocolMapper
+
+	err := keycloakClient.get(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper)
+	if err != nil {
+		return nil, err
+	}
+
+	return protocolMapper.convertToSamlUserAttributeProtocolMapper(realmId, clientId, clientScopeId), nil
+}
+
+func (keycloakClient *KeycloakClient) DeleteSamlUserAttributeProtocolMapper(realmId, clientId, clientScopeId, mapperId string) error {
+	return keycloakClient.delete(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId))
+}
+
+func (keycloakClient *KeycloakClient) NewSamlUserAttributeProtocolMapper(mapper *SamlUserAttributeProtocolMapper) 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) UpdateSamlUserAttributeProtocolMapper(mapper *SamlUserAttributeProtocolMapper) error {
+	path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id)
+
+	return keycloakClient.put(path, mapper.convertToGenericProtocolMapper())
+}
+
+func (keycloakClient *KeycloakClient) ValidateSamlUserAttributeProtocolMapper(mapper *SamlUserAttributeProtocolMapper) 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
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 28aad5f..b394f40 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,7 +9,6 @@ nav:
   - keycloak_group: resources/keycloak_group.md
   - keycloak_group_memberships: resources/keycloak_group_memberships.md
   - keycloak_openid_client: resources/keycloak_openid_client.md
-  - keycloak_saml_client: resources/keycloak_saml_client.md
   - keycloak_openid_client_scope: resources/keycloak_openid_client_scope.md
   - keycloak_openid_client_default_scopes: resources/keycloak_openid_client_default_scopes.md
   - keycloak_openid_user_attribute_protocol_mapper: resources/keycloak_openid_user_attribute_protocol_mapper.md
@@ -17,6 +16,8 @@ nav:
   - keycloak_openid_group_membership_protocol_mapper: resources/keycloak_openid_group_membership_protocol_mapper.md
   - keycloak_openid_hardcoded_claim_protocol_mapper:  resources/keycloak_openid_hardcoded_claim_protocol_mapper.md
   - 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_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
diff --git a/provider/generic_protocol_mapper_helpers.go b/provider/generic_protocol_mapper_helpers.go
new file mode 100644
index 0000000..57ba29d
--- /dev/null
+++ b/provider/generic_protocol_mapper_helpers.go
@@ -0,0 +1,33 @@
+package provider
+
+import (
+	"fmt"
+	"github.com/hashicorp/terraform/helper/schema"
+	"strings"
+)
+
+func genericProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
+	parts := strings.Split(data.Id(), "/")
+
+	if len(parts) != 4 {
+		return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
+	}
+
+	realmId := parts[0]
+	parentResourceType := parts[1]
+	parentResourceId := parts[2]
+	mapperId := parts[3]
+
+	data.Set("realm_id", realmId)
+	data.SetId(mapperId)
+
+	if parentResourceType == "client" {
+		data.Set("client_id", parentResourceId)
+	} else if parentResourceType == "client-scope" {
+		data.Set("client_scope_id", parentResourceId)
+	} else {
+		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
+	}
+
+	return []*schema.ResourceData{data}, nil
+}
diff --git a/provider/keycloak_openid_full_name_protocol_mapper.go b/provider/keycloak_openid_full_name_protocol_mapper.go
index 41f3016..6f52c0e 100644
--- a/provider/keycloak_openid_full_name_protocol_mapper.go
+++ b/provider/keycloak_openid_full_name_protocol_mapper.go
@@ -1,10 +1,8 @@
 package provider
 
 import (
-	"fmt"
 	"github.com/hashicorp/terraform/helper/schema"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakOpenIdFullNameProtocolMapper() *schema.Resource {
@@ -18,7 +16,7 @@ func resourceKeycloakOpenIdFullNameProtocolMapper() *schema.Resource {
 			// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
 			// or a client scope:
 			// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
-			State: resourceKeycloakOpenIdFullNameProtocolMapperImport,
+			State: genericProtocolMapperImport,
 		},
 		Schema: map[string]*schema.Schema{
 			"name": {
@@ -160,29 +158,3 @@ func resourceKeycloakOpenIdFullNameProtocolMapperDelete(data *schema.ResourceDat
 
 	return keycloakClient.DeleteOpenIdFullNameProtocolMapper(realmId, clientId, clientScopeId, data.Id())
 }
-
-func resourceKeycloakOpenIdFullNameProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
-	parts := strings.Split(data.Id(), "/")
-
-	if len(parts) != 4 {
-		return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
-	}
-
-	realmId := parts[0]
-	parentResourceType := parts[1]
-	parentResourceId := parts[2]
-	mapperId := parts[3]
-
-	data.Set("realm_id", realmId)
-	data.SetId(mapperId)
-
-	if parentResourceType == "client" {
-		data.Set("client_id", parentResourceId)
-	} else if parentResourceType == "client-scope" {
-		data.Set("client_scope_id", parentResourceId)
-	} else {
-		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
-	}
-
-	return []*schema.ResourceData{data}, nil
-}
diff --git a/provider/keycloak_openid_group_membership_protocol_mapper.go b/provider/keycloak_openid_group_membership_protocol_mapper.go
index 168e218..0216cb0 100644
--- a/provider/keycloak_openid_group_membership_protocol_mapper.go
+++ b/provider/keycloak_openid_group_membership_protocol_mapper.go
@@ -1,10 +1,8 @@
 package provider
 
 import (
-	"fmt"
 	"github.com/hashicorp/terraform/helper/schema"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakOpenIdGroupMembershipProtocolMapper() *schema.Resource {
@@ -18,7 +16,7 @@ func resourceKeycloakOpenIdGroupMembershipProtocolMapper() *schema.Resource {
 			// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
 			// or a client scope:
 			// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
-			State: resourceKeycloakOpenIdGroupMembershipProtocolMapperImport,
+			State: genericProtocolMapperImport,
 		},
 		Schema: map[string]*schema.Schema{
 			"name": {
@@ -173,29 +171,3 @@ func resourceKeycloakOpenIdGroupMembershipProtocolMapperDelete(data *schema.Reso
 
 	return keycloakClient.DeleteOpenIdGroupMembershipProtocolMapper(realmId, clientId, clientScopeId, data.Id())
 }
-
-func resourceKeycloakOpenIdGroupMembershipProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
-	parts := strings.Split(data.Id(), "/")
-
-	if len(parts) != 4 {
-		return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
-	}
-
-	realmId := parts[0]
-	parentResourceType := parts[1]
-	parentResourceId := parts[2]
-	mapperId := parts[3]
-
-	data.Set("realm_id", realmId)
-	data.SetId(mapperId)
-
-	if parentResourceType == "client" {
-		data.Set("client_id", parentResourceId)
-	} else if parentResourceType == "client-scope" {
-		data.Set("client_scope_id", parentResourceId)
-	} else {
-		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
-	}
-
-	return []*schema.ResourceData{data}, nil
-}
diff --git a/provider/keycloak_openid_hardcoded_claim_protocol_mapper.go b/provider/keycloak_openid_hardcoded_claim_protocol_mapper.go
index 769c988..c5b6547 100644
--- a/provider/keycloak_openid_hardcoded_claim_protocol_mapper.go
+++ b/provider/keycloak_openid_hardcoded_claim_protocol_mapper.go
@@ -1,11 +1,9 @@
 package provider
 
 import (
-	"fmt"
 	"github.com/hashicorp/terraform/helper/schema"
 	"github.com/hashicorp/terraform/helper/validation"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakOpenIdHardcodedClaimProtocolMapper() *schema.Resource {
@@ -19,7 +17,7 @@ func resourceKeycloakOpenIdHardcodedClaimProtocolMapper() *schema.Resource {
 			// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
 			// or a client scope:
 			// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
-			State: resourceKeycloakOpenIdHardcodedClaimProtocolMapperImport,
+			State: genericProtocolMapperImport,
 		},
 		Schema: map[string]*schema.Schema{
 			"name": {
@@ -184,29 +182,3 @@ func resourceKeycloakOpenIdHardcodedClaimProtocolMapperDelete(data *schema.Resou
 
 	return keycloakClient.DeleteOpenIdHardcodedClaimProtocolMapper(realmId, clientId, clientScopeId, data.Id())
 }
-
-func resourceKeycloakOpenIdHardcodedClaimProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
-	parts := strings.Split(data.Id(), "/")
-
-	if len(parts) != 4 {
-		return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
-	}
-
-	realmId := parts[0]
-	parentResourceType := parts[1]
-	parentResourceId := parts[2]
-	mapperId := parts[3]
-
-	data.Set("realm_id", realmId)
-	data.SetId(mapperId)
-
-	if parentResourceType == "client" {
-		data.Set("client_id", parentResourceId)
-	} else if parentResourceType == "client-scope" {
-		data.Set("client_scope_id", parentResourceId)
-	} else {
-		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
-	}
-
-	return []*schema.ResourceData{data}, nil
-}
diff --git a/provider/keycloak_openid_user_attribute_protocol_mapper.go b/provider/keycloak_openid_user_attribute_protocol_mapper.go
index 2f7c12f..309269f 100644
--- a/provider/keycloak_openid_user_attribute_protocol_mapper.go
+++ b/provider/keycloak_openid_user_attribute_protocol_mapper.go
@@ -1,11 +1,9 @@
 package provider
 
 import (
-	"fmt"
 	"github.com/hashicorp/terraform/helper/schema"
 	"github.com/hashicorp/terraform/helper/validation"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakOpenIdUserAttributeProtocolMapper() *schema.Resource {
@@ -19,7 +17,7 @@ func resourceKeycloakOpenIdUserAttributeProtocolMapper() *schema.Resource {
 			// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
 			// or a client scope:
 			// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
-			State: resourceKeycloakOpenIdUserAttributeProtocolMapperImport,
+			State: genericProtocolMapperImport,
 		},
 		Schema: map[string]*schema.Schema{
 			"name": {
@@ -192,29 +190,3 @@ func resourceKeycloakOpenIdUserAttributeProtocolMapperDelete(data *schema.Resour
 
 	return keycloakClient.DeleteOpenIdUserAttributeProtocolMapper(realmId, clientId, clientScopeId, data.Id())
 }
-
-func resourceKeycloakOpenIdUserAttributeProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
-	parts := strings.Split(data.Id(), "/")
-
-	if len(parts) != 4 {
-		return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
-	}
-
-	realmId := parts[0]
-	parentResourceType := parts[1]
-	parentResourceId := parts[2]
-	mapperId := parts[3]
-
-	data.Set("realm_id", realmId)
-	data.SetId(mapperId)
-
-	if parentResourceType == "client" {
-		data.Set("client_id", parentResourceId)
-	} else if parentResourceType == "client-scope" {
-		data.Set("client_scope_id", parentResourceId)
-	} else {
-		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
-	}
-
-	return []*schema.ResourceData{data}, nil
-}
diff --git a/provider/keycloak_openid_user_property_protocol_mapper.go b/provider/keycloak_openid_user_property_protocol_mapper.go
index 370f52d..dd22c56 100644
--- a/provider/keycloak_openid_user_property_protocol_mapper.go
+++ b/provider/keycloak_openid_user_property_protocol_mapper.go
@@ -1,11 +1,9 @@
 package provider
 
 import (
-	"fmt"
 	"github.com/hashicorp/terraform/helper/schema"
 	"github.com/hashicorp/terraform/helper/validation"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakOpenIdUserPropertyProtocolMapper() *schema.Resource {
@@ -19,7 +17,7 @@ func resourceKeycloakOpenIdUserPropertyProtocolMapper() *schema.Resource {
 			// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
 			// or a client scope:
 			// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
-			State: resourceKeycloakOpenIdUserPropertyProtocolMapperImport,
+			State: genericProtocolMapperImport,
 		},
 		Schema: map[string]*schema.Schema{
 			"name": {
@@ -178,29 +176,3 @@ func resourceKeycloakOpenIdUserPropertyProtocolMapperDelete(data *schema.Resourc
 
 	return keycloakClient.DeleteOpenIdUserPropertyProtocolMapper(realmId, clientId, clientScopeId, data.Id())
 }
-
-func resourceKeycloakOpenIdUserPropertyProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
-	parts := strings.Split(data.Id(), "/")
-
-	if len(parts) != 4 {
-		return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
-	}
-
-	realmId := parts[0]
-	parentResourceType := parts[1]
-	parentResourceId := parts[2]
-	mapperId := parts[3]
-
-	data.Set("realm_id", realmId)
-	data.SetId(mapperId)
-
-	if parentResourceType == "client" {
-		data.Set("client_id", parentResourceId)
-	} else if parentResourceType == "client-scope" {
-		data.Set("client_scope_id", parentResourceId)
-	} else {
-		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
-	}
-
-	return []*schema.ResourceData{data}, nil
-}
diff --git a/provider/keycloak_saml_user_attribute_protocol_mapper.go b/provider/keycloak_saml_user_attribute_protocol_mapper.go
new file mode 100644
index 0000000..c2eb83f
--- /dev/null
+++ b/provider/keycloak_saml_user_attribute_protocol_mapper.go
@@ -0,0 +1,162 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform/helper/schema"
+	"github.com/hashicorp/terraform/helper/validation"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+var keycloakSamlUserAttributeProtocolMapperNameFormats = []string{"Basic", "URI Reference", "Unspecified"}
+
+func resourceKeycloakSamlUserAttributeProtocolMapper() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceKeycloakSamlUserAttributeProtocolMapperCreate,
+		Read:   resourceKeycloakSamlUserAttributeProtocolMapperRead,
+		Update: resourceKeycloakSamlUserAttributeProtocolMapperUpdate,
+		Delete: resourceKeycloakSamlUserAttributeProtocolMapperDelete,
+		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_attribute": {
+				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 mapFromDataToSamlUserAttributeProtocolMapper(data *schema.ResourceData) *keycloak.SamlUserAttributeProtocolMapper {
+	return &keycloak.SamlUserAttributeProtocolMapper{
+		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),
+
+		UserAttribute:           data.Get("user_attribute").(string),
+		FriendlyName:            data.Get("friendly_name").(string),
+		SamlAttributeName:       data.Get("saml_attribute_name").(string),
+		SamlAttributeNameFormat: data.Get("saml_attribute_name_format").(string),
+	}
+}
+
+func mapFromSamlUserAttributeMapperToData(mapper *keycloak.SamlUserAttributeProtocolMapper, 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_attribute", mapper.UserAttribute)
+	data.Set("friendly_name", mapper.FriendlyName)
+	data.Set("saml_attribute_name", mapper.SamlAttributeName)
+	data.Set("saml_attribute_name_format", mapper.SamlAttributeNameFormat)
+}
+
+func resourceKeycloakSamlUserAttributeProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	samlUserAttributeMapper := mapFromDataToSamlUserAttributeProtocolMapper(data)
+
+	err := keycloakClient.ValidateSamlUserAttributeProtocolMapper(samlUserAttributeMapper)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.NewSamlUserAttributeProtocolMapper(samlUserAttributeMapper)
+	if err != nil {
+		return err
+	}
+
+	mapFromSamlUserAttributeMapperToData(samlUserAttributeMapper, data)
+
+	return resourceKeycloakSamlUserAttributeProtocolMapperRead(data, meta)
+}
+
+func resourceKeycloakSamlUserAttributeProtocolMapperRead(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)
+
+	samlUserAttributeMapper, err := keycloakClient.GetSamlUserAttributeProtocolMapper(realmId, clientId, clientScopeId, data.Id())
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	mapFromSamlUserAttributeMapperToData(samlUserAttributeMapper, data)
+
+	return nil
+}
+
+func resourceKeycloakSamlUserAttributeProtocolMapperUpdate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	samlUserAttributeMapper := mapFromDataToSamlUserAttributeProtocolMapper(data)
+
+	err := keycloakClient.ValidateSamlUserAttributeProtocolMapper(samlUserAttributeMapper)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.UpdateSamlUserAttributeProtocolMapper(samlUserAttributeMapper)
+	if err != nil {
+		return err
+	}
+
+	return resourceKeycloakSamlUserAttributeProtocolMapperRead(data, meta)
+}
+
+func resourceKeycloakSamlUserAttributeProtocolMapperDelete(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.DeleteSamlUserAttributeProtocolMapper(realmId, clientId, clientScopeId, data.Id())
+}
diff --git a/provider/provider.go b/provider/provider.go
index 3d4cf5b..001c97c 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -27,6 +27,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_openid_hardcoded_claim_protocol_mapper":  resourceKeycloakOpenIdHardcodedClaimProtocolMapper(),
 			"keycloak_openid_client_default_scopes":            resourceKeycloakOpenidClientDefaultScopes(),
 			"keycloak_saml_client":                             resourceKeycloakSamlClient(),
+			"keycloak_saml_user_attribute_protocol_mapper":     resourceKeycloakSamlUserAttributeProtocolMapper(),
 		},
 		Schema: map[string]*schema.Schema{
 			"client_id": {


Test patch:
diff --git a/provider/keycloak_openid_user_attribute_protocol_mapper_test.go b/provider/keycloak_openid_user_attribute_protocol_mapper_test.go
index 06eb0f2..609dfea 100644
--- a/provider/keycloak_openid_user_attribute_protocol_mapper_test.go
+++ b/provider/keycloak_openid_user_attribute_protocol_mapper_test.go
@@ -62,7 +62,7 @@ func TestAccKeycloakOpenIdUserAttributeProtocolMapper_import(t *testing.T) {
 	resource.Test(t, resource.TestCase{
 		Providers:    testAccProviders,
 		PreCheck:     func() { testAccPreCheck(t) },
-		CheckDestroy: testAccKeycloakOpenIdFullNameProtocolMapperDestroy(),
+		CheckDestroy: testAccKeycloakOpenIdUserAttributeProtocolMapperDestroy(),
 		Steps: []resource.TestStep{
 			{
 				Config: testKeycloakOpenIdUserAttributeProtocolMapper_import(realmName, clientId, clientScopeId, mapperName),
diff --git a/provider/keycloak_saml_user_attribute_protocol_mapper_test.go b/provider/keycloak_saml_user_attribute_protocol_mapper_test.go
new file mode 100644
index 0000000..5be86fc
--- /dev/null
+++ b/provider/keycloak_saml_user_attribute_protocol_mapper_test.go
@@ -0,0 +1,318 @@
+package provider
+
+import (
+	"fmt"
+	"github.com/hashicorp/terraform/helper/acctest"
+	"github.com/hashicorp/terraform/helper/resource"
+	"github.com/hashicorp/terraform/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"regexp"
+	"testing"
+)
+
+// Tests for attaching SAML mappers to SAML client scopes are omitted
+// because the keycloak_saml_client_scope resource does not exist yet.
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_basicClient(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_import(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-saml-client-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(5)
+
+	clientResourceName := "keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(clientResourceName),
+			},
+			{
+				ResourceName:      clientResourceName,
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getGenericProtocolMapperIdForClient(clientResourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_update(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(5)
+
+	userAttribute := "attr-" + acctest.RandString(10)
+	updatedUserAttribute := "attr-update-" + acctest.RandString(10)
+	resourceName := "keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_userAttribute(realmName, clientId, mapperName, userAttribute),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_userAttribute(realmName, clientId, mapperName, updatedUserAttribute),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_createAfterManualDestroy(t *testing.T) {
+	var mapper = &keycloak.SamlUserAttributeProtocolMapper{}
+
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperFetch(resourceName, mapper),
+			},
+			{
+				PreConfig: func() {
+					keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+					err := keycloakClient.DeleteSamlUserAttributeProtocolMapper(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id)
+					if err != nil {
+						t.Error(err)
+					}
+				},
+				Config: testKeycloakSamlUserAttributeProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_validateClaimValueType(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(10)
+	invalidSamlNameFormat := acctest.RandString(5)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config:      testKeycloakSamlUserAttributeProtocolMapper_samlAttributeNameFormat(realmName, clientId, mapperName, invalidSamlNameFormat),
+				ExpectError: regexp.MustCompile("expected saml_attribute_name_format to be one of .+ got " + invalidSamlNameFormat),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_updateClientIdForceNew(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	updatedClientId := "terraform-client-update-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(5)
+
+	userAttribute := "attr-" + acctest.RandString(10)
+	resourceName := "keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_userAttribute(realmName, clientId, mapperName, userAttribute),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_userAttribute(realmName, updatedClientId, mapperName, userAttribute),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakSamlUserAttributeProtocolMapper_updateRealmIdForceNew(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	newRealmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-saml-user-attribute-mapper-" + acctest.RandString(5)
+
+	userAttribute := "attr-" + acctest.RandString(10)
+	resourceName := "keycloak_saml_user_attribute_protocol_mapper.saml_user_attribute_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakSamlUserAttributeProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_userAttribute(realmName, clientId, mapperName, userAttribute),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakSamlUserAttributeProtocolMapper_userAttribute(newRealmName, clientId, mapperName, userAttribute),
+				Check:  testKeycloakSamlUserAttributeProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func testAccKeycloakSamlUserAttributeProtocolMapperDestroy() resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		for resourceName, rs := range state.RootModule().Resources {
+			if rs.Type != "keycloak_saml_user_attribute_protocol_mapper" {
+				continue
+			}
+
+			mapper, _ := getSamlUserAttributeMapperUsingState(state, resourceName)
+
+			if mapper != nil {
+				return fmt.Errorf("saml user attribute protocol mapper with id %s still exists", rs.Primary.ID)
+			}
+		}
+
+		return nil
+	}
+}
+
+func testKeycloakSamlUserAttributeProtocolMapperExists(resourceName string) resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		_, err := getSamlUserAttributeMapperUsingState(state, resourceName)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+func testKeycloakSamlUserAttributeProtocolMapperFetch(resourceName string, mapper *keycloak.SamlUserAttributeProtocolMapper) resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		fetchedMapper, err := getSamlUserAttributeMapperUsingState(state, resourceName)
+		if err != nil {
+			return err
+		}
+
+		mapper.Id = fetchedMapper.Id
+		mapper.ClientId = fetchedMapper.ClientId
+		mapper.ClientScopeId = fetchedMapper.ClientScopeId
+		mapper.RealmId = fetchedMapper.RealmId
+
+		return nil
+	}
+}
+
+func getSamlUserAttributeMapperUsingState(state *terraform.State, resourceName string) (*keycloak.SamlUserAttributeProtocolMapper, error) {
+	rs, ok := state.RootModule().Resources[resourceName]
+	if !ok {
+		return nil, fmt.Errorf("resource not found in TF state: %s ", resourceName)
+	}
+
+	id := rs.Primary.ID
+	realm := rs.Primary.Attributes["realm_id"]
+	clientId := rs.Primary.Attributes["client_id"]
+	clientScopeId := rs.Primary.Attributes["client_scope_id"]
+
+	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+	return keycloakClient.GetSamlUserAttributeProtocolMapper(realm, clientId, clientScopeId, id)
+}
+
+func testKeycloakSamlUserAttributeProtocolMapper_basic_client(realmName, clientId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_saml_client" "saml_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "%s"
+}
+
+resource "keycloak_saml_user_attribute_protocol_mapper" "saml_user_attribute_mapper" {
+	name                       = "%s"
+	realm_id                   = "${keycloak_realm.realm.id}"
+	client_id                  = "${keycloak_saml_client.saml_client.id}"
+
+	user_attribute             = "foo"
+	saml_attribute_name        = "bar"
+	saml_attribute_name_format = "Unspecified"
+}`, realmName, clientId, mapperName)
+}
+
+func testKeycloakSamlUserAttributeProtocolMapper_userAttribute(realmName, clientId, mapperName, userAttribute string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_saml_client" "saml_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "%s"
+}
+
+resource "keycloak_saml_user_attribute_protocol_mapper" "saml_user_attribute_mapper" {
+	name                       = "%s"
+	realm_id                   = "${keycloak_realm.realm.id}"
+	client_id                  = "${keycloak_saml_client.saml_client.id}"
+
+	user_attribute             = "%s"
+	saml_attribute_name        = "bar"
+	saml_attribute_name_format = "Unspecified"
+}`, realmName, clientId, mapperName, userAttribute)
+}
+
+func testKeycloakSamlUserAttributeProtocolMapper_samlAttributeNameFormat(realmName, clientName, mapperName, samlAttributeNameFormat string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_saml_client" "saml_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "%s"
+}
+
+resource "keycloak_saml_user_attribute_protocol_mapper" "saml_user_attribute_mapper" {
+	name                       = "%s"
+	realm_id                   = "${keycloak_realm.realm.id}"
+	client_id                  = "${keycloak_saml_client.saml_client.id}"
+
+	user_attribute             = "foo"
+	saml_attribute_name        = "bar"
+	saml_attribute_name_format = "%s"
+}`, realmName, clientName, mapperName, samlAttributeNameFormat)
+}