Repository: mrparkers/terraform-provider-keycloak
Pull request #199 (2020-01-17 17:18:41)

Add support for group attributes
This PR adds support for group attributes following user attribute implementation.

AdrienFromToulouse: closes https://github.com/mrparkers/terraform-provider-keycloak/issues/194

Patch:
diff --git a/docs/resources/keycloak_group.md b/docs/resources/keycloak_group.md
index e7feb95..f6f03b3 100644
--- a/docs/resources/keycloak_group.md
+++ b/docs/resources/keycloak_group.md
@@ -6,6 +6,8 @@ Groups provide a logical wrapping for users within Keycloak. Users within a
 group can share attributes and roles, and group membership can be mapped
 to a claim.
 
+Attributes can also be defined on Groups.
+
 Groups can also be federated from external data sources, such as LDAP or Active Directory.
 This resource **should not** be used to manage groups that were created this way.
 
@@ -27,6 +29,16 @@ resource "keycloak_group" "child_group" {
     parent_id = "${keycloak_group.parent_group.id}"
     name      = "child-group"
 }
+
+resource "keycloak_group" "child_group_with_optional_attributes" {
+    realm_id  = "${keycloak_realm.realm.id}"
+    parent_id = "${keycloak_group.parent_group.id}"
+    name      = "child-group-with-optional-attributes"
+    attributes = {
+		"key1" = "value1"
+		"key2" = "value2"
+    }
+}
 ```
 
 ### Argument Reference
@@ -36,6 +48,7 @@ The following arguments are supported:
 - `realm_id` - (Required) The realm this group exists in.
 - `parent_id` - (Optional) The ID of this group's parent. If omitted, this group will be defined at the root level.
 - `name` - (Required) The name of the group.
+- `attributes` - (Optional) A dict of key/value pairs to set as custom attributes for the group.
 
 ### Attributes Reference
 
diff --git a/keycloak/group.go b/keycloak/group.go
index 138185d..e4d2998 100644
--- a/keycloak/group.go
+++ b/keycloak/group.go
@@ -14,6 +14,7 @@ type Group struct {
 	SubGroups   []*Group            `json:"subGroups,omitempty"`
 	RealmRoles  []string            `json:"realmRoles,omitempty"`
 	ClientRoles map[string][]string `json:"clientRoles,omitempty"`
+	Attributes  map[string][]string `json:"attributes"`
 }
 
 /*
diff --git a/provider/resource_keycloak_group.go b/provider/resource_keycloak_group.go
index 73d4e9d..97e4aba 100644
--- a/provider/resource_keycloak_group.go
+++ b/provider/resource_keycloak_group.go
@@ -36,28 +36,43 @@ func resourceKeycloakGroup() *schema.Resource {
 				Type:     schema.TypeString,
 				Computed: true,
 			},
+			"attributes": {
+				Type:     schema.TypeMap,
+				Optional: true,
+			},
 		},
 	}
 }
 
 func mapFromDataToGroup(data *schema.ResourceData) *keycloak.Group {
+	attributes := map[string][]string{}
+	if v, ok := data.GetOk("attributes"); ok {
+		for key, value := range v.(map[string]interface{}) {
+			attributes[key] = splitLen(value.(string), MAX_ATTRIBUTE_VALUE_LEN)
+		}
+	}
+
 	group := &keycloak.Group{
-		Id:       data.Id(),
-		RealmId:  data.Get("realm_id").(string),
-		ParentId: data.Get("parent_id").(string),
-		Name:     data.Get("name").(string),
+		Id:         data.Id(),
+		RealmId:    data.Get("realm_id").(string),
+		ParentId:   data.Get("parent_id").(string),
+		Name:       data.Get("name").(string),
+		Attributes: attributes,
 	}
 
 	return group
 }
 
 func mapFromGroupToData(data *schema.ResourceData, group *keycloak.Group) {
+	attributes := map[string]string{}
+	for k, v := range group.Attributes {
+		attributes[k] = strings.Join(v, "")
+	}
 	data.SetId(group.Id)
-
 	data.Set("realm_id", group.RealmId)
 	data.Set("name", group.Name)
 	data.Set("path", group.Path)
-
+	data.Set("attributes", attributes)
 	if group.ParentId != "" {
 		data.Set("parent_id", group.ParentId)
 	}


Test patch:
diff --git a/provider/resource_keycloak_group_test.go b/provider/resource_keycloak_group_test.go
index 52502ee..e1445a5 100644
--- a/provider/resource_keycloak_group_test.go
+++ b/provider/resource_keycloak_group_test.go
@@ -6,12 +6,15 @@ import (
 	"github.com/hashicorp/terraform/helper/resource"
 	"github.com/hashicorp/terraform/terraform"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+	"strings"
 	"testing"
 )
 
 func TestAccKeycloakGroup_basic(t *testing.T) {
 	realmName := "terraform-" + acctest.RandString(10)
 	groupName := "terraform-group-" + acctest.RandString(10)
+	attributeName := "terraform-attribute-" + acctest.RandString(10)
+	attributeValue := acctest.RandString(250)
 
 	resource.Test(t, resource.TestCase{
 		Providers:    testAccProviders,
@@ -19,7 +22,7 @@ func TestAccKeycloakGroup_basic(t *testing.T) {
 		CheckDestroy: testAccCheckKeycloakGroupDestroy(),
 		Steps: []resource.TestStep{
 			{
-				Config: testKeycloakGroup_basic(realmName, groupName),
+				Config: testKeycloakGroup_basic(realmName, groupName, attributeName, attributeValue),
 				Check:  testAccCheckKeycloakGroupExists("keycloak_group.group"),
 			},
 			{
@@ -37,6 +40,8 @@ func TestAccKeycloakGroup_createAfterManualDestroy(t *testing.T) {
 
 	realmName := "terraform-" + acctest.RandString(10)
 	groupName := "terraform-group-" + acctest.RandString(10)
+	attributeName := "terraform-attribute-" + acctest.RandString(10)
+	attributeValue := acctest.RandString(250)
 
 	resource.Test(t, resource.TestCase{
 		Providers:    testAccProviders,
@@ -44,7 +49,7 @@ func TestAccKeycloakGroup_createAfterManualDestroy(t *testing.T) {
 		CheckDestroy: testAccCheckKeycloakGroupDestroy(),
 		Steps: []resource.TestStep{
 			{
-				Config: testKeycloakGroup_basic(realmName, groupName),
+				Config: testKeycloakGroup_basic(realmName, groupName, attributeName, attributeValue),
 				Check: resource.ComposeTestCheckFunc(
 					testAccCheckKeycloakGroupExists("keycloak_group.group"),
 					testAccCheckKeycloakGroupFetch("keycloak_group.group", group),
@@ -59,7 +64,7 @@ func TestAccKeycloakGroup_createAfterManualDestroy(t *testing.T) {
 						t.Fatal(err)
 					}
 				},
-				Config: testKeycloakGroup_basic(realmName, groupName),
+				Config: testKeycloakGroup_basic(realmName, groupName, attributeName, attributeValue),
 				Check:  testAccCheckKeycloakGroupExists("keycloak_group.group"),
 			},
 		},
@@ -71,6 +76,8 @@ func TestAccKeycloakGroup_updateGroupName(t *testing.T) {
 
 	groupNameBefore := "terraform-group-" + acctest.RandString(10)
 	groupNameAfter := "terraform-group-" + acctest.RandString(10)
+	attributeName := "terraform-attribute-" + acctest.RandString(10)
+	attributeValue := acctest.RandString(250)
 
 	resource.Test(t, resource.TestCase{
 		Providers:    testAccProviders,
@@ -78,14 +85,14 @@ func TestAccKeycloakGroup_updateGroupName(t *testing.T) {
 		CheckDestroy: testAccCheckKeycloakGroupDestroy(),
 		Steps: []resource.TestStep{
 			{
-				Config: testKeycloakGroup_basic(realmName, groupNameBefore),
+				Config: testKeycloakGroup_basic(realmName, groupNameBefore, attributeName, attributeValue),
 				Check: resource.ComposeTestCheckFunc(
 					testAccCheckKeycloakGroupExists("keycloak_group.group"),
 					resource.TestCheckResourceAttr("keycloak_group.group", "name", groupNameBefore),
 				),
 			},
 			{
-				Config: testKeycloakGroup_basic(realmName, groupNameAfter),
+				Config: testKeycloakGroup_basic(realmName, groupNameAfter, attributeName, attributeValue),
 				Check: resource.ComposeTestCheckFunc(
 					testAccCheckKeycloakGroupExists("keycloak_group.group"),
 					resource.TestCheckResourceAttr("keycloak_group.group", "name", groupNameAfter),
@@ -167,6 +174,41 @@ func TestAccKeycloakGroup_nested(t *testing.T) {
 	})
 }
 
+func TestAccKeycloakGroup_unsetOptionalAttributes(t *testing.T) {
+	attributeName := "terraform-attribute-" + acctest.RandString(10)
+	groupWithOptionalAttributes := &keycloak.Group{
+		RealmId: "terraform-" + acctest.RandString(10),
+		Name:    "terraform-group-" + acctest.RandString(10),
+		Attributes: map[string][]string{
+			attributeName: {
+				acctest.RandString(230),
+				acctest.RandString(12),
+			},
+		},
+	}
+
+	resourceName := "keycloak_group.group"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakUserDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakGroup_fromInterface(groupWithOptionalAttributes),
+				Check:  testAccCheckKeycloakGroupExists(resourceName),
+			},
+			{
+				Config: testKeycloakGroup_basic(groupWithOptionalAttributes.RealmId, groupWithOptionalAttributes.Name, attributeName, strings.Join(groupWithOptionalAttributes.Attributes[attributeName], "")),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakGroupExists(resourceName),
+					resource.TestCheckResourceAttr(resourceName, "name", groupWithOptionalAttributes.Name),
+				),
+			},
+		},
+	})
+}
+
 func testAccCheckKeycloakGroupExists(resourceName string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		_, err := getGroupFromState(s, resourceName)
@@ -248,7 +290,7 @@ func getGroupFromState(s *terraform.State, resourceName string) (*keycloak.Group
 	return group, nil
 }
 
-func testKeycloakGroup_basic(realm, group string) string {
+func testKeycloakGroup_basic(realm, group string, attributeName string, attributeValue string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
 	realm = "%s"
@@ -257,8 +299,11 @@ resource "keycloak_realm" "realm" {
 resource "keycloak_group" "group" {
 	name     = "%s"
 	realm_id = "${keycloak_realm.realm.id}"
+	attributes = {
+		"%s" = "%s"
+	}
 }
-	`, realm, group)
+	`, realm, group, attributeName, attributeValue)
 }
 
 func testKeycloakGroup_updateRealmBefore(realmOne, realmTwo, group string) string {
@@ -319,3 +364,16 @@ resource "keycloak_group" "second_child_group" {
 }
 	`, realm, parentGroup, firstChildGroup, secondChildGroup, secondChildGroupParent)
 }
+
+func testKeycloakGroup_fromInterface(group *keycloak.Group) string {
+	return fmt.Sprintf(`
+	resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_group" "group" {
+	realm_id   = "${keycloak_realm.realm.id}"
+	name   = "%s"
+}
+	`, group.RealmId, group.Name)
+}