diff --git a/docs/resources/roles_permissions.md b/docs/resources/roles_permissions.md new file mode 100644 index 000000000..0c53a9aed --- /dev/null +++ b/docs/resources/roles_permissions.md @@ -0,0 +1,91 @@ +--- +page_title: "keycloak_role_permissions Resource" +--- + +# keycloak_role_permissions + +Allows you to manage all role Scope Based Permissions https://www.keycloak.org/docs/latest/server_admin/#role. + +This is part of a preview keycloak feature. You need to enable this feature to be able to use this resource. +More information about enabling the preview feature can be found here: https://www.keycloak.org/docs/latest/server_installation/#profiles + +When enabling Users Permissions, Keycloak does several things automatically: +1. Enable Authorization on build-in realm-management client (if not already enabled) +1. Create a resource representing the role permissions +1. Create scopes "map-role", "map-role-client-scope", "map-role-composite" +1. Create all scope based permission for the scopes and role resource + +If the realm-management Authorization is not enable, you have to ceate a dependency (`depends_on`) with the policy and the role. + +### Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "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 + enabled = true +} + +resource keycloak_user test { + realm_id = keycloak_realm.realm.id + username = "test-user" + + email = "test-user@fakedomain.com" + first_name = "Testy" + last_name = "Tester" +} + +resource keycloak_openid_client_user_policy test { + resource_server_id = "${data.keycloak_openid_client.realm_management.id}" + realm_id = keycloak_realm.realm.id + name = "client_user_policy_test" + users = ["${keycloak_user.test.id}"] + logic = "POSITIVE" + decision_strategy = "UNANIMOUS" + depends_on = [ + keycloak_openid_client_permissions.realm-management_permission, + ] +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role_permissions" "my_permission" { + realm_id = keycloak_realm.realm.id + role_id = keycloak_role.role.id + + map_role_scope_policy_id = keycloak_openid_client_user_policy.test.id + map_role_client_scope_scope_policy_id = keycloak_openid_client_user_policy.test.id + map_role_composite_scope_policy_id = keycloak_openid_client_user_policy.test.id +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this group exists in. +- `role_id` - (Required) The id of the role. +- `map_role_scope_policy_id` - (Optional) Policy id that will be set on the scope based map-role permission automatically created by enabling permissions on the reference role. +- `map_role_client_scope_scope_policy_id` - (Optional) Policy id that will be set on the scope based map-role-client-scope permission automatically created by enabling permissions on the reference role. +- `map_role_composite_scope_policy_id` - (Optional) Policy id that will be set on the scope based map-role-composite permission automatically created by enabling permissions on the reference role. + + +### Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +- `enabled` - User permissions are Enabled (true) +- `authorization_resource_server_id` - Resource server id representing the realm management client on which this permission is managed. + diff --git a/keycloak/roles_permissions.go b/keycloak/roles_permissions.go new file mode 100644 index 000000000..bc454c366 --- /dev/null +++ b/keycloak/roles_permissions.go @@ -0,0 +1,38 @@ +package keycloak + +import ( + "fmt" +) + +type RolePermissionsInput struct { + Enabled bool `json:"enabled"` +} + +type RolePermissions struct { + RealmId string `json:"-"` + RoleId string `json:"-"` + Enabled bool `json:"enabled"` + Resource string `json:"resource"` + ScopePermissions map[string]interface{} `json:"scopePermissions"` +} + +func (keycloakClient *KeycloakClient) EnableRolePermissions(realmId, clientId string) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/roles-by-id/%s/management/permissions", realmId, clientId), RolePermissionsInput{Enabled: true}) +} + +func (keycloakClient *KeycloakClient) DisableRolePermissions(realmId, clientId string) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/roles-by-id/%s/management/permissions", realmId, clientId), RolePermissionsInput{Enabled: false}) +} + +func (keycloakClient *KeycloakClient) GetRolePermissions(realmId, roleId string) (*RolePermissions, error) { + var rolePermissions RolePermissions + rolePermissions.RealmId = realmId + rolePermissions.RoleId = roleId + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/roles-by-id/%s/management/permissions", realmId, roleId), &rolePermissions, nil) + if err != nil { + return nil, err + } + + return &rolePermissions, nil +} diff --git a/provider/provider.go b/provider/provider.go index 36bce2e60..6bfa517df 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -92,6 +92,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_authentication_execution_config": resourceKeycloakAuthenticationExecutionConfig(), "keycloak_identity_provider_token_exchange_scope_permission": resourceKeycloakIdentityProviderTokenExchangeScopePermission(), "keycloak_openid_client_permissions": resourceKeycloakOpenidClientPermissions(), + "keycloak_role_permissions": resourceKeycloakRolePermissions(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/provider/resource_keycloak_role_permissions.go b/provider/resource_keycloak_role_permissions.go new file mode 100644 index 000000000..998396732 --- /dev/null +++ b/provider/resource_keycloak_role_permissions.go @@ -0,0 +1,218 @@ +package provider + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakRolePermissions() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakRolePermissionsCreate, + Read: resourceKeycloakRolePermissionsRead, + Delete: resourceKeycloakRolePermissionsDelete, + Update: resourceKeycloakRolePermissionsUpdate, + Importer: &schema.ResourceImporter{ + State: resourceKeycloakRolePermissionsImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: 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", + }, + "map_role_scope_policy_id": { + Type: schema.TypeString, + Optional: true, + }, + "map_role_client_scope_scope_policy_id": { + Type: schema.TypeString, + Optional: true, + }, + "map_role_composite_scope_policy_id": { + Type: schema.TypeString, + Optional: true, + }}, + } +} + +func rolePermissionsId(realmId, roleId string) string { + return fmt.Sprintf("%s/%s", realmId, roleId) +} + +func setRoleScopePermissionPolicy(keycloakClient *keycloak.KeycloakClient, realmId, roleId string, scopeName string, policyId string) error { + rolePermissions, err := keycloakClient.GetRolePermissions(realmId, roleId) + if err != nil { + return err + } + + realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management") + if err != nil { + return err + } + + permission, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, rolePermissions.ScopePermissions[scopeName].(string)) + if err != nil { + return err + } + + permission.Policies = []string{policyId} + + return keycloakClient.UpdateOpenidClientAuthorizationPermission(permission) +} + +func unsetRoleScopePermissionPolicy(keycloakClient *keycloak.KeycloakClient, realmId, roleId, scopeName string) error { + rolePermissions, err := keycloakClient.GetRolePermissions(realmId, roleId) + if err != nil { + return err + } + + realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management") + if err != nil { + return err + } + + permission, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, rolePermissions.ScopePermissions[scopeName].(string)) + if err != nil { + return err + } + + permission.Policies = []string{} + err = keycloakClient.UpdateOpenidClientAuthorizationPermission(permission) + if err != nil { + return err + } + + return nil +} + +func resourceKeycloakRolePermissionsCreate(data *schema.ResourceData, meta interface{}) error { + return resourceKeycloakRolePermissionsUpdate(data, meta) +} + +func resourceKeycloakRolePermissionsUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + roleId := data.Get("role_id").(string) + + err := keycloakClient.EnableRolePermissions(realmId, roleId) + if err != nil { + return err + } + + mapRolesScopePolicyId, ok := data.GetOk("map_role_scope_policy_id") + if ok && mapRolesScopePolicyId.(string) != "" { + err := setRoleScopePermissionPolicy(keycloakClient, realmId, roleId, "map-role", mapRolesScopePolicyId.(string)) + if err != nil { + return err + } + } + mapRolesClientsScopePolicyId, ok := data.GetOk("map_role_client_scope_scope_policy_id") + if ok && mapRolesClientsScopePolicyId.(string) != "" { + err := setRoleScopePermissionPolicy(keycloakClient, realmId, roleId, "map-role-client-scope", mapRolesClientsScopePolicyId.(string)) + if err != nil { + return err + } + } + mapRolesCompositeScopePolicyId, ok := data.GetOk("map_role_composite_scope_policy_id") + if ok && mapRolesCompositeScopePolicyId.(string) != "" { + err := setRoleScopePermissionPolicy(keycloakClient, realmId, roleId, "map-role-composite", mapRolesCompositeScopePolicyId.(string)) + if err != nil { + return err + } + } + + return resourceKeycloakRolePermissionsRead(data, meta) +} + +func resourceKeycloakRolePermissionsRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + realmId := data.Get("realm_id").(string) + roleId := data.Get("role_id").(string) + + rolePermissions, err := keycloakClient.GetRolePermissions(realmId, roleId) + if err != nil { + return handleNotFoundError(err, data) + } + + data.SetId(rolePermissionsId(rolePermissions.RealmId, rolePermissions.RoleId)) + data.Set("realm_id", rolePermissions.RealmId) + data.Set("role_id", rolePermissions.RoleId) + + data.Set("enabled", rolePermissions.Enabled) + + realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management") + if err != nil { + return err + } + + permissionMapRoles, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, rolePermissions.ScopePermissions["map-role"].(string)) + if err != nil { + return err + } + if permissionMapRoles != nil && len(permissionMapRoles.Policies) > 0 { + data.Set("map_role_scope_policy_id", permissionMapRoles.Policies[0]) + } + permissionMapRolesClientScope, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, rolePermissions.ScopePermissions["map-role-client-scope"].(string)) + if err != nil { + return err + } + if permissionMapRolesClientScope != nil && len(permissionMapRolesClientScope.Policies) > 0 { + data.Set("map_role_client_scope_scope_policy_id", permissionMapRolesClientScope.Policies[0]) + } + permissionMapRolesComposite, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, rolePermissions.ScopePermissions["map-role-composite"].(string)) + if err != nil { + return err + } + if permissionMapRolesComposite != nil && len(permissionMapRolesComposite.Policies) > 0 { + data.Set("map_role_composite_scope_policy_id", permissionMapRolesComposite.Policies[0]) + } + + data.Set("authorization_resource_server_id", realmManagementClient.Id) + + return nil +} + +func resourceKeycloakRolePermissionsDelete(data *schema.ResourceData, meta interface{}) error { + + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + roleId := data.Get("role_id").(string) + + rolePermissions, err := keycloakClient.GetRolePermissions(realmId, roleId) + if err == nil && rolePermissions.Enabled { + _ = unsetRoleScopePermissionPolicy(keycloakClient, realmId, roleId, "map-role") + } + return keycloakClient.DisableRolePermissions(realmId, roleId) +} + +func resourceKeycloakRolePermissionsImport(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}}/{{roleId}}") + } + d.Set("realm_id", parts[0]) + d.Set("role_id", parts[1]) + + d.SetId(rolePermissionsId(parts[0], parts[1])) + + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_roles_permissions_test.go b/provider/resource_keycloak_roles_permissions_test.go new file mode 100644 index 000000000..75141b77a --- /dev/null +++ b/provider/resource_keycloak_roles_permissions_test.go @@ -0,0 +1,220 @@ +package provider + +import ( + "fmt" + "testing" + + "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" +) + +func TestAccKeycloakRolePermission_basic(t *testing.T) { + realmName := "tf_view-" + acctest.RandString(10) + roleName := "role-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakRolePermission_basic(realmName, roleName), + Check: testAccCheckKeycloakRolePermissionExists("keycloak_role_permissions.my_permission"), + }, { + ResourceName: "keycloak_role_permissions.my_permission", + ImportState: true, + ImportStateVerify: true, + }, { + Config: testKeycloakRolePermissionDelete_basic(realmName, roleName), + Check: testAccCheckKeycloakRolePermissionDoesNotExists("keycloak_role.role"), + }, + }, + }) +} + +func testAccCheckKeycloakRolePermissionExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + permissions, err := getRolePermissionsFromState(s, resourceName) + if err != nil { + return err + } + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + authorizationResourceServerId := rs.Primary.Attributes["authorization_resource_server_id"] + mapRoleScopePolicyId := rs.Primary.Attributes["map_role_scope_policy_id"] + + var realmManagementId string + clients, _ := keycloakClient.GetOpenidClients(permissions.RealmId, false) + for _, client := range clients { + if client.ClientId == "realm-management" { + realmManagementId = client.Id + break + } + } + + if authorizationResourceServerId != realmManagementId { + return fmt.Errorf("computed authorizationResourceServerId %s was not equal to %s (the id of the realm-management client)", authorizationResourceServerId, realmManagementId) + } + + authzClient, err := keycloakClient.GetOpenidClientAuthorizationPermission(permissions.RealmId, realmManagementId, permissions.ScopePermissions["map-role"].(string)) + if err != nil { + return err + } + + policyId := authzClient.Policies[0] + if mapRoleScopePolicyId != policyId { + return fmt.Errorf("computed mapRoleScopePolicyId %s was not equal to policyId %s", mapRoleScopePolicyId, policyId) + } + + return nil + } +} + +func testAccCheckKeycloakRolePermissionDoesNotExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realmId := rs.Primary.Attributes["realm_id"] + roleId := rs.Primary.ID + + permissions, err := keycloakClient.GetRolePermissions(realmId, roleId) + if err != nil { + return fmt.Errorf("error getting role permissions with realm id %s and role id %s : %s", realmId, roleId, err) + } + + if permissions.Enabled != false { + return fmt.Errorf("Users Permission in Keycloak is not disabled") + } + + return nil + } +} + +func getRolePermissionsFromState(s *terraform.State, resourceName string) (*keycloak.RolePermissions, 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"] + roleId := rs.Primary.Attributes["role_id"] + + permissions, err := keycloakClient.GetRolePermissions(realmId, roleId) + if err != nil { + return nil, fmt.Errorf("error getting role permissions with realm id %s and role id %s : %s", realmId, roleId, err) + } + + return permissions, nil +} + +func testKeycloakRolePermission_basic(realmId, roleName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +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 + enabled = true +} + +resource keycloak_user test { + realm_id = keycloak_realm.realm.id + username = "test-user" + + email = "test-user@fakedomain.com" + first_name = "Testy" + last_name = "Tester" +} + +resource keycloak_openid_client_user_policy test { + resource_server_id = "${data.keycloak_openid_client.realm_management.id}" + realm_id = keycloak_realm.realm.id + name = "client_user_policy_test" + users = ["${keycloak_user.test.id}"] + logic = "POSITIVE" + decision_strategy = "UNANIMOUS" + depends_on = [ + keycloak_openid_client_permissions.realm-management_permission, + ] +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role_permissions" "my_permission" { + realm_id = keycloak_realm.realm.id + role_id = keycloak_role.role.id + + map_role_scope_policy_id = keycloak_openid_client_user_policy.test.id + map_role_client_scope_scope_policy_id = keycloak_openid_client_user_policy.test.id + map_role_composite_scope_policy_id = keycloak_openid_client_user_policy.test.id +} + + `, realmId, roleName) +} + +func testKeycloakRolePermissionDelete_basic(realmId, roleName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +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 + enabled = true +} + +resource keycloak_user test { + realm_id = keycloak_realm.realm.id + username = "test-user" + + email = "test-user@fakedomain.com" + first_name = "Testy" + last_name = "Tester" +} + +resource keycloak_openid_client_user_policy test { + resource_server_id = "${data.keycloak_openid_client.realm_management.id}" + realm_id = keycloak_realm.realm.id + name = "client_user_policy_test" + users = ["${keycloak_user.test.id}"] + logic = "POSITIVE" + decision_strategy = "UNANIMOUS" + depends_on = [ + keycloak_openid_client_permissions.realm-management_permission, + ] +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + + `, realmId, roleName) +}