Repository: mrparkers/terraform-provider-keycloak
Pull request #119 (2019-06-04 16:34:24)

generic implementation for a client protocol mapper
This is generic implementation for #14 . It supports saml and openid-connect clients. In order to support custom protocol mappers it does not enforce a specific structure.

example:
```hcl
resource "keycloak_generic_client_protocol_mapper" "Session_Duration" {
  name = "Session Duration"
  protocol = "saml"
  protocol_mapper = "saml-hardcode-attribute-mapper"
  realm_id = "${keycloak_realm.aws.id}"
  client_id = "${keycloak_saml_client.urn_amazon_webservices.id}"
  config = {
    "attribute.value" = "28800",
    "attribute.nameformat" = "Basic"
    "friendly.name" =  "Session Duration"
    "attribute.name" = "https://aws.amazon.com/SAML/Attributes/SessionDuration"
  }
} 
```

Before creating documentation and test cases it would be nice to get some feedback about the current code.

mrparkers: Before I give this a code review, I want to understand the use case for this resource.

Right now, there are specific resources for different protocol mappers (such as `keycloak_openid_user_attribute_protocol_mapper`).  I thought that implementing a separate resource for each type of protocol mapper was a better user experience because it didn't require referencing the Keycloak API docs in order to figure out the protocol mapper type (`oidc-usermodel-attribute-mapper` in this case) or the config values, which varied between different protocol mappers.

That being said, I can think of a few reasons why a generic protocol mapper resource like this would be useful:

1) If the provider doesn't support a particular protocol mapper, this resource can be used instead.
2) If you implemented a custom protocol mapper, this resource can be used to configure it, since the provider probably would never support that.

Are these the use cases you're thinking of, or is there something else you're trying to accomplish with this resource?
BernhardBerbuir: I completely agree with you: the generic client protocol mapper is less user friendly and more error prone. Above all, I need it for a custom protocol mapper (use case 2).  
=> If needed I can rename it to keycloak_custom_client_protocol_mapper
mrparkers: Perfect, sounds like we're on the same page then.  I think the code looks good so far.  I'd be happy to merge this and cut a release for you once some test cases are added.
mrparkers: Also, to answer your question, I think `keycloak_generic_client_protocol_mapper` is a good name for this resource.

Patch:
diff --git a/docs/index.md b/docs/index.md
index 1ba6437..459eafb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -17,12 +17,12 @@ sure which to use, the client credentials grant is recommended, as it was design
 
 1. Create a new client using the `openid-connect` protocol. This client can be created in the `master` realm if you would
 like to manage your entire Keycloak instance, or in any other realm if you only want to manage that realm.
-2. Update the client you just created:
-    2a. Set "Access Type" to "confidential".
-    2b. Set "Standard Flow Enabled" to "OFF".
-    2c. Set "Direct Access Grants Enabled" to "OFF"
-    2d. Set "Service Accounts Enabled" to "ON".
-3. Grant required roles for managing Keycloak via the "Service Account Roles" tab in the client you created in step 1.
+1. Update the client you just created:
+    1. Set "Access Type" to "confidential".
+    1. Set "Standard Flow Enabled" to "OFF".
+    1. Set "Direct Access Grants Enabled" to "OFF"
+    1. Set "Service Accounts Enabled" to "ON".
+1. Grant required roles for managing Keycloak via the "Service Account Roles" tab in the client you created in step 1.
 
 ## Password Grant Setup
 
@@ -30,7 +30,7 @@ These steps will assume that you are using the `admin-cli` client, which is alre
 of authentication. Do not follow these steps if you have already followed the steps for the client credentials grant.
 
 1. Create or identify the user who's credentials will be used for authentication.
-2. Edit this user in the "Users" section of the management console and assign roles using the "Role Mappings" tab.
+1. Edit this user in the "Users" section of the management console and assign roles using the "Role Mappings" tab.
 
 ## Assigning Roles
 
diff --git a/docs/resources/keycloak_generic_client_protocol_mapper.md b/docs/resources/keycloak_generic_client_protocol_mapper.md
new file mode 100644
index 0000000..2746481
--- /dev/null
+++ b/docs/resources/keycloak_generic_client_protocol_mapper.md
@@ -0,0 +1,60 @@
+# keycloak_generic_client_protocol_mapper
+
+Allows for creating and managing protocol mapper for both types of clients (openid-connect and saml) within Keycloak.
+
+There are two uses cases for using this resource:
+* If you implemented a custom protocol mapper, this resource can be used to configure it
+* If the provider doesn't support a particular protocol mapper, this resource can be used instead.
+
+Due to the generic nature of this mapper, it is less user-friendly and more prone to configuration errors. 
+Therefore, if possible, a specific mapper should be used.
+
+### Example Usage
+
+```hcl
+resource "keycloak_realm" "realm" {
+    realm   = "my-realm"
+    enabled = true
+}
+
+resource "keycloak_saml_client" "saml_client" {
+  realm_id  = "${keycloak_realm.realm.id}"
+  client_id = "test-client"
+}
+
+resource "keycloak_generic_client_protocol_mapper" "saml_hardcode_attribute_mapper" {
+  realm_id        = "${keycloak_realm.realm.id}"
+  client_id       = "${keycloak_saml_client.saml_client.id}"
+  name            = "tes-mapper"
+  protocol        = "saml"
+  protocol_mapper = "saml-hardcode-attribute-mapper"
+  config = {
+    "attribute.name"       = "name"
+    "attribute.nameformat" = "Basic"
+    "attribute.value"      = "value"
+    "friendly.name"        = "display name"
+  }
+}
+```
+
+### Argument Reference
+
+The following arguments are supported:
+
+- `realm_id` - (Required) The realm this protocol mapper exists within.
+- `client_id` - (Required) The client this protocol mapper is attached to.
+- `name` - (Required) The display name of this protocol mapper in the GUI.
+- `protocol` - (Required) The type of client (either `openid-connect` or `saml`). The type must match the type of the client.
+- `protocol_mapper` - (Required) The name of the protocol mapper. The protocol mapper must be
+   compatible with the specified client.
+- `config` - (Required) A map with key / value pairs for configuring the protocol mapper. The supported keys depends on the protocol mapper.
+
+### Import
+
+Protocol mappers can be imported using the following format: `{{realm_id}}/client/{{client_keycloak_id}}/{{protocol_mapper_id}}`
+
+Example:
+
+```bash
+$ terraform import keycloak_generic_client_protocol_mapper.saml_hardcode_attribute_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4
+```
diff --git a/docs/resources/keycloak_ldap_user_federation.md b/docs/resources/keycloak_ldap_user_federation.md
index 256a34b..d399887 100644
--- a/docs/resources/keycloak_ldap_user_federation.md
+++ b/docs/resources/keycloak_ldap_user_federation.md
@@ -60,13 +60,13 @@ The following arguments are supported:
 - `bind_credential` - (Optional) Password of LDAP admin. This attribute must be set if `bind_dn` is set.
 - `custom_user_search_filter` - (Optional) Additional LDAP filter for filtering searched users. Must begin with `(` and end with `)`.
 - `search_scope` - (Optional) Can be one of `ONE_LEVEL` or `SUBTREE`:
-  - `ONE_LEVEL`: Only search for users in the DN specified by `user_dn`.
-  - `SUBTREE`: Search entire LDAP subtree.
+    - `ONE_LEVEL`: Only search for users in the DN specified by `user_dn`.
+    - `SUBTREE`: Search entire LDAP subtree.
 - `validate_password_policy` - (Optional) When `true`, Keycloak will validate passwords using the realm policy before updating it.
 - `use_truststore_spi` - (Optional) Can be one of `ALWAYS`, `ONLY_FOR_LDAPS`, or `NEVER`:
-  - `ALWAYS` - Always use the truststore SPI for LDAP connections.
-  - `NEVER` - Never use the truststore SPI for LDAP connections.
-  - `ONLY_FOR_LDAPS` - Only use the truststore SPI if your LDAP connection uses the ldaps protocol.
+    - `ALWAYS` - Always use the truststore SPI for LDAP connections.
+    - `NEVER` - Never use the truststore SPI for LDAP connections.
+    - `ONLY_FOR_LDAPS` - Only use the truststore SPI if your LDAP connection uses the ldaps protocol.
 - `connection_timeout` - (Optional) LDAP connection timeout in the format of a [Go duration string](https://golang.org/pkg/time/#Duration.String).
 - `read_timeout` - (Optional) LDAP read timeout in the format of a [Go duration string](https://golang.org/pkg/time/#Duration.String).
 - `pagination` - (Optional) When true, Keycloak assumes the LDAP server supports pagination. Defaults to `true`.
diff --git a/docs/resources/keycloak_openid_client.md b/docs/resources/keycloak_openid_client.md
index 8482dd1..a880b5b 100644
--- a/docs/resources/keycloak_openid_client.md
+++ b/docs/resources/keycloak_openid_client.md
@@ -38,11 +38,11 @@ The following arguments are supported:
 - `enabled` - (Optional) When false, this client will not be able to initiate a login or obtain access tokens. Defaults to `true`.
 - `description` - (Optional) The description of this client in the GUI.
 - `access_type` - (Required) Specifies the type of client, which can be one of the following:
-	- `CONFIDENTIAL` - Used for server-side clients that require both client ID and secret when authenticating.
-	This client should be used for applications using the Authorization Code or Client Credentials grant flows.
-	- `PUBLIC` - Used for browser-only applications that do not require a client secret, and instead rely only on authorized redirect
-	URIs for security. This client should be used for applications using the Implicit grant flow.
-	- `BEARER-ONLY` - Used for services that never initiate a login. This client will only allow bearer token requests.
+    - `CONFIDENTIAL` - Used for server-side clients that require both client ID and secret when authenticating.
+      This client should be used for applications using the Authorization Code or Client Credentials grant flows.
+    - `PUBLIC` - Used for browser-only applications that do not require a client secret, and instead rely only on authorized redirect
+      URIs for security. This client should be used for applications using the Implicit grant flow.
+    - `BEARER-ONLY` - Used for services that never initiate a login. This client will only allow bearer token requests.
 - `client_secret` - (Optional) The secret for clients with an `access_type` of `CONFIDENTIAL` or `BEARER-ONLY`. This value is sensitive and
 should be treated with the same care as a password. If omitted, Keycloak will generate a GUID for this attribute.
 - `standard_flow_enabled` - (Optional) When `true`, the OAuth2 Authorization Code Grant will be enabled for this client. Defaults to `false`.
diff --git a/docs/resources/keycloak_user.md b/docs/resources/keycloak_user.md
index 86a62df..03de8b1 100644
--- a/docs/resources/keycloak_user.md
+++ b/docs/resources/keycloak_user.md
@@ -48,8 +48,8 @@ The following arguments are supported:
 - `username` - (Required) The unique username of this user.
 - `initial_password` (Optional) When given, the user's initial password will be set.
    This attribute is only respected during initial user creation.
-  - `value` (Required) The initial password.
-  - `temporary` (Optional) If set to `true`, the initial password is set up for renewal on first use. Default to `false`.
+    - `value` (Required) The initial password.
+    - `temporary` (Optional) If set to `true`, the initial password is set up for renewal on first use. Default to `false`.
 - `enabled` - (Optional) When false, this user cannot log in. Defaults to `true`.
 - `email` - (Optional) The user's email.
 - `first_name` - (Optional) The user's first name.
diff --git a/keycloak/generic_client_protocol_mapper.go b/keycloak/generic_client_protocol_mapper.go
new file mode 100644
index 0000000..a2c0d0c
--- /dev/null
+++ b/keycloak/generic_client_protocol_mapper.go
@@ -0,0 +1,53 @@
+package keycloak
+
+import (
+	"fmt"
+)
+
+type GenericClientProtocolMapper struct {
+	ClientId       string            `json:"-"`
+	Config         map[string]string `json:"config"`
+	Id             string            `json:"id,omitempty"`
+	Name           string            `json:"name"`
+	Protocol       string            `json:"protocol"`
+	ProtocolMapper string            `json:"protocolMapper"`
+	RealmId        string            `json:"-"`
+}
+
+func (keycloakClient *KeycloakClient) NewGenericClientProtocolMapper(genericClientProtocolMapper *GenericClientProtocolMapper) error {
+	_, location, err := keycloakClient.post(
+		fmt.Sprintf("/realms/%s/clients/%s/protocol-mappers/models", genericClientProtocolMapper.RealmId, genericClientProtocolMapper.ClientId),
+		genericClientProtocolMapper)
+	if err != nil {
+		return err
+	}
+
+	genericClientProtocolMapper.Id = getIdFromLocationHeader(location)
+
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) GetGenericClientProtocolMapper(realmId string, clientId string, id string) (*GenericClientProtocolMapper, error) {
+	var genericClientProtocolMapper GenericClientProtocolMapper
+
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/clients/%s/protocol-mappers/models/%s", realmId, clientId, id), &genericClientProtocolMapper, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// these values are not provided by the keycloak API
+	genericClientProtocolMapper.ClientId = clientId
+	genericClientProtocolMapper.RealmId = realmId
+
+	return &genericClientProtocolMapper, nil
+}
+
+func (keycloakClient *KeycloakClient) UpdateGenericClientProtocolMapper(genericClientProtocolMapper *GenericClientProtocolMapper) error {
+	return keycloakClient.put(
+		fmt.Sprintf("/realms/%s/clients/%s/protocol-mappers/models/%s", genericClientProtocolMapper.RealmId, genericClientProtocolMapper.ClientId, genericClientProtocolMapper.Id),
+		genericClientProtocolMapper)
+}
+
+func (keycloakClient *KeycloakClient) DeleteGenericClientProtocolMapper(realmId string, clientId string, id string) error {
+	return keycloakClient.delete(fmt.Sprintf("/realms/%s/clients/%s/protocol-mappers/models/%s", realmId, clientId, id), nil)
+}
diff --git a/provider/provider.go b/provider/provider.go
index 2b3c28b..814911f 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -33,6 +33,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_openid_client_default_scopes":                    resourceKeycloakOpenidClientDefaultScopes(),
 			"keycloak_openid_client_optional_scopes":                   resourceKeycloakOpenidClientOptionalScopes(),
 			"keycloak_saml_client":                                     resourceKeycloakSamlClient(),
+			"keycloak_generic_client_protocol_mapper":                  resourceKeycloakGenericClientProtocolMapper(),
 			"keycloak_saml_user_attribute_protocol_mapper":             resourceKeycloakSamlUserAttributeProtocolMapper(),
 			"keycloak_saml_user_property_protocol_mapper":              resourceKeycloakSamlUserPropertyProtocolMapper(),
 			"keycloak_hardcoded_attribute_identity_provider_mapper":    resourceKeycloakHardcodedAttributeIdentityProviderMapper(),
diff --git a/provider/resource_keycloak_generic_client_protocol_mapper.go b/provider/resource_keycloak_generic_client_protocol_mapper.go
new file mode 100644
index 0000000..3035481
--- /dev/null
+++ b/provider/resource_keycloak_generic_client_protocol_mapper.go
@@ -0,0 +1,144 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform/helper/schema"
+	"github.com/hashicorp/terraform/helper/validation"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"log"
+)
+
+func resourceKeycloakGenericClientProtocolMapper() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceKeycloakGenericClientProtocolMapperCreate,
+		Read:   resourceKeycloakGenericClientProtocolMapperRead,
+		Delete: resourceKeycloakGenericClientProtocolMapperDelete,
+		Update: resourceKeycloakGenericClientProtocolMapperUpdate,
+		// This resource can be imported using {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
+		Importer: &schema.ResourceImporter{
+			State: genericProtocolMapperImport,
+		},
+		Schema: map[string]*schema.Schema{
+			"name": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "A human-friendly name that will appear in the Keycloak console.",
+			},
+			"realm_id": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The realm id where the associated client exists.",
+			},
+			"client_id": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The mapper's associated client.",
+			},
+			"protocol": {
+				Type:         schema.TypeString,
+				Required:     true,
+				ForceNew:     true,
+				Description:  "The protocol of the client (openid-connect / saml).",
+				ValidateFunc: validation.StringInSlice([]string{"openid-connect", "saml"}, false),
+			},
+			"protocol_mapper": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The type of the protocol mapper.",
+			},
+			"config": {
+				Type:     schema.TypeMap,
+				Required: true,
+			},
+		},
+	}
+}
+
+func getGenericClientProtocolMapperFromData(data *schema.ResourceData) *keycloak.GenericClientProtocolMapper {
+	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.GenericClientProtocolMapper{
+		ClientId:       data.Get("client_id").(string),
+		Config:         config,
+		Id:             data.Id(),
+		Name:           data.Get("name").(string),
+		Protocol:       data.Get("protocol").(string),
+		ProtocolMapper: data.Get("protocol_mapper").(string),
+		RealmId:        data.Get("realm_id").(string),
+	}
+}
+
+func setGenericClientProtocolMapperData(data *schema.ResourceData, resource *keycloak.GenericClientProtocolMapper) {
+	data.SetId(resource.Id)
+	data.Set("client_id", resource.ClientId)
+	data.Set("config", resource.Config)
+	data.Set("name", resource.Name)
+	data.Set("protocol", resource.Protocol)
+	data.Set("protocol_mapper", resource.ProtocolMapper)
+	data.Set("realm_id", resource.RealmId)
+}
+
+func resourceKeycloakGenericClientProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	resource := getGenericClientProtocolMapperFromData(data)
+
+	err := keycloakClient.NewGenericClientProtocolMapper(resource)
+	if err != nil {
+		return err
+	}
+	setGenericClientProtocolMapperData(data, resource)
+
+	return resourceKeycloakGenericClientProtocolMapperRead(data, meta)
+}
+
+func resourceKeycloakGenericClientProtocolMapperRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmId := data.Get("realm_id").(string)
+	clientId := data.Get("client_id").(string)
+	id := data.Id()
+
+	resource, err := keycloakClient.GetGenericClientProtocolMapper(realmId, clientId, id)
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	setGenericClientProtocolMapperData(data, resource)
+
+	return nil
+}
+
+func resourceKeycloakGenericClientProtocolMapperUpdate(data *schema.ResourceData, meta interface{}) error {
+	log.Printf("[DEBUG] updating\n")
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	resource := getGenericClientProtocolMapperFromData(data)
+
+	err := keycloakClient.UpdateGenericClientProtocolMapper(resource)
+	if err != nil {
+		return err
+	}
+
+	setGenericClientProtocolMapperData(data, resource)
+
+	return nil
+}
+
+func resourceKeycloakGenericClientProtocolMapperDelete(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmId := data.Get("realm_id").(string)
+	clientId := data.Get("client_id").(string)
+	id := data.Id()
+
+	return keycloakClient.DeleteGenericClientProtocolMapper(realmId, clientId, id)
+}


Test patch:
diff --git a/provider/provider_test.go b/provider/provider_test.go
index 7a92889..96d07b4 100644
--- a/provider/provider_test.go
+++ b/provider/provider_test.go
@@ -12,6 +12,7 @@ var testAccProvider *schema.Provider
 
 var requiredEnvironmentVariables = []string{
 	"KEYCLOAK_CLIENT_ID",
+	"KEYCLOAK_REALM",
 	"KEYCLOAK_URL",
 }
 
diff --git a/provider/resource_keycloak_generic_client_protocol_mapper_test.go b/provider/resource_keycloak_generic_client_protocol_mapper_test.go
new file mode 100644
index 0000000..6784e61
--- /dev/null
+++ b/provider/resource_keycloak_generic_client_protocol_mapper_test.go
@@ -0,0 +1,218 @@
+package provider
+
+import (
+	"fmt"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"testing"
+
+	"github.com/hashicorp/terraform/helper/acctest"
+	"github.com/hashicorp/terraform/helper/resource"
+	"github.com/hashicorp/terraform/terraform"
+)
+
+const (
+	TF_RESOURCE_NAME = "client_protocol_mapper"
+)
+
+func TestAccKeycloakGenericClientProtocolMapper_basicClient(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-generic-client-protocol-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_generic_client_protocol_mapper." + TF_RESOURCE_NAME
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakGenericClientProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakGenericClientProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakGenericClientProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakGenericClientProtocolMapper_import(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-generic-client-protocol-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_generic_client_protocol_mapper." + TF_RESOURCE_NAME
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakGenericClientProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakGenericClientProtocolMapper_import(realmName, clientId, mapperName),
+				Check:  testKeycloakGenericClientProtocolMapperExists(resourceName),
+			},
+			{
+				ResourceName:      resourceName,
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getGenericProtocolMapperIdForClient(resourceName),
+			},
+		},
+	})
+}
+
+func TestGenericClientProtocolMapper_update(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-generic-client-protocol-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_generic_client_protocol_mapper." + TF_RESOURCE_NAME
+
+	oldAttributeName := "attribute-name-" + acctest.RandString(10)
+	oldAttributeValue := "attribute-name-" + acctest.RandString(10)
+	newAttributeName := "attribute-value-" + acctest.RandString(10)
+	newAttributeValue := "attribute-value-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakGenericClientProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakGenericClientProtocolMapper_update(realmName, clientId, mapperName, oldAttributeName, oldAttributeValue),
+				Check:  testKeycloakGenericClientProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakGenericClientProtocolMapper_update(realmName, clientId, mapperName, newAttributeName, newAttributeValue),
+				Check: resource.ComposeTestCheckFunc(
+					testKeycloakGenericClientProtocolMapperExists(resourceName),
+					resource.TestCheckResourceAttr(resourceName, "config.attribute.name", newAttributeName),
+					resource.TestCheckResourceAttr(resourceName, "config.attribute.value", newAttributeValue)),
+			},
+		},
+	})
+}
+
+/*  =================================================================================================================
+    Helper functions
+    ================================================================================================================= */
+func testAccKeycloakGenericClientProtocolMapperDestroy() resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		for resourceName, rs := range state.RootModule().Resources {
+			if rs.Type != "keycloak_generic_client_protocol_mapper" {
+				continue
+			}
+
+			mapper, _ := getGenericClientProtocolMapperUsingState(state, resourceName)
+
+			if mapper != nil {
+				return fmt.Errorf("generic client protocol mapper with id %s still exists", rs.Primary.ID)
+			}
+		}
+
+		return nil
+	}
+}
+
+func getGenericClientProtocolMapperUsingState(state *terraform.State, resourceName string) (*keycloak.GenericClientProtocolMapper, 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
+	realmId := rs.Primary.Attributes["realm_id"]
+	clientId := rs.Primary.Attributes["client_id"]
+
+	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+	return keycloakClient.GetGenericClientProtocolMapper(realmId, clientId, id)
+}
+
+func testKeycloakGenericClientProtocolMapper_basic_client(realmName string, clientId string, 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_generic_client_protocol_mapper" "%s" {
+  client_id       = "${keycloak_saml_client.saml_client.id}"
+  name            = "%s"
+  protocol        = "saml"
+  protocol_mapper = "saml-hardcode-attribute-mapper"
+  realm_id        = "${keycloak_realm.realm.id}"
+  config = {
+    "attribute.name"       = "name"
+    "attribute.nameformat" = "Basic"
+    "attribute.value"      = "value"
+    "friendly.name"        = "%s"
+  }
+}`, realmName, clientId, TF_RESOURCE_NAME, mapperName, mapperName)
+}
+
+func testKeycloakGenericClientProtocolMapper_import(realmName string, clientId string, 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_generic_client_protocol_mapper" "%s" {
+  client_id       = "${keycloak_saml_client.saml_client.id}"
+  name            = "%s"
+  protocol        = "saml"
+  protocol_mapper = "saml-hardcode-attribute-mapper"
+  realm_id        = "${keycloak_realm.realm.id}"
+  config = {
+    "attribute.name"       = "name"
+    "attribute.nameformat" = "Basic"
+    "attribute.value"      = "value"
+    "friendly.name"        = "%s"
+  }
+}`, realmName, clientId, TF_RESOURCE_NAME, mapperName, mapperName)
+}
+
+func testKeycloakGenericClientProtocolMapper_update(realmName string, clientId string, mapperName string, attributeName string, attributeValue 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_generic_client_protocol_mapper" "%s" {
+  client_id       = "${keycloak_saml_client.saml_client.id}"
+  name            = "%s"
+  protocol        = "saml"
+  protocol_mapper = "saml-hardcode-attribute-mapper"
+  realm_id        = "${keycloak_realm.realm.id}"
+  config = {
+    "attribute.name"       = "%s"
+    "attribute.nameformat" = "Basic"
+    "attribute.value"      = "%s"
+    "friendly.name"        = "%s"
+  }
+}`, realmName, clientId, TF_RESOURCE_NAME, mapperName, attributeName, attributeValue, mapperName)
+}
+
+func testKeycloakGenericClientProtocolMapperExists(resourceName string) resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		_, err := getGenericClientProtocolMapperUsingState(state, resourceName)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}