Repository: mrparkers/terraform-provider-keycloak
Pull request #202 (2020-01-23 14:49:40)

Service account realm roles support
This PR adds support to add a realm-role to a service account. 

With `resource_keycloak_openid_client_service_account_role` it was not possible to add realm-roles. Maybe this is not the best solution, but it works. 

I added `resource_keycloak_openid_client_service_account_realm_role`.
Currently it is only possible to add a single role per resource. There are no docs yet. 

Maybe `resource_keycloak_openid_client_service_account_role` and `resource_keycloak_openid_client_service_account_realm_role` could be merged. 

mrparkers: Yeah, I agree that these two resources should eventually be merged, but there really isn't a clean way to do that right now.  Unfortunately, the way I decided to handle resource IDs when I first started writing this provider makes it difficult to use them them practically.  In this case, they could have be used to determine the difference between a realm role and a client role, so another resource wouldn't be needed.

At some point this year I plan to release a 2.0 version that completely reworks how resource IDs are treated which should simplify the code a lot, and allow for this to be condensed into a single resource.

For now I'll merge this for you so you can get the value you need.  Thanks for the PR!

Patch:
diff --git a/keycloak/openid_client_service_account_realm_role.go b/keycloak/openid_client_service_account_realm_role.go
new file mode 100644
index 0000000..1a50273
--- /dev/null
+++ b/keycloak/openid_client_service_account_realm_role.go
@@ -0,0 +1,59 @@
+package keycloak
+
+import (
+	"fmt"
+)
+
+type OpenidClientServiceAccountRealmRole struct {
+	Id                   string `json:"id"`
+	RealmId              string `json:"-"`
+	ServiceAccountUserId string `json:"-"`
+	Name                 string `json:"name,omitempty"`
+	Description          string `json:"description"`
+}
+
+func (keycloakClient *KeycloakClient) NewOpenidClientServiceAccountRealmRole(serviceAccountRole *OpenidClientServiceAccountRealmRole) error {
+	serviceAccountRoles := []OpenidClientServiceAccountRealmRole{*serviceAccountRole}
+
+	_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/users/%s/role-mappings/realm", serviceAccountRole.RealmId, serviceAccountRole.ServiceAccountUserId), serviceAccountRoles)
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) DeleteOpenidClientServiceAccountRealmRole(realm, serviceAccountUserId, roleId string) error {
+	serviceAccountRole, err := keycloakClient.GetOpenidClientServiceAccountRealmRole(realm, serviceAccountUserId, roleId)
+	if err != nil {
+		return err
+	}
+	serviceAccountRoles := []OpenidClientServiceAccountRealmRole{*serviceAccountRole}
+	err = keycloakClient.delete(fmt.Sprintf("/realms/%s/users/%s/role-mappings/realm", realm, serviceAccountUserId), &serviceAccountRoles)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) GetOpenidClientServiceAccountRealmRole(realm, serviceAccountUserId, roleId string) (*OpenidClientServiceAccountRealmRole, error) {
+	serviceAccountRoles := []OpenidClientServiceAccountRealmRole{
+		{
+			Id:                   roleId,
+			RealmId:              realm,
+			ServiceAccountUserId: serviceAccountUserId,
+		},
+	}
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/users/%s/role-mappings/realm", realm, serviceAccountUserId), &serviceAccountRoles, nil)
+	if err != nil {
+		return nil, err
+	}
+	for _, serviceAccountRole := range serviceAccountRoles {
+		if serviceAccountRole.Id == roleId {
+			serviceAccountRole.RealmId = realm
+			serviceAccountRole.ServiceAccountUserId = serviceAccountUserId
+			return &serviceAccountRole, nil
+		}
+	}
+	return &OpenidClientServiceAccountRealmRole{}, nil
+}
diff --git a/provider/provider.go b/provider/provider.go
index d113364..77130c7 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -58,6 +58,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_openid_client_authorization_scope":               resourceKeycloakOpenidClientAuthorizationScope(),
 			"keycloak_openid_client_authorization_permission":          resourceKeycloakOpenidClientAuthorizationPermission(),
 			"keycloak_openid_client_service_account_role":              resourceKeycloakOpenidClientServiceAccountRole(),
+			"keycloak_openid_client_service_account_realm_role":        resourceKeycloakOpenidClientServiceAccountRealmRole(),
 			"keycloak_role":                                            resourceKeycloakRole(),
 		},
 		Schema: map[string]*schema.Schema{
diff --git a/provider/resource_keycloak_openid_client_service_account_realm_role.go b/provider/resource_keycloak_openid_client_service_account_realm_role.go
new file mode 100644
index 0000000..880b61f
--- /dev/null
+++ b/provider/resource_keycloak_openid_client_service_account_realm_role.go
@@ -0,0 +1,125 @@
+package provider
+
+import (
+	"fmt"
+	"github.com/hashicorp/terraform/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"strings"
+)
+
+func resourceKeycloakOpenidClientServiceAccountRealmRole() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceKeycloakOpenidClientServiceAccountRealmRoleCreate,
+		Read:   resourceKeycloakOpenidClientServiceAccountRealmRoleRead,
+		Delete: resourceKeycloakOpenidClientServiceAccountRealmRoleDelete,
+		Importer: &schema.ResourceImporter{
+			State: resourceKeycloakOpenidClientServiceAccountRealmRoleImport,
+		},
+		Schema: map[string]*schema.Schema{
+			"service_account_user_id": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"role": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+		},
+	}
+}
+
+func getOpenidClientServiceAccountRealmRoleFromData(data *schema.ResourceData, keycloakClient *keycloak.KeycloakClient) (*keycloak.OpenidClientServiceAccountRealmRole, error) {
+	roleName := data.Get("role").(string)
+	realmId := data.Get("realm_id").(string)
+	serviceAccountRoleId := data.Get("service_account_user_id").(string)
+
+	role, err := keycloakClient.GetRoleByName(realmId, "", roleName)
+	if err != nil {
+		if keycloak.ErrorIs404(err) {
+			role = &keycloak.Role{Id: ""}
+		} else {
+			return nil, err
+		}
+	}
+
+	return &keycloak.OpenidClientServiceAccountRealmRole{
+		Id:                   role.Id,
+		Name:                 roleName,
+		RealmId:              realmId,
+		ServiceAccountUserId: serviceAccountRoleId,
+	}, nil
+}
+
+func setOpenidClientServiceAccountRealmRoleData(data *schema.ResourceData, serviceAccountRole *keycloak.OpenidClientServiceAccountRealmRole) {
+	data.SetId(fmt.Sprintf("%s/%s", serviceAccountRole.ServiceAccountUserId, serviceAccountRole.Id))
+	data.Set("realm_id", serviceAccountRole.RealmId)
+	data.Set("service_account_user_id", serviceAccountRole.ServiceAccountUserId)
+	data.Set("role", serviceAccountRole.Name)
+}
+
+func resourceKeycloakOpenidClientServiceAccountRealmRoleCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+	serviceAccountRole, err := getOpenidClientServiceAccountRealmRoleFromData(data, keycloakClient)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.NewOpenidClientServiceAccountRealmRole(serviceAccountRole)
+	if err != nil {
+		return err
+	}
+	setOpenidClientServiceAccountRealmRoleData(data, serviceAccountRole)
+	return resourceKeycloakOpenidClientServiceAccountRealmRoleRead(data, meta)
+}
+
+func resourceKeycloakOpenidClientServiceAccountRealmRoleRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	serviceAccountRole, err := getOpenidClientServiceAccountRealmRoleFromData(data, keycloakClient)
+	if err != nil {
+		return err
+	}
+
+	serviceAccountRole, err = keycloakClient.GetOpenidClientServiceAccountRealmRole(serviceAccountRole.RealmId, serviceAccountRole.ServiceAccountUserId, serviceAccountRole.Id)
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	setOpenidClientServiceAccountRealmRoleData(data, serviceAccountRole)
+
+	return nil
+}
+
+func resourceKeycloakOpenidClientServiceAccountRealmRoleDelete(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	serviceAccountRole, err := getOpenidClientServiceAccountRealmRoleFromData(data, keycloakClient)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.DeleteOpenidClientServiceAccountRealmRole(serviceAccountRole.RealmId, serviceAccountRole.ServiceAccountUserId, serviceAccountRole.Id)
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+	return nil
+}
+
+func resourceKeycloakOpenidClientServiceAccountRealmRoleImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
+	parts := strings.Split(d.Id(), "/")
+	if len(parts) != 2 {
+		return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{serviceAccountUserId}}/{{roleId}}")
+	}
+	d.Set("realm_id", parts[0])
+	d.Set("service_account_user_id", parts[1])
+	d.SetId(fmt.Sprintf("%s/%s", parts[1], parts[2]))
+
+	return []*schema.ResourceData{d}, nil
+}


Test patch:
diff --git a/provider/resource_keycloak_openid_client_service_account_realm_role_test.go b/provider/resource_keycloak_openid_client_service_account_realm_role_test.go
new file mode 100644
index 0000000..443194f
--- /dev/null
+++ b/provider/resource_keycloak_openid_client_service_account_realm_role_test.go
@@ -0,0 +1,177 @@
+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"
+	"strings"
+	"testing"
+)
+
+func TestAccKeycloakOpenidClientServiceAccountRealmRole_basic(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	clientId := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOpenidClientServiceAccountRealmRoleDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenidClientServiceAccountRealmRole_basic(realmName, clientId),
+				Check:  testAccCheckKeycloakOpenidClientServiceAccountRealmRoleExists("keycloak_openid_client_service_account_realm_role.test"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenidClientServiceAccountRealmRole_createAfterManualDestroy(t *testing.T) {
+	var serviceAccountRole = &keycloak.OpenidClientServiceAccountRealmRole{}
+
+	realmName := "terraform-" + acctest.RandString(10)
+	clientId := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOpenidClientServiceAccountRealmRoleDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenidClientServiceAccountRealmRole_basic(realmName, clientId),
+				Check:  testAccCheckKeycloakOpenidClientServiceAccountRealmRoleFetch("keycloak_openid_client_service_account_realm_role.test", serviceAccountRole),
+			},
+			{
+				PreConfig: func() {
+					keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+					err := keycloakClient.DeleteOpenidClientServiceAccountRealmRole(serviceAccountRole.RealmId, serviceAccountRole.ServiceAccountUserId, serviceAccountRole.Id)
+					if err != nil {
+						t.Fatal(err)
+					}
+				},
+				Config: testKeycloakOpenidClientServiceAccountRealmRole_basic(realmName, clientId),
+				Check:  testAccCheckKeycloakOpenidClientServiceAccountRealmRoleExists("keycloak_openid_client_service_account_realm_role.test"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenidClientServiceAccountRealmRole_basicUpdateRealm(t *testing.T) {
+	firstRealm := "terraform-" + acctest.RandString(10)
+	secondRealm := "terraform-" + acctest.RandString(10)
+	clientId := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOpenidClientServiceAccountRealmRoleDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenidClientServiceAccountRealmRole_basic(firstRealm, clientId),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakOpenidClientServiceAccountRealmRoleExists("keycloak_openid_client_service_account_realm_role.test"),
+					resource.TestCheckResourceAttr("keycloak_openid_client_service_account_realm_role.test", "realm_id", firstRealm),
+				),
+			},
+			{
+				Config: testKeycloakOpenidClientServiceAccountRealmRole_basic(secondRealm, clientId),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakOpenidClientServiceAccountRealmRoleExists("keycloak_openid_client_service_account_realm_role.test"),
+					resource.TestCheckResourceAttr("keycloak_openid_client_service_account_realm_role.test", "realm_id", secondRealm),
+				),
+			},
+		},
+	})
+}
+
+func testAccCheckKeycloakOpenidClientServiceAccountRealmRoleExists(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		_, err := getKeycloakOpenidClientServiceAccountRealmRoleFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakOpenidClientServiceAccountRealmRoleFetch(resourceName string, serviceAccountRole *keycloak.OpenidClientServiceAccountRealmRole) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		fetchedServiceAccountRole, err := getKeycloakOpenidClientServiceAccountRealmRoleFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		serviceAccountRole.ServiceAccountUserId = fetchedServiceAccountRole.ServiceAccountUserId
+		serviceAccountRole.RealmId = fetchedServiceAccountRole.RealmId
+		serviceAccountRole.Id = fetchedServiceAccountRole.Id
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakOpenidClientServiceAccountRealmRoleDestroy() resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		for _, rs := range s.RootModule().Resources {
+			if rs.Type != "keycloak_openid_client_service_account_realm_role" {
+				continue
+			}
+
+			realmId := rs.Primary.Attributes["realm_id"]
+			serviceAccountUserId := rs.Primary.Attributes["service_account_user_id"]
+			id := strings.Split(rs.Primary.ID, "/")[1]
+
+			keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+			serviceAccountRole, _ := keycloakClient.GetOpenidClientServiceAccountRealmRole(realmId, serviceAccountUserId, id)
+			if serviceAccountRole != nil {
+				return fmt.Errorf("service account role exists")
+			}
+		}
+
+		return nil
+	}
+}
+
+func getKeycloakOpenidClientServiceAccountRealmRoleFromState(s *terraform.State, resourceName string) (*keycloak.OpenidClientServiceAccountRealmRole, error) {
+	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+	rs, ok := s.RootModule().Resources[resourceName]
+	if !ok {
+		return nil, fmt.Errorf("resource not found: %s", resourceName)
+	}
+
+	realmId := rs.Primary.Attributes["realm_id"]
+	serviceAccountUserId := rs.Primary.Attributes["service_account_user_id"]
+	id := strings.Split(rs.Primary.ID, "/")[1]
+
+	serviceAccountRole, err := keycloakClient.GetOpenidClientServiceAccountRealmRole(realmId, serviceAccountUserId, id)
+	if err != nil {
+		return nil, fmt.Errorf("error getting service account role mapping: %s", err)
+	}
+
+	return serviceAccountRole, nil
+}
+
+func testKeycloakOpenidClientServiceAccountRealmRole_basic(realm, clientId string) string {
+	return fmt.Sprintf(`
+resource keycloak_realm test {
+	realm = "%s"
+}
+
+resource keycloak_openid_client test {
+	client_id                = "%s"
+	realm_id                 = "${keycloak_realm.test.id}"
+	access_type              = "CONFIDENTIAL"
+	service_accounts_enabled = true
+}
+
+resource keycloak_openid_client_service_account_realm_role test {
+	service_account_user_id = "${keycloak_openid_client.test.service_account_user_id}"
+	realm_id 					= "${keycloak_realm.test.id}"
+	role 						= "offline_access"
+}
+	`, realm, clientId)
+}