Repository: mrparkers/terraform-provider-keycloak
Pull request #311 (2020-06-02 16:23:06)

Use the terraform-plugin-sdk user agent string with running tf version
Http requests now using the user-agent built by the terraform-plugin-sdk using the running terraform version string instead of the default Go user agent.

Before: `Go-http-client/1.1`
After: `HashiCorp Terraform/0.12.25 (+https://www.terraform.io) Terraform Plugin SDK/1.6.0`


Starting with Terraform version v.0.12 the running version Terraform can be obtained from the provider. However this field is not included in prior versions of Terraform and the running version cannot be discovered. As a way to support older versions, the version string of "0.11+compatible" is used. This was inspired by the way the aws provider plugin works, https://github.com/terraform-providers/terraform-provider-aws/commit/0e183c1c5919a6cc1727b829e7a6bf1968f8b265

Fix for issue #276.
 

mrparkers: thanks for this, this is a good change.  I made one small update to compute the user agent in the `provider` package, since I want to keep the `keycloak` package free of anything terraform related so it can be used outside of the context of this provider.

Patch:
diff --git a/docs/resources/keycloak_generic_client_role_mapper.md b/docs/resources/keycloak_generic_client_role_mapper.md
index 123bb02..fc9bd20 100644
--- a/docs/resources/keycloak_generic_client_role_mapper.md
+++ b/docs/resources/keycloak_generic_client_role_mapper.md
@@ -7,7 +7,7 @@ the token or assertion. When `full_scope_allowed` is set to `false` for a
 client, role scope mapping allows you to limit the roles that get declared
 inside an access token for a client.
 
-### Example Usage (Realm role)
+### Example Usage (Realm Role to Client)
 
 ```hcl
 resource "keycloak_realm" "realm" {
@@ -38,8 +38,7 @@ resource "keycloak_generic_client_role_mapper" "client_role_mapper" {
 }
 ```
 
-### Example Usage (Client role)
-
+### Example Usage (Client Role to Client)
 
 ```hcl
 resource "keycloak_realm" "realm" {
@@ -88,11 +87,75 @@ resource "keycloak_generic_client_role_mapper" "client_b_role_mapper" {
 }
 ```
 
+### Example Usage (Realm Role to Client Scope)
+
+```hcl
+resource "keycloak_realm" "realm" {
+  realm   = "my-realm"
+  enabled = true
+}
+
+resource "keycloak_openid_client_scope" "client_scope" {
+  realm_id  = keycloak_realm.realm.id
+  name      = "my-client-scope"
+}
+
+resource "keycloak_role" "realm_role" {
+  realm_id    = keycloak_realm.realm.id
+  name        = "my-realm-role"
+  description = "My Realm Role"
+}
+
+resource "keycloak_generic_client_role_mapper" "client_role_mapper" {
+  realm_id        = keycloak_realm.realm.id
+  client_scope_id = keycloak_openid_client_scope.client_scope.id
+  role_id         = keycloak_role.realm_role.id
+}
+```
+
+### Example Usage (Client Role to Client Scope)
+
+```hcl
+resource "keycloak_realm" "realm" {
+  realm   = "my-realm"
+  enabled = true
+}
+
+resource "keycloak_openid_client" "client" {
+  realm_id  = keycloak_realm.realm.id
+  client_id = "client"
+  name      = "client"
+
+  enabled = true
+
+  access_type = "BEARER-ONLY"
+}
+
+resource "keycloak_role" "client_role" {
+  realm_id    = keycloak_realm.realm.id
+  client_id   = keycloak_openid_client.client.id
+  name        = "my-client-role"
+  description = "My Client Role"
+}
+
+resource "keycloak_openid_client_scope" "client_scope" {
+  realm_id  = keycloak_realm.realm.id
+  name      = "my-client-scope"
+}
+
+resource "keycloak_generic_client_role_mapper" "client_b_role_mapper" {
+  realm_id        = keycloak_realm.realm.id
+  client_scope_id = keycloak_client_scope.client_scope.id
+  role_id         = keycloak_role.client_role.id
+}
+```
+
 ### Argument Reference
 
 The following arugments are supported:
 
 - `realm_id` - (Required) The realm this role mapper exists within
-- `client_id` - (Required) The ID of the client this role mapper is created for
+- `client_id` - (Optional) The ID of the client this role mapper is added to
+- `client_scope_id` - (Optional) The ID of the client scope this role mapper is added to
 - `role_id` - (Required) The ID of the role to be added to this role mapper
 
diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go
index 1c55b22..ee8638b 100644
--- a/keycloak/keycloak_client.go
+++ b/keycloak/keycloak_client.go
@@ -25,6 +25,7 @@ type KeycloakClient struct {
 	clientCredentials *ClientCredentials
 	httpClient        *http.Client
 	initialLogin      bool
+	userAgent         string
 }
 
 type ClientCredentials struct {
@@ -43,7 +44,7 @@ const (
 	tokenUrl = "%s/auth/realms/%s/protocol/openid-connect/token"
 )
 
-func NewKeycloakClient(baseUrl, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool) (*KeycloakClient, error) {
+func NewKeycloakClient(baseUrl, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool, userAgent string) (*KeycloakClient, error) {
 	cookieJar, err := cookiejar.New(&cookiejar.Options{
 		PublicSuffixList: publicsuffix.List,
 	})
@@ -91,6 +92,7 @@ func NewKeycloakClient(baseUrl, clientId, clientSecret, realm, username, passwor
 		httpClient:        httpClient,
 		initialLogin:      initialLogin,
 		realm:             realm,
+		userAgent:         userAgent,
 	}
 
 	if keycloakClient.initialLogin {
@@ -112,6 +114,10 @@ func (keycloakClient *KeycloakClient) login() error {
 	accessTokenRequest, _ := http.NewRequest(http.MethodPost, accessTokenUrl, strings.NewReader(accessTokenData.Encode()))
 	accessTokenRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 
+	if keycloakClient.userAgent != "" {
+		accessTokenRequest.Header.Set("User-Agent", keycloakClient.userAgent)
+	}
+
 	accessTokenResponse, err := keycloakClient.httpClient.Do(accessTokenRequest)
 	if err != nil {
 		return err
@@ -146,6 +152,10 @@ func (keycloakClient *KeycloakClient) refresh() error {
 	refreshTokenRequest, _ := http.NewRequest(http.MethodPost, refreshTokenUrl, strings.NewReader(refreshTokenData.Encode()))
 	refreshTokenRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 
+	if keycloakClient.userAgent != "" {
+		refreshTokenRequest.Header.Set("User-Agent", keycloakClient.userAgent)
+	}
+
 	refreshTokenResponse, err := keycloakClient.httpClient.Do(refreshTokenRequest)
 	if err != nil {
 		return err
@@ -204,6 +214,10 @@ func (keycloakClient *KeycloakClient) addRequestHeaders(request *http.Request) {
 	request.Header.Set("Authorization", fmt.Sprintf("%s %s", tokenType, accessToken))
 	request.Header.Set("Accept", "application/json")
 
+	if keycloakClient.userAgent != "" {
+		request.Header.Set("User-Agent", keycloakClient.userAgent)
+	}
+
 	if request.Method == http.MethodPost || request.Method == http.MethodPut || request.Method == http.MethodDelete {
 		request.Header.Set("Content-type", "application/json")
 	}
diff --git a/provider/provider.go b/provider/provider.go
index de1020f..acbfaaa 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -2,11 +2,12 @@ package provider
 
 import (
 	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/httpclient"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
 )
 
 func KeycloakProvider() *schema.Provider {
-	return &schema.Provider{
+	provider := &schema.Provider{
 		DataSourcesMap: map[string]*schema.Resource{
 			"keycloak_group":                              dataSourceKeycloakGroup(),
 			"keycloak_openid_client":                      dataSourceKeycloakOpenidClient(),
@@ -138,11 +139,25 @@ func KeycloakProvider() *schema.Provider {
 				Default:     false,
 			},
 		},
-		ConfigureFunc: configureKeycloakProvider,
 	}
+
+	provider.ConfigureFunc = func(d *schema.ResourceData) (interface{}, error) {
+		terraformVersion := provider.TerraformVersion
+		if terraformVersion == "" {
+			// Terraform 0.12 introduced this field to the protocol
+			// We can therefore assume that if it's missing it's 0.10 or 0.11
+			terraformVersion = "0.11+compatible"
+		}
+
+		userAgent := httpclient.TerraformUserAgent(terraformVersion)
+
+		return configureKeycloakProvider(d, userAgent)
+	}
+
+	return provider
 }
 
-func configureKeycloakProvider(data *schema.ResourceData) (interface{}, error) {
+func configureKeycloakProvider(data *schema.ResourceData, userAgent string) (interface{}, error) {
 	url := data.Get("url").(string)
 	clientId := data.Get("client_id").(string)
 	clientSecret := data.Get("client_secret").(string)
@@ -154,5 +169,5 @@ func configureKeycloakProvider(data *schema.ResourceData) (interface{}, error) {
 	tlsInsecureSkipVerify := data.Get("tls_insecure_skip_verify").(bool)
 	rootCaCertificate := data.Get("root_ca_certificate").(string)
 
-	return keycloak.NewKeycloakClient(url, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify)
+	return keycloak.NewKeycloakClient(url, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify, userAgent)
 }
diff --git a/provider/resource_keycloak_generic_client_role_mapper.go b/provider/resource_keycloak_generic_client_role_mapper.go
index 9b39c87..cbdc60f 100644
--- a/provider/resource_keycloak_generic_client_role_mapper.go
+++ b/provider/resource_keycloak_generic_client_role_mapper.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
@@ -12,7 +13,9 @@ func resourceKeycloakGenericClientRoleMapper() *schema.Resource {
 		Create: resourceKeycloakGenericClientRoleMapperCreate,
 		Read:   resourceKeycloakGenericClientRoleMapperRead,
 		Delete: resourceKeycloakGenericClientRoleMapperDelete,
-
+		Importer: &schema.ResourceImporter{
+			State: resourceKeycloakGenericClientRoleMapperImport,
+		},
 		Schema: map[string]*schema.Schema{
 			"realm_id": {
 				Type:        schema.TypeString,
@@ -108,3 +111,27 @@ func resourceKeycloakGenericClientRoleMapperDelete(data *schema.ResourceData, me
 
 	return keycloakClient.DeleteRoleScopeMapping(realmId, clientId, clientScopeId, role)
 }
+
+func resourceKeycloakGenericClientRoleMapperImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
+	parts := strings.Split(d.Id(), "/")
+
+	if len(parts) != 6 {
+		return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/client/{{clientId}}/scope-mappings/{{roleClientId}}/{{roleId}}, {{realmId}}/client-scope/{{clientScopeId}}/scope-mappings/{{roleClientId}}/{{roleId}}")
+	}
+
+	parentResourceType := parts[1]
+	parentResourceId := parts[2]
+
+	d.Set("realm_id", parts[0])
+
+	if parentResourceType == "client" {
+		d.Set("client_id", parentResourceId)
+	} else if parentResourceType == "client-scope" {
+		d.Set("client_scope_id", parentResourceId)
+	} else {
+		return nil, fmt.Errorf("the associated parent resource must be either a client or a client-scope")
+	}
+
+	d.Set("role_id", parts[5])
+	return []*schema.ResourceData{d}, nil
+}


Test patch:
diff --git a/keycloak/keycloak_client_test.go b/keycloak/keycloak_client_test.go
index 7c33c9f..57a0deb 100644
--- a/keycloak/keycloak_client_test.go
+++ b/keycloak/keycloak_client_test.go
@@ -51,7 +51,7 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) {
 		t.Fatal("KEYCLOAK_CLIENT_TIMEOUT must be an integer")
 	}
 
-	keycloakClient, err := NewKeycloakClient(os.Getenv("KEYCLOAK_URL"), os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, clientTimeout, "", false)
+	keycloakClient, err := NewKeycloakClient(os.Getenv("KEYCLOAK_URL"), os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, clientTimeout, "", false, "")
 	if err != nil {
 		t.Fatalf("%s", err)
 	}
diff --git a/provider/provider_test.go b/provider/provider_test.go
index 49bf3a0..f4a4244 100644
--- a/provider/provider_test.go
+++ b/provider/provider_test.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/httpclient"
 	"github.com/hashicorp/terraform-plugin-sdk/terraform"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
 	"os"
@@ -25,7 +26,7 @@ func init() {
 		"keycloak": testAccProvider,
 	}
 
-	keycloakClient, _ = keycloak.NewKeycloakClient(os.Getenv("KEYCLOAK_URL"), os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", false)
+	keycloakClient, _ = keycloak.NewKeycloakClient(os.Getenv("KEYCLOAK_URL"), os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", false, httpclient.TerraformUserAgent(testAccProvider.TerraformVersion))
 }
 
 func TestProvider(t *testing.T) {
diff --git a/provider/resource_keycloak_generic_client_role_mapper_test.go b/provider/resource_keycloak_generic_client_role_mapper_test.go
index 7983f5c..f299bb7 100644
--- a/provider/resource_keycloak_generic_client_role_mapper_test.go
+++ b/provider/resource_keycloak_generic_client_role_mapper_test.go
@@ -2,11 +2,12 @@ package provider
 
 import (
 	"fmt"
+	"testing"
+
 	"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/terraform"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"testing"
 )
 
 func TestGenericRoleMapper_basic(t *testing.T) {
@@ -27,6 +28,32 @@ func TestGenericRoleMapper_basic(t *testing.T) {
 	})
 }
 
+func TestGenericRoleMapper_import(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	parentClientName := "client1-" + acctest.RandString(10)
+	parentRoleName := "role-" + acctest.RandString(10)
+	childClientName := "client2-" + acctest.RandString(10)
+
+	resourceName := "keycloak_generic_client_role_mapper.child-client-with-parent-client-role"
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakGenericRoleMapping_basic(realmName, parentClientName, parentRoleName, childClientName),
+				Check:  testAccCheckKeycloakScopeMappingExists(resourceName),
+			},
+			{
+				ResourceName:      resourceName,
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getGenericRoleMapperId(resourceName),
+			},
+		},
+	})
+}
+
 func TestGenericRoleMapperClientScope_basic(t *testing.T) {
 	realmName := "terraform-" + acctest.RandString(10)
 	clientName := "client-" + acctest.RandString(10)
@@ -45,6 +72,32 @@ func TestGenericRoleMapperClientScope_basic(t *testing.T) {
 	})
 }
 
+func TestGenericRoleMapperClientScope_import(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	clientName := "client-" + acctest.RandString(10)
+	roleName := "role-" + acctest.RandString(10)
+	clientScopeName := "clientscope-" + acctest.RandString(10)
+
+	resourceName := "keycloak_generic_client_role_mapper.clientscope-with-client-role"
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakGenericRoleMappingClientScope_basic(realmName, clientName, roleName, clientScopeName),
+				Check:  testAccCheckKeycloakScopeMappingExists(resourceName),
+			},
+			{
+				ResourceName:      resourceName,
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getGenericRoleMapperId(resourceName),
+			},
+		},
+	})
+}
+
 func TestGenericRoleMapper_createAfterManualDestroy(t *testing.T) {
 	var role = &keycloak.Role{}
 	var childClient = &keycloak.GenericClient{}
@@ -259,3 +312,14 @@ func getOpenidClientScopeFromState(s *terraform.State, resourceName string) (*ke
 
 	return client, nil
 }
+
+func getGenericRoleMapperId(resourceName string) resource.ImportStateIdFunc {
+	return func(s *terraform.State) (string, error) {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return "", fmt.Errorf("resource not found: %s", resourceName)
+		}
+
+		return rs.Primary.ID, nil
+	}
+}