Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add keycloak_groups_permissions for admin_fine_grained_authz #617

Merged
merged 5 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/resources/group_permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
page_title: "keycloak_group_permissions Resource"
---

# keycloak_group_permissions

Allows you to manage all group Scope Based Permissions https://www.keycloak.org/docs/latest/server_admin/#group.

This is part of a preview Keycloak feature: `admin_fine_grained_authz` (see https://www.keycloak.org/docs/latest/server_admin/#_fine_grain_permissions).
This feature can be enabled with the Keycloak option `-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled`. See the
example [`docker-compose.yml`](https://github.com/mrparkers/terraform-provider-keycloak/blob/898094df6b3e01c3404981ce7ca268142d6ff0e5/docker-compose.yml#L21) file for an example.

When enabling Roles Permissions, Keycloak does several things automatically:
1. Enable Authorization on built-in `realm-management` client (if not already enabled).
1. Create a resource representing the role permissions.
1. Create scopes `view`, `manage`, `view-members`, `manage-members`, `manage-membership`.
1. Create all scope based permission for the scopes and role resource


### Example Usage

```hcl
resource "keycloak_realm" "realm" {
realm = "my_realm"
}

data "keycloak_openid_client" "realm_management" {
realm_id = keycloak_realm.realm.id
client_id = "realm-management"
}

resource "keycloak_openid_client_permissions" "realm-management_permission" {
realm_id = keycloak_realm.realm.id
client_id = data.keycloak_openid_client.realm_management.id
}

resource "keycloak_group" "group" {
realm_id = keycloak_realm.realm.id
name = "%s"
}

resource "keycloak_openid_client_group_policy" "test" {
realm_id = keycloak_realm.realm.id
resource_server_id = data.keycloak_openid_client.realm_management.id
name = "client_group_policy_test"
groups {
id = keycloak_group.group.id
path = keycloak_group.group.path
extend_children = false
}
logic = "POSITIVE"
decision_strategy = "UNANIMOUS"
depends_on = [
keycloak_openid_client_permissions.realm-management_permission,
]
}

resource "keycloak_group_permissions" "test" {
realm_id = keycloak_realm.realm.id
group_id = keycloak_group.group.id
manage_members_scope {
policies = [
keycloak_openid_client_group_policy.test.id
]
description = "mangage_members_scope"
decision_strategy = "UNANIMOUS"
}

}
```

### Argument Reference

The following arguments are supported:

- `realm_id` - (Required) The realm in which to manage fine-grained role permissions.
- `group_id` - (Required) The id of the group.


Each of the scopes that can be managed are defined below:

- `view_scope` - (Optional) Policies that decide if the admin can view information about the group.
- `manage_scope` - (Optional) Policies that decide if the admin can manage the configuration of the group.
- `view_members_scope` - (Optional) Policies that decide if the admin can view the user details of members of the group.
- `manage_members_scope` - (Optional) Policies that decide if the admin can manage the users that belong to this group.
- `manage_membership_scope` - (Optional) Policies that decide if an admin can change the membership of the group. Add or remove members from the group.

The configuration block for each of these scopes supports the following arguments:

- `policies` - (Optional) Assigned policies to the permission. Each element within this list should be a policy ID.
- `description` - (Optional) Description of the permission.
- `decision_strategy` - (Optional) Decision strategy of the permission.

### Attributes Reference

In addition to the arguments listed above, the following computed attributes are exported:

- `enabled` - When true, this indicates that fine-grained role permissions are enabled. This will always be `true`.
- `authorization_resource_server_id` - Resource server id representing the realm management client on which these permissions are managed.
39 changes: 39 additions & 0 deletions keycloak/group_permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package keycloak

import (
"fmt"
)

type GroupPermissionsInput struct {
Enabled bool `json:"enabled"`
}

type GroupPermissions struct {
RealmId string `json:"-"`
GroupId string `json:"-"`
Enabled bool `json:"enabled"`
Resource string `json:"resource"`
ScopePermissions map[string]interface{} `json:"scopePermissions"`
}

func (keycloakClient *KeycloakClient) EnableGroupPermissions(realmId, groupId string) error {
return keycloakClient.put(fmt.Sprintf("/realms/%s/groups/%s/management/permissions", realmId, groupId), GroupPermissionsInput{Enabled: true})
}

func (keycloakClient *KeycloakClient) DisableGroupPermissions(realmId, groupId string) error {
return keycloakClient.put(fmt.Sprintf("/realms/%s/groups/%s/management/permissions", realmId, groupId), GroupPermissionsInput{Enabled: false})
}

func (keycloakClient *KeycloakClient) GetGroupPermissions(realmId, groupId string) (*GroupPermissions, error) {
var groupPermissions GroupPermissions

err := keycloakClient.get(fmt.Sprintf("/realms/%s/groups/%s/management/permissions", realmId, groupId), &groupPermissions, nil)
if err != nil {
return nil, err
}

groupPermissions.RealmId = realmId
groupPermissions.GroupId = groupId

return &groupPermissions, nil
}
1 change: 1 addition & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
"keycloak_openid_client_permissions": resourceKeycloakOpenidClientPermissions(),
"keycloak_users_permissions": resourceKeycloakUsersPermissions(),
"keycloak_user_groups": resourceKeycloakUserGroups(),
"keycloak_group_permissions": resourceKeycloakGroupPermissions(),
},
Schema: map[string]*schema.Schema{
"client_id": {
Expand Down
188 changes: 188 additions & 0 deletions provider/resource_keycloak_group_permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package provider

import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
)

func resourceKeycloakGroupPermissions() *schema.Resource {
return &schema.Resource{
Create: resourceKeycloakGroupPermissionsCreate,
Read: resourceKeycloakGroupPermissionsRead,
Delete: resourceKeycloakGroupPermissionsDelete,
Update: resourceKeycloakGroupPermissionsUpdate,
Importer: &schema.ResourceImporter{
State: resourceKeycloakGroupPermissionsImport,
},
Schema: map[string]*schema.Schema{
"realm_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"enabled": {
Type: schema.TypeBool,
Computed: true,
},
"group_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"authorization_resource_server_id": {
Type: schema.TypeString,
Computed: true,
Description: "Resource server id representing the realm management client on which this permission is managed",
},
"view_scope": scopePermissionsSchema(),
"manage_scope": scopePermissionsSchema(),
"view_members_scope": scopePermissionsSchema(),
"manage_members_scope": scopePermissionsSchema(),
"manage_membership_scope": scopePermissionsSchema(),
},
}
}

func groupPermissionsId(realmId, groupId string) string {
return fmt.Sprintf("%s/%s", realmId, groupId)
}

func resourceKeycloakGroupPermissionsCreate(data *schema.ResourceData, meta interface{}) error {
return resourceKeycloakGroupPermissionsUpdate(data, meta)
}

func resourceKeycloakGroupPermissionsUpdate(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
groupId := data.Get("group_id").(string)

// the existence of this resource implies that it is enabled.
err := keycloakClient.EnableGroupPermissions(realmId, groupId)
if err != nil {
return err
}

// setting scope permissions requires us to fetch the users permissions details, as well as the realm management client
groupPermissions, err := keycloakClient.GetGroupPermissions(realmId, groupId)
if err != nil {
return err
}

realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management")
if err != nil {
return err
}

if viewScope, ok := data.GetOk("view_scope"); ok {
err := setOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["view"].(string), viewScope.(*schema.Set))
if err != nil {
return err
}
}
if manageScope, ok := data.GetOk("manage_scope"); ok {
err := setOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["manage"].(string), manageScope.(*schema.Set))
if err != nil {
return err
}
}
if viewMembersScope, ok := data.GetOk("view_members_scope"); ok {
err := setOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["view-members"].(string), viewMembersScope.(*schema.Set))
if err != nil {
return err
}
}
if manageMembersScope, ok := data.GetOk("manage_members_scope"); ok {
err := setOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["manage-members"].(string), manageMembersScope.(*schema.Set))
if err != nil {
return err
}
}
if manageMembershipScope, ok := data.GetOk("manage_membership_scope"); ok {
err := setOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["manage-membership"].(string), manageMembershipScope.(*schema.Set))
if err != nil {
return err
}
}

return resourceKeycloakGroupPermissionsRead(data, meta)
}

func resourceKeycloakGroupPermissionsRead(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)
realmId := data.Get("realm_id").(string)
groupId := data.Get("group_id").(string)

realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management")
if err != nil {
return err
}

groupPermissions, err := keycloakClient.GetGroupPermissions(realmId, groupId)
if err != nil {
return handleNotFoundError(err, data)
}

data.SetId(groupPermissionsId(groupPermissions.RealmId, groupPermissions.GroupId))
data.Set("realm_id", groupPermissions.RealmId)
data.Set("group_id", groupPermissions.GroupId)
data.Set("enabled", groupPermissions.Enabled)
data.Set("authorization_resource_server_id", realmManagementClient.Id)

if viewScope, err := getOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["view"].(string)); err == nil && viewScope != nil {
data.Set("view_scope", []interface{}{viewScope})
} else if err != nil {
return err
}

if manageScope, err := getOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["manage"].(string)); err == nil && manageScope != nil {
data.Set("manage_scope", []interface{}{manageScope})
} else if err != nil {
return err
}

if viewMembersScope, err := getOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["view-members"].(string)); err == nil && viewMembersScope != nil {
data.Set("view_members_scope", []interface{}{viewMembersScope})
} else if err != nil {
return err
}

if manageMembersScope, err := getOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["manage-members"].(string)); err == nil && manageMembersScope != nil {
data.Set("manage_members_scope", []interface{}{manageMembersScope})
} else if err != nil {
return err
}

if manageMembershipScope, err := getOpenidClientScopePermissionPolicy(keycloakClient, realmId, realmManagementClient.Id, groupPermissions.ScopePermissions["manage-membership"].(string)); err == nil && manageMembershipScope != nil {
data.Set("manage_membership_scope", []interface{}{manageMembershipScope})
} else if err != nil {
return err
}

return nil
}

func resourceKeycloakGroupPermissionsDelete(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
groupId := data.Get("group_id").(string)

return keycloakClient.DisableGroupPermissions(realmId, groupId)
}

func resourceKeycloakGroupPermissionsImport(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}}/{{groupId}}")
}
d.Set("realm_id", parts[0])
d.Set("group_id", parts[1])

d.SetId(groupPermissionsId(parts[0], parts[1]))

return []*schema.ResourceData{d}, nil
}
Loading