Skip to content

Commit

Permalink
Add user groups resource (#2)
Browse files Browse the repository at this point in the history
* implement resource_keycloak_user_groups
* implement exhaustive mode of keycloak_user_groups
* also remove groups when they are removed from the resource. Even if the user_groups resource is non-exhaustive

Co-authored-by: Benedikt <benedikt.suessmann@sva.de>
  • Loading branch information
StatueFungus and StatueFungus authored Mar 29, 2021
1 parent ec8e50a commit b3f52b0
Show file tree
Hide file tree
Showing 7 changed files with 841 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/resources/group_memberships.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
95 changes: 95 additions & 0 deletions docs/resources/user_groups.md
Original file line number Diff line number Diff line change
@@ -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.

39 changes: 39 additions & 0 deletions keycloak/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
146 changes: 146 additions & 0 deletions provider/resource_keycloak_user_groups.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit b3f52b0

Please sign in to comment.