diff --git a/docs/resources/group_memberships.md b/docs/resources/group_memberships.md index f18dedc5..cf1da2d9 100644 --- a/docs/resources/group_memberships.md +++ b/docs/resources/group_memberships.md @@ -16,6 +16,8 @@ Also note that you should not use `keycloak_group_memberships` with a group has This resource **should not** be used to control membership of a group that has its members federated from an external source via group mapping. +To non-exclusivly manage the group's of a user, see the [`keycloak_user_groups` resource][1] + ## Example Usage ```hcl @@ -54,3 +56,5 @@ resource "keycloak_group_memberships" "group_members" { This resource does not support import. Instead of importing, feel free to create this resource as if it did not already exist on the server. + +[1]: /docs/resources/user_groups.html diff --git a/docs/resources/user_groups.md b/docs/resources/user_groups.md new file mode 100644 index 00000000..4f42ea2b --- /dev/null +++ b/docs/resources/user_groups.md @@ -0,0 +1,95 @@ +--- +page_title: "keycloak_user_groups Resource" +--- + +# keycloak\_user\_groups Resource + +Allows for managing a Keycloak user's groups. + +If `exhaustive` is true, this resource attempts to be an **authoritative** source over user groups: groups that are manually added to the user will be removed, and groups that are manually removed from the user group will be added upon the next run of `terraform apply`. +If `exhaustive` is false, this resource is a partial assignation of groups to a user. As a result, you can get multiple `keycloak_user_groups` for the same `user_id`. + + +## Example Usage (exhaustive groups) +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_group" "group" { + realm_id = keycloak_realm.realm.id + name = "foo" +} + +resource "keycloak_user" "user" { + realm_id = keycloak_realm.realm.id + username = "my-user" +} + +resource "keycloak_user_groups" "user_groups" { + realm_id = keycloak_realm.realm.id + user_id = keycloak_user.user.id + + group_ids = [ + keycloak_group.group.id + ] +} + +``` + +## Example Usage (non exhaustive groups) +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_group" "group_foo" { + realm_id = keycloak_realm.realm.id + name = "foo" +} + +resource "keycloak_group" "group_bar" { + realm_id = keycloak_realm.realm.id + name = "bar" +} + +resource "keycloak_user" "user" { + realm_id = keycloak_realm.realm.id + username = "my-user" +} + +resource "keycloak_user_groups" "user_groups_association_1" { + realm_id = keycloak_realm.realm.id + user_id = keycloak_user.user.id + exhaustive = false + + group_ids = [ + keycloak_group.group_foo.id + ] +} + +resource "keycloak_user_groups" "user_groups_association_1" { + realm_id = keycloak_realm.realm.id + user_id = keycloak_user.user.id + exhaustive = false + + group_ids = [ + keycloak_group.group_bar.id + ] +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm this group exists in. +- `user_id` - (Required) The ID of the user this resource should manage groups for. +- `group_ids` - (Required) A list of group IDs that the user is member of. +- `exhaustive` - (Optional) + +## Import + +This resource does not support import. Instead of importing, feel free to create this resource +as if it did not already exist on the server. + diff --git a/keycloak/user.go b/keycloak/user.go index 3dd39cde..583e26b8 100644 --- a/keycloak/user.go +++ b/keycloak/user.go @@ -160,6 +160,17 @@ func (keycloakClient *KeycloakClient) GetUserByUsername(realmId, username string return nil, nil } +func (keycloakClient *KeycloakClient) GetUserGroups(realmId, userId string) ([]*Group, error) { + var groups []*Group + err := keycloakClient.get(fmt.Sprintf("/realms/%s/users/%s/groups/", realmId, userId), &groups, nil) + + if err != nil { + return nil, err + } + + return groups, nil +} + func (keycloakClient *KeycloakClient) addUserToGroup(user *User, groupId string) error { return keycloakClient.put(fmt.Sprintf("/realms/%s/users/%s/groups/%s", user.RealmId, user.Id, groupId), nil) } @@ -205,3 +216,31 @@ func (keycloakClient *KeycloakClient) RemoveUsersFromGroup(realmId, groupId stri return nil } + +func (keycloakClient *KeycloakClient) AddUserToGroups(groupIds []string, userId string, realmId string) error { + for _, groupId := range groupIds { + var user User + user.Id = userId + user.RealmId = realmId + err := keycloakClient.addUserToGroup(&user, groupId) + + if err != nil { + return err + } + } + return nil +} + +func (keycloakClient *KeycloakClient) RemoveUserFromGroups(groupIds []string, userId string, realmId string) error { + for _, groupId := range groupIds { + var user User + user.Id = userId + user.RealmId = realmId + err := keycloakClient.RemoveUserFromGroup(&user, groupId) + + if err != nil { + return err + } + } + return nil +} diff --git a/provider/provider.go b/provider/provider.go index 892434d0..9a454e7e 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -97,6 +97,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_identity_provider_token_exchange_scope_permission": resourceKeycloakIdentityProviderTokenExchangeScopePermission(), "keycloak_openid_client_permissions": resourceKeycloakOpenidClientPermissions(), "keycloak_users_permissions": resourceKeycloakUsersPermissions(), + "keycloak_user_groups": resourceKeycloakUserGroups(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/provider/resource_keycloak_user_groups.go b/provider/resource_keycloak_user_groups.go new file mode 100644 index 00000000..efc1abe9 --- /dev/null +++ b/provider/resource_keycloak_user_groups.go @@ -0,0 +1,146 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "strings" +) + +func resourceKeycloakUserGroups() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakUserGroupsReconcile, + Read: resourceKeycloakUserGroupsRead, + Delete: resourceKeycloakUserGroupsDelete, + Update: resourceKeycloakUserGroupsReconcile, + // This resource can be imported using {{realm}}/{{userId}}. + Importer: &schema.ResourceImporter{ + State: resourceKeycloakUserGroupsImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "user_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "group_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Required: true, + }, + "exhaustive": { + Type: schema.TypeBool, + Default: true, + Optional: true, + }, + }, + } +} + +func resourceKeycloakUserGroupsRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + userId := data.Get("user_id").(string) + groupIds := data.Get("group_ids").(*schema.Set) + exhaustive := data.Get("exhaustive").(bool) + + userGroups, err := keycloakClient.GetUserGroups(realmId, userId) + if err != nil { + return handleNotFoundError(err, data) + } + + var groups []string + for _, group := range userGroups { + //only add groups that we care about + if exhaustive || groupIds.Contains(group.Id) { + groups = append(groups, group.Id) + } + } + + data.Set("group_ids", groups) + data.SetId(userGroupsId(realmId, userId)) + + return nil +} + +func resourceKeycloakUserGroupsReconcile(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + userId := data.Get("user_id").(string) + groupIds := interfaceSliceToStringSlice(data.Get("group_ids").(*schema.Set).List()) + exhaustive := data.Get("exhaustive").(bool) + + if data.HasChange("group_ids") { + o, n := data.GetChange("group_ids") + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := interfaceSliceToStringSlice(os.Difference(ns).List()) + + if err := keycloakClient.RemoveUserFromGroups(remove, userId, realmId); err != nil { + return err + } + } + + userGroups, err := keycloakClient.GetUserGroups(realmId, userId) + if err != nil { + return err + } + + var userGroupsIds []string + for _, group := range userGroups { + userGroupsIds = append(userGroupsIds, group.Id) + } + + remove := stringArrayDifference(userGroupsIds, groupIds) + add := stringArrayDifference(groupIds, userGroupsIds) + + if err := keycloakClient.AddUserToGroups(add, userId, realmId); err != nil { + return err + } + + if exhaustive { + if err := keycloakClient.RemoveUserFromGroups(remove, userId, realmId); err != nil { + return err + } + } + + data.SetId(userGroupsId(realmId, userId)) + return resourceKeycloakUserGroupsRead(data, meta) +} + +func resourceKeycloakUserGroupsDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + userId := data.Get("user_id").(string) + groupIds := interfaceSliceToStringSlice(data.Get("group_ids").(*schema.Set).List()) + + return keycloakClient.RemoveUserFromGroups(groupIds, userId, realmId) +} + +func resourceKeycloakUserGroupsImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import format: {{realm}}/{{userId}}.") + } + + d.Set("realm_id", parts[0]) + d.Set("user_id", parts[1]) + + d.SetId(userGroupsId(parts[0], parts[1])) + + return []*schema.ResourceData{d}, nil +} + +func userGroupsId(realmId, userId string) string { + return fmt.Sprintf("%s/%s", realmId, userId) +} diff --git a/provider/resource_keycloak_user_groups_test.go b/provider/resource_keycloak_user_groups_test.go new file mode 100644 index 00000000..fe40ebcc --- /dev/null +++ b/provider/resource_keycloak_user_groups_test.go @@ -0,0 +1,535 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "regexp" + "testing" +) + +func TestAccKeycloakUserGroups_basic(t *testing.T) { + t.Parallel() + + groupName := acctest.RandomWithPrefix("tf-acc") + userName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakUserGroups_basic(groupName, userName), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + { + ResourceName: "keycloak_user_groups.user_groups", + ImportState: true, + ImportStateVerify: true, + }, + // check destroy + { + Config: testKeycloakUserGroups_noUserGroups(groupName, userName), + Check: testAccCheckKeycloakUserHasNoGroups("keycloak_user.user"), + }, + }, + }) +} + +func TestAccKeycloakUserGroups_basicNonExhaustive(t *testing.T) { + t.Parallel() + + groupName := acctest.RandomWithPrefix("tf-acc") + userName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakUserGroups_nonExhaustive(groupName, userName), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + { + ResourceName: "keycloak_user_groups.user_groups", + ImportState: true, + ImportStateVerify: true, + }, + // check destroy + { + Config: testKeycloakUserGroups_noUserGroups(groupName, userName), + Check: testAccCheckKeycloakUserHasNoGroups("keycloak_user.user"), + }, + }, + }) +} + +func TestAccKeycloakUserGroups_update(t *testing.T) { + t.Parallel() + + userName := acctest.RandomWithPrefix("tf-acc") + groupName := acctest.RandomWithPrefix("tf-acc") + + allGroupIds := []string{ + "${keycloak_group.group1.id}", + "${keycloak_group.group2.id}", + "${keycloak_group.group3.id}", + "${keycloak_group.group4.id}", + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + // initial setup, resource is defined but no roles are specified + { + Config: testKeycloakUserGroups_update(userName, groupName, []string{}), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // add all roles + { + Config: testKeycloakUserGroups_update(userName, groupName, allGroupIds), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // remove some + { + Config: testKeycloakUserGroups_update(userName, groupName, []string{ + "${keycloak_group.group3.id}", + "${keycloak_group.group4.id}", + }), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // add some and remove some + { + Config: testKeycloakUserGroups_update(userName, groupName, []string{ + "${keycloak_group.group1.id}", + "${keycloak_group.group4.id}", + }), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // add some and remove some again + { + Config: testKeycloakUserGroups_update(userName, groupName, []string{ + "${keycloak_group.group1.id}", + "${keycloak_group.group2.id}", + "${keycloak_group.group3.id}", + }), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // add all back + { + Config: testKeycloakUserGroups_update(userName, groupName, allGroupIds), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // random scenario 1 + { + Config: testKeycloakUserGroups_update(userName, groupName, randomStringSliceSubset(allGroupIds)), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // random scenario 2 + { + Config: testKeycloakUserGroups_update(userName, groupName, randomStringSliceSubset(allGroupIds)), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // random scenario 3 + { + Config: testKeycloakUserGroups_update(userName, groupName, randomStringSliceSubset(allGroupIds)), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + // remove all + { + Config: testKeycloakUserGroups_update(userName, groupName, []string{}), + Check: testAccCheckKeycloakUserHasGroups("keycloak_user_groups.user_groups"), + }, + }, + }) +} + +func TestAccKeycloakUserGroups_updateNonExhaustive(t *testing.T) { + t.Parallel() + + userName := acctest.RandomWithPrefix("tf-acc") + groupName := acctest.RandomWithPrefix("tf-acc") + + allGroupIdsSet1 := []string{ + "${keycloak_group.group1.id}", + "${keycloak_group.group2.id}", + "${keycloak_group.group3.id}", + } + + allGroupIdsSet2 := []string{ + "${keycloak_group.group4.id}", + "${keycloak_group.group5.id}", + "${keycloak_group.group6.id}", + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + // initial setup, resource is defined but no roles are specified + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, []string{}, []string{}), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2")), + }, + // add all roles + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, allGroupIdsSet1, allGroupIdsSet2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2"))}, + // remove some + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, []string{ + "${keycloak_group.group3.id}", + }, allGroupIdsSet2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2"))}, + // add some and remove some + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, []string{ + "${keycloak_group.group1.id}", + "${keycloak_group.group2.id}", + }, allGroupIdsSet2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2"))}, + // add some and remove some again + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, []string{ + "${keycloak_group.group1.id}", + "${keycloak_group.group3.id}", + }, allGroupIdsSet2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2"))}, + // add all back + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, allGroupIdsSet1, allGroupIdsSet2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2"))}, + // random scenario 1 + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, randomStringSliceSubset(allGroupIdsSet1), randomStringSliceSubset(allGroupIdsSet2)), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2"))}, + // remove all + { + Config: testKeycloakUserGroups_updateNonExhaustive(userName, groupName, []string{}, []string{}), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_1"), + testAccCheckKeycloakUserHasNonExhaustiveGroups("keycloak_user_groups.user_groups_2")), + }, + }, + }) +} + +func testAccCheckKeycloakUserHasGroups(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realm := rs.Primary.Attributes["realm_id"] + userId := rs.Primary.Attributes["user_id"] + + var expectedGroups []*keycloak.Group + for k, v := range rs.Primary.Attributes { + if match, _ := regexp.MatchString("group_ids\\.[^#]+", k); !match { + continue + } + + group, err := keycloakClient.GetGroup(realm, v) + if err != nil { + return err + } + + expectedGroups = append(expectedGroups, group) + } + + userGroups, err := keycloakClient.GetUserGroups(realm, userId) + if err != nil { + return err + } + + if len(userGroups) != len(expectedGroups) { + return fmt.Errorf("expected number of user groups to be %d, got %d", len(expectedGroups), len(userGroups)) + } + + for _, expectedGroup := range expectedGroups { + + found := false + + for _, group := range userGroups { + if group.Id == expectedGroup.Id { + found = true + break + } + } + + if !found { + return fmt.Errorf("expected to find group %s assigned to user %s", expectedGroup.Id, userId) + } + } + + return nil + } +} + +func testAccCheckKeycloakUserHasNonExhaustiveGroups(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realm := rs.Primary.Attributes["realm_id"] + userId := rs.Primary.Attributes["user_id"] + + var expectedGroups []*keycloak.Group + for k, v := range rs.Primary.Attributes { + if match, _ := regexp.MatchString("group_ids\\.[^#]+", k); !match { + continue + } + + group, err := keycloakClient.GetGroup(realm, v) + if err != nil { + return err + } + + expectedGroups = append(expectedGroups, group) + } + + userGroups, err := keycloakClient.GetUserGroups(realm, userId) + if err != nil { + return err + } + + if len(userGroups) < len(expectedGroups) { + return fmt.Errorf("expected number of user groups to be greater or equals to %d, got %d", len(expectedGroups), len(userGroups)) + } + + for _, expectedGroup := range expectedGroups { + + found := false + + for _, group := range userGroups { + if group.Id == expectedGroup.Id { + found = true + break + } + } + + if !found { + return fmt.Errorf("expected to find group %s assigned to user %s", expectedGroup.Id, userId) + } + } + + return nil + } +} + +func testAccCheckKeycloakUserHasNoGroups(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realm := rs.Primary.Attributes["realm_id"] + id := rs.Primary.ID + + userGroups, err := keycloakClient.GetUserGroups(realm, id) + if err != nil { + return err + } + + if len(userGroups) != 0 { + return fmt.Errorf("expected user %s to have no groups", id) + } + + return nil + } +} + +func testKeycloakUserGroups_basic(groupName, userName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_group" "group" { + realm_id = data.keycloak_realm.realm.id + name = "%s" +} + +resource "keycloak_user" "user" { + realm_id = data.keycloak_realm.realm.id + username = "%s" +} + +resource "keycloak_user_groups" "user_groups" { + realm_id = data.keycloak_realm.realm.id + user_id = keycloak_user.user.id + group_ids = [ + keycloak_group.group.id + ] +} + `, testAccRealm.Realm, groupName, userName) +} + +func testKeycloakUserGroups_noUserGroups(groupName, userName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_group" "group" { + realm_id = data.keycloak_realm.realm.id + name = "%s" +} + +resource "keycloak_user" "user" { + realm_id = data.keycloak_realm.realm.id + username = "%s" +} + `, testAccRealm.Realm, groupName, userName) +} + +func testKeycloakUserGroups_nonExhaustive(groupName, userName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_group" "group" { + realm_id = data.keycloak_realm.realm.id + name = "%s" +} + +resource "keycloak_user" "user" { + realm_id = data.keycloak_realm.realm.id + username = "%s" +} + +resource "keycloak_user_groups" "user_groups" { + realm_id = data.keycloak_realm.realm.id + user_id = keycloak_user.user.id + group_ids = [ + keycloak_group.group.id + ] + exhaustive = false +} + `, testAccRealm.Realm, groupName, userName) +} + +func testKeycloakUserGroups_update(userName, groupName string, groupIds []string) string { + tfGroupIds := fmt.Sprintf("group_ids = %s", arrayOfStringsForTerraformResource(groupIds)) + + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_group" "group1" { + realm_id = data.keycloak_realm.realm.id + name = "%s1" +} + +resource "keycloak_group" "group2" { + realm_id = data.keycloak_realm.realm.id + name = "%s2" +} + +resource "keycloak_group" "group3" { + realm_id = data.keycloak_realm.realm.id + name = "%s3" +} + +resource "keycloak_group" "group4" { + realm_id = data.keycloak_realm.realm.id + name = "%s4" +} + +resource "keycloak_user" "user" { + realm_id = data.keycloak_realm.realm.id + username = "%s" +} + +resource "keycloak_user_groups" "user_groups" { + realm_id = data.keycloak_realm.realm.id + user_id = keycloak_user.user.id + %s +} + `, testAccRealm.Realm, userName, groupName, groupName, groupName, groupName, tfGroupIds) +} + +func testKeycloakUserGroups_updateNonExhaustive(userName, groupName string, groupIds1, groupIds2 []string) string { + tfGroupIds1 := fmt.Sprintf("group_ids = %s", arrayOfStringsForTerraformResource(groupIds1)) + tfGroupIds2 := fmt.Sprintf("group_ids = %s", arrayOfStringsForTerraformResource(groupIds2)) + + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_group" "group1" { + realm_id = data.keycloak_realm.realm.id + name = "%s1" +} + +resource "keycloak_group" "group2" { + realm_id = data.keycloak_realm.realm.id + name = "%s2" +} + +resource "keycloak_group" "group3" { + realm_id = data.keycloak_realm.realm.id + name = "%s3" +} + +resource "keycloak_group" "group4" { + realm_id = data.keycloak_realm.realm.id + name = "%s4" +} + +resource "keycloak_group" "group5" { + realm_id = data.keycloak_realm.realm.id + name = "%s5" +} + +resource "keycloak_group" "group6" { + realm_id = data.keycloak_realm.realm.id + name = "%s6" +} + +resource "keycloak_user" "user" { + realm_id = data.keycloak_realm.realm.id + username = "%s" +} + +resource "keycloak_user_groups" "user_groups_1" { + realm_id = data.keycloak_realm.realm.id + user_id = keycloak_user.user.id + + exhaustive = false + %s +} + +resource "keycloak_user_groups" "user_groups_2" { + realm_id = data.keycloak_realm.realm.id + user_id = keycloak_user.user.id + + exhaustive = false + %s +} + `, testAccRealm.Realm, userName, groupName, groupName, groupName, groupName, groupName, groupName, tfGroupIds1, tfGroupIds2) +} diff --git a/provider/utils.go b/provider/utils.go index 1a884ffc..64c3eb46 100644 --- a/provider/utils.go +++ b/provider/utils.go @@ -73,3 +73,24 @@ func interfaceSliceToStringSlice(iv []interface{}) []string { return sv } + +func stringArrayDifference(a, b []string) []string { + var aWithoutB []string + + for _, s := range a { + if !stringArrayContains(b, s) { + aWithoutB = append(aWithoutB, s) + } + } + + return aWithoutB +} + +func stringArrayContains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +}