From c5ee38d7060aa547f63677077eb9e07942e0e86f Mon Sep 17 00:00:00 2001 From: Daniel Gozalo Date: Fri, 1 Oct 2021 14:26:22 +0200 Subject: [PATCH 1/3] initial draft --- design/namespaced-roles.md | 299 +++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 design/namespaced-roles.md diff --git a/design/namespaced-roles.md b/design/namespaced-roles.md new file mode 100644 index 0000000..fad3ee5 --- /dev/null +++ b/design/namespaced-roles.md @@ -0,0 +1,299 @@ +* **Status**: Draft #1 +* **JIRA**: TBD +* **Github Discussion**: [Namespaced Roles](https://github.com/keycloak/keycloak/discussions/8516) + +## Motivation + +In Keycloak, roles are defined at the Realm and Client levels. These roles can then be assigned directly to a User or to a Group, making every user in that group inherit the role. + +This is done through role mapping tables in the database, limiting the way you can assign roles to those two use cases. + +We will need more flexibility in the future, for example, to assign a role to a user in the context of a specific group (instead of having everyone in the group inherit a role) or, as the tenancy model gets implemented, a way to both define and assign roles in the context of a tenant. + +In order to have this flexibility and still support current use cases, we could create a new kind of role or backport existing roles to this new format: Namespaced roles. + +## Specification + +A Namespaced Role would follow a certain structure that would unambiguously identify it across the system, following a hierarchical structure. + +They would work similarly to ARNs in AWS, where a portion of the format is fixed and always identifiable by position, with the possibility of containing empty fields, containing a final variable part that could be freely named. + +An example would be: + +`role_v1:/\/\/\/\/` + +**Realm**: All resources in Keycloak are namespaced to a realm, so a unique identifier for a role would also contain it. + - It doesn't support empty values - all roles will be scoped to a realm. + - Example: `role_v1:/realm1////roleName` + +**Tenant**: There's a proposal to make Keycloak multitenant aware, so even if it's not currently supported, for future compatibility, it should be considered when defining a unique identifier. + - Supports empty values - A role might be created and assigned at a realm level + - Example: `role_v1:/realm1/tenantA///roleName`, `role_v1:/realm1///groupA/roleName` + +**Client**: Roles can be defined and assigned for a certain client in Keycloak, so a unique identifier has to support this possibility. + - Supports empty values - A role might be created and assigned without being restricted to a client. + - Example: `role_v1:/realm1/tenantA/clientId//roleName`, `role_v1:/realm1/tenantA//groupA/roleName` + +**Group**: Roles can be assigned to groups or, as intended in this design proposal, assigned to a user in the context of a group. + - Supports empty values - A role might be created at any level and not necessarily assigned to a group. + - Example: `role_v1:/realm1/tenantA/clientId/groupB/roleName`, `role_v1:/realm1/tenantA/clientId//roleName` + +**RoleName**: Finally, the name of the role that is being defined. + - Doesn't support empty values - You need to know the name of the role that is being created. + - Example: `role_v1:/realm1/tenantA/clientId/groupB/roleName` + +A possible free-form role identifier could be allowed for resource-server specific roles. + +The initial fixed part would be kept in order to identify the role within a realm, tenant, client, group etc, but instead of a fixed name for the role, it would be a reference to other hierarchical role formed with any random structure. + +A possible format would be: + +- `role_v1:/realm1/tenant1//group1/frole_v1:/mycompany/resources/department-a-roles/developer` + +If the role name is another reference to a free form role, starting with `frole_v1:/`, this role will also be represented in a hierarchical way, making it possible for protocol mappers to show them as such in tokens. + +## Use Cases + +As previously mentioned, there are a few use cases that we'll want to support and are currently not possible in Keycloak. + +### Use Case 1 - Assign a role in the context of a group + +Say we want to have the following group composition in our realm (or our tenant, in the future), most likely imported from a federated LDAP: + +realm/tenant:tenant1 + - group:iam + - group:devops + - group:leadership + +Following the current implementation, the users would need to define roles at the realm level and then assign them to these groups. Everyone in those groups would inherit the roles. + +But for a client, these groups may have business meaning or reflect organizational structure within their company, they aren't just a mean to group users together with the same roles, but rather fully functional teams that users can be part of with different roles. + +So UserA would be a `manager` in `group:iam`, but UserB could be a `developer`in `group:iam`. + +Namespaced roles would be specified when created, and loosely assigned based on the namespace, so let's say we have the following roles: + +`role_v1:/realm1/tenant1//iam/manager` +`role_v1:/realm1/tenant1//iam/developer` +`role_v1:/realm1/tenant1//devops/developer` +`role_v1:/realm1/tenant1//devops/devops_role` + +Now these roles are assigned directly to `UserA` and `UserB` so that: + +- `UserA` has the `role_v1:/realm1/tenant1//iam/manager` and the `role_v1:/realm1/tenant1//devops/developer` roles. +- `UserB` has the `role_v1:/realm1/tenant1//iam/developer` and the `role_v1:/realm1/tenant1//devops/developer` roles. + +Also, we should be able to still support giving everyone in a group an inherited role, such as: + +- `Group:devops` has the `role_v1:/realm1/tenant1//devops/devops_role` role. + +#### Representation in the tokens + +So now that we have a namespaced role attached to a user, the representation becomes inherently hierarchical, a **possible representation** in the tokens could look like: + +- For UserA + +```json +{ + .... + "roles": { + "tenants": { + "tenant1": { + "groups": { + "iam": ["manager"], + "devops": ["developer", "devops_role"] + } + } + } + } + .... +} + +``` + +- For UserB + +```json +{ + .... + "roles": { + "tenants": { + "tenant1": { + "groups": { + "iam": ["developer"], + "devops": ["developer", "devops_role"] + } + } + } + } + .... +} +``` + +This also provides a way to isolate certain roles to specific tenants. + +- `role_v1:/realm1/tenant1//iam/developer` would only be visible in `tenant1` while +- `role_v1:/realm1/tenant2//iam/somethingelse` would only be visible in `tenant2`. + +Or just to specific groups within the realm: + +- `role_v1:/realm1///iam/developer` would only be assignable to users in Group `iam`. +- `role_v1:/realm1///devops/devops` would only be assignable to users in Group `devops`. + +### Use Case 2 - Custom, resource server specific roles + +Another interesting use case is the creation of roles that are not directly restricted to Keycloak's entities such as Realms, Groups, Clients etc. + +Right now in Keycloak, clients are effectively a representation of a Resource Server (among other things). You can assign roles to clients and those roles would only be usable on those clients. + +As previously mentioned, a role would be namespaced to a certain client such as: + +- `role_v1:/realm1//clientId//roleName` + +A different way to be more flexible on giving users the possibility to set their own resource-server specific roles would be to provide a free-form unique role identifier that can be parsed by protocol mappers or just shown in the tokens in a hierarchical way. + +For example, a free form role could look like: + +- `role_v1:/realm1/tenant1///frole_v1:/mycompany/resources/department-a-roles/developer` + +This could map to a token such as: + +```json +{ + .... + "roles": { + "tenants": { + "tenant1": { + "myCompany": { + "resources": { + "department-a-roles": ["developer"] + } + } + } + } + } + .... +} +``` + +#### Advantages of such a structure + +Having namespaced claims, following a hierarchical structure, will simplify how we work with roles. And will expand the ways we can assign them to users by giving the possibility to apply roles based on certain contexts. + +It would be possible to query a User's roles based on the applied context. + +Let's assume that, additionally to previous assigned roles to `UserA`, this user also has the following role in `tenant2`: + +- `role_v1:/realm1/tenant2//iam/somethingelse` + +Let's say we want to get all the roles for UserA: + +- `getRoles("v1", "/")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant1//devops/developer`, `role_v1:/realm1/tenant1//devops/devops_role` and `role_v1:/realm1/tenant2//iam/somethingelse`. + +Now, we want to get all the roles that a user has in `tenant1`: + +- `getRoles("v1", "/realm1/tenant1")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant1//devops/developer`, `role_v1:/realm1/tenant1//devops/devops_role` + +Or we could go a level further and query by group: + +- `getRoles("v1", "/realm1/tenant1//iam")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`. + +Or even allow for wildcards and regexps in order to get a certain subset, for example, all roles applied to an `iam` group, regardless of the tenant (Note: this could be useful for the dashboard, but probably restricted when generating tokens) + +- `getRoles("v1", "/*/iam")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant2//iam/somethingelse`. + +Another advantage is the possibility to show which roles a user has in a specific context in the UI in a tree representation which could be easier to digest. + +### UX Improvements when creating and assigning roles + +With the hierarchical structure of namespaced roles, the UX to create or assign a role could be improved by showing a tree structure of the current entities in Keycloak. + +For example, say you want to create a role `developer` in tenant `TenantB`, group `GroupZ`. + +The user could be presented with a screen having a tree structure such as: + +``` +TenantA +TenantB + | + -- GroupA + | + -- GroupB + | + ... + | + -- GroupZ: + | + -- RoleA + -- RoleB +``` +The user could select the GroupZ level and create the role at that level. + +The resulting role's unique ID would be: + +- `role_v1:/realm1/tenantA//GroupZ/developer` + +But the user would not have to get to this level of complexity. + +**Note**: Need to find a way to also include clients in this assignment, that is trickier because it doesn't necessarily fit the hierarchical view, so it might need to be a different field that users can choose. + +When assigning a role, the same format could be used. + +If assigning it to a user, any level could be selected, when assigning it to a group, the tree would be filtered to the level of that group only. + + +### Integration with Dynamic Scopes / Rich Authorization Requests + +One of the main reasons to have this composable way or defining, assigning and referencing roles is to integrate with Dynamic Scopes and RAR in the near future. + +For example, when a user requests a dynamic scope `group:iam`, a mapper would be able to get al the roles that a user has in that specific group in a very easy way, by calling querying roles for that specific context: + +- `getRoles("v1", "/realm1(current realm)/tenant1(if the group is in a tenant)//iam")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`. + +If a user requests a dynamic scope such as: `tenant:tenant1`: + +- `getRoles("v1", "/realm1(or current realm)/tenant1")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant1//devops/developer`, `role_v1:/realm1/tenant1//devops/devops_role` + +If the client decided to use free-form roles, such as: + +- `role_v1:/realm1/tenant1///frole_v1:/mycompany/resources/department-a-roles/developer` + +A possible query for these roles based on dynamic scopes, if the `mycompany/resources` or `mycompany:resources` dynamic scope was provided, the query could look like: + +- `getRoles("v1", "/realm1/tenant1///mycompany/resources")` -> Returns: `frole_v1:/mycompany/resources/department-a-roles/developer` + + +### Implementation details + +Roles are a core entity within Keycloak and making changes to them have the potential to break significant parts of the code and functionality. + +An incremental way to implement namespaced roles would be to add a new field `namespace` in the `keycloak_role` database table, containig the whole id string of the role. + +Checks would need to be made to make sure that: + +- The realm in the namespace matches the role's realm database field. +- On role mappings, if the namespace field is present, the following checks could be made: + + - On user-role mapping: Make sure the user belongs to the group variable in the namespace, if present, same thing with tenants whenever we have them etc. + + - On group-role mapping: Make sure the group in the namespace is the same as the group being mapped to the role. + + - On client-role mapping: Make sure the client variable in the namespace matches the client that the role is being mapped to. +- On resource deletion: The system should query all roles with the resource in its namespace and delete them. + - Example: Role `role_v1:/realm1///groupA/developer` exists. User deletes `groupA`. The system will query `getRoles("v1", "/realm1///groupA")` and delete any returned entries. + +### Extensibility + +Protocol Mappers would be able to work with a predefined role id format to create any necessary representation in tokens. + +Other mappers could be defined to work with the free form roles formats by receiving configuration on how to access and map them to specific fields in the tokens. + +In case a new base entity needs to be added to the fixed part of the role identifier, a new namespace version could be defined such as: + +- `role_v2:/\/\/\/\/\/\` + +And the hypothetical roles query would receive the new version as a parameter: + +`getRoles("v2", "/realm1//newField")` + +When creating a role, users could choose which version to use and configure new mappers to either or all versions depending on the query they run. \ No newline at end of file From 3ac777aa83e74a0f1bc8811a14b08d6b7048582e Mon Sep 17 00:00:00 2001 From: Daniel Gozalo Date: Thu, 7 Oct 2021 22:23:31 +0200 Subject: [PATCH 2/3] feat: rework the format of the roles namespaces to make it easier to understand and add more use cases --- design/namespaced-roles.md | 266 ++++++++++++++++++++++--------------- 1 file changed, 158 insertions(+), 108 deletions(-) diff --git a/design/namespaced-roles.md b/design/namespaced-roles.md index fad3ee5..6fce095 100644 --- a/design/namespaced-roles.md +++ b/design/namespaced-roles.md @@ -10,47 +10,95 @@ This is done through role mapping tables in the database, limiting the way you c We will need more flexibility in the future, for example, to assign a role to a user in the context of a specific group (instead of having everyone in the group inherit a role) or, as the tenancy model gets implemented, a way to both define and assign roles in the context of a tenant. +A few things that we'd also like to change in Keycloak with regard to roles are: + +- Avoid the need to create clients to "namespace" roles for specific cases. Right now when creating a new Realm, Keycloak creates some default clients such as the `Realm Management` one that holds Keycloak specific roles to work with the dashboard. + +- The ability to share a group of roles among clients instead of having to define them individually per client. + +- Improve the experience of managing roles. By namespacing them, it would be easier to add custom structures that have business meaning for users. + In order to have this flexibility and still support current use cases, we could create a new kind of role or backport existing roles to this new format: Namespaced roles. ## Specification -A Namespaced Role would follow a certain structure that would unambiguously identify it across the system, following a hierarchical structure. +A Namespaced Role would contain a version, a path that start with a known particle to identify the nature of the role, and finally the role name. + +For example, Keycloak management specific roles would start with `/kc`. + +User defined roles, but still used to interact with Keycloak entities would start with `/ud`. + +Totally custom namespaces created with a specific business meaning for users wouldn't contain any of the previous particles. -They would work similarly to ARNs in AWS, where a portion of the format is fixed and always identifiable by position, with the possibility of containing empty fields, containing a final variable part that could be freely named. +Namespaced Roles would always follow a structure that contains a fixed part identifying the entity, followed by a variable part containing its identifier. This pattern could be repeated in a hierarchical way. -An example would be: +A few examples would be: -`role_v1:/\/\/\/\/` +`role_v1:/kc/groups/{group-name}/roleName` -**Realm**: All resources in Keycloak are namespaced to a realm, so a unique identifier for a role would also contain it. - - It doesn't support empty values - all roles will be scoped to a realm. - - Example: `role_v1:/realm1////roleName` +`role_v1:/ud/tenants/{tenant-name}/roleName` + +`role_v1:/ud/tenants/{tenant-name}/clients/{client-name}/roleName` + +Some of the Keycloak supported entities would be: **Tenant**: There's a proposal to make Keycloak multitenant aware, so even if it's not currently supported, for future compatibility, it should be considered when defining a unique identifier. - - Supports empty values - A role might be created and assigned at a realm level - - Example: `role_v1:/realm1/tenantA///roleName`, `role_v1:/realm1///groupA/roleName` + - Example: `role_v1:/kc/tenants/tenantA/roleName`. **Client**: Roles can be defined and assigned for a certain client in Keycloak, so a unique identifier has to support this possibility. - - Supports empty values - A role might be created and assigned without being restricted to a client. - - Example: `role_v1:/realm1/tenantA/clientId//roleName`, `role_v1:/realm1/tenantA//groupA/roleName` + - Example: `role_v1:/kc/clients/clientId/roleName`. + +Alternatively, if a namespace wants to be created for roles that can be shared among clients, it could be defined such as: `role_v1:/clients/*/roleName`. **Group**: Roles can be assigned to groups or, as intended in this design proposal, assigned to a user in the context of a group. - - Supports empty values - A role might be created at any level and not necessarily assigned to a group. - - Example: `role_v1:/realm1/tenantA/clientId/groupB/roleName`, `role_v1:/realm1/tenantA/clientId//roleName` + - Example: `role_v1:/groups/groupA/roleName`. + +Alternatively, if a namespace wants to be created for roles that can be shared among groups, it could be defined such as: `role_v1:/groups/*/roleName` + +A free-form role identifier would be allowed for resource-server specific roles thay may hold business meaning for users. It would lack the fixed `kc` or `ud` particles and allow any user defined path. For example: + +- `role_v1:/business-units/myBusinessUnit/const-centers/myCostCenter/roleName` + +**Question**: In the initial proposal, these roles would be preceded by a fixed part that would identify the context where this role would be applied. We could do somethng similar here such as: + +- `role_v1:/groups/{group-name}/role-path/business-units/myBusinessUnit/const-centers/myCostCenter/roleName` + +But not convinced on the way to let Keycloak know that that's not a role name but rather a new namespace. + +## Management Roles + +These roles would only exist in the Master realm and would have a meaning for Keycloak, hence the `kc` fragment. + +- `role_v1:/kc/admin/{role-name}` -> Roles to manage the Keycloak instance. + +When a new Realm is created in Keycloak, a new client is created in the Master realm to represent a namespace for this new Realm's admin roles. With the exception of the previous one, they will be used to deprecate the Clients that Keycloak creates to group certain management roles together. + +The rest of these roles would always be scoped to a specific realm that would replace the current client-per-realm. + +- `role_v1:/kc/realms/{realm-name}/clients/{client-name}/{role-name}` -> Roles to manage a certain client. +- `role_v1:/kc/realms/{realm-name}/groups/{group-name}/{role-name}` -> Roles to manage a certain group. +- `role_v1:/kc/realms/{realm-name}/tenants/{tenant-name}/{role-name}` -> Roles to manage a certain tenant. +- `role_v1:/kc/realms/{realm-name}/tenants/{tenant-name}/groups/{group-name}/{role-name}` -> Roles to manage a certain group in a certain tenant. + +These realm-clients that are created in the Master realm include roles such as: + +- manage-realm, manage-users, manage-authorization, etc. + +We would replace these clients by creating the following namespace: -**RoleName**: Finally, the name of the role that is being defined. - - Doesn't support empty values - You need to know the name of the role that is being created. - - Example: `role_v1:/realm1/tenantA/clientId/groupB/roleName` +- `role_v1:/kc/realms/{new-realm-name}/{role-name}` -A possible free-form role identifier could be allowed for resource-server specific roles. +## User Defined Roles -The initial fixed part would be kept in order to identify the role within a realm, tenant, client, group etc, but instead of a fixed name for the role, it would be a reference to other hierarchical role formed with any random structure. +These roles would have a meaning for Users, hence the `ud (user defined)` fragment. -A possible format would be: +The same format, but defined by users to map roles to these entities would look like: -- `role_v1:/realm1/tenant1//group1/frole_v1:/mycompany/resources/department-a-roles/developer` +- `role_v1:/ud/groups/{group-name}/{role-name}` -> Roles that can only be assigned in the context of a specific group. +- `role_v1:/ud/tenants/{tenant-name}/{role-name}` -> Roles that can only be assigned in the context of a specific tenant. +- `role_v1:/ud/tenants/{tenant-name}/groups/{group-name}/{role-name}` -> Roles that can only be assigned in the context of a specific tenant and group. -If the role name is another reference to a free form role, starting with `frole_v1:/`, this role will also be represented in a hierarchical way, making it possible for protocol mappers to show them as such in tokens. +Keycloak would parse these roles by looking at the entity types before the variables in order to query and assign them. ## Use Cases @@ -73,23 +121,23 @@ So UserA would be a `manager` in `group:iam`, but UserB could be a `developer`in Namespaced roles would be specified when created, and loosely assigned based on the namespace, so let's say we have the following roles: -`role_v1:/realm1/tenant1//iam/manager` -`role_v1:/realm1/tenant1//iam/developer` -`role_v1:/realm1/tenant1//devops/developer` -`role_v1:/realm1/tenant1//devops/devops_role` +`role_v1:/ud/groups/iam/manager` +`role_v1:/ud/groups/iam/developer` +`role_v1:/ud/groups/devops/developer` +`role_v1:/ud/groups/devops/devops_role` Now these roles are assigned directly to `UserA` and `UserB` so that: -- `UserA` has the `role_v1:/realm1/tenant1//iam/manager` and the `role_v1:/realm1/tenant1//devops/developer` roles. -- `UserB` has the `role_v1:/realm1/tenant1//iam/developer` and the `role_v1:/realm1/tenant1//devops/developer` roles. +- `UserA` has the `role_v1:/ud/groups/iam/manager` and the `role_v1:/ud/groups/devops/developer` roles. +- `UserB` has the `role_v1:/ud/groups/iam/developer` and the `role_v1:/ud/groups/devops/developer` roles. Also, we should be able to still support giving everyone in a group an inherited role, such as: -- `Group:devops` has the `role_v1:/realm1/tenant1//devops/devops_role` role. +- `Group:devops` has the `role_v1:/ud/groups/devops/devops_role` role or `role_v1:/ud/devops_role`. #### Representation in the tokens -So now that we have a namespaced role attached to a user, the representation becomes inherently hierarchical, a **possible representation** in the tokens could look like: +So now that we have a namespaced role attached to a user, a **possible representation** in the tokens, defined by a token mappers could look like: - For UserA @@ -97,13 +145,9 @@ So now that we have a namespaced role attached to a user, the representation bec { .... "roles": { - "tenants": { - "tenant1": { - "groups": { - "iam": ["manager"], - "devops": ["developer", "devops_role"] - } - } + "groups": { + "iam": ["manager"], + "devops": ["developer", "devops_role"] } } .... @@ -111,36 +155,53 @@ So now that we have a namespaced role attached to a user, the representation bec ``` +As mappers can be custom implementations, users would be able to define other representations, or Keycloak could provide more built-in mappers to do so. + +An alternative representation could look like: + +```json +{ + .... + "roles": [ + "groups/iam/manager", + "groups/devops/developer", + "groups/devops/devops_role" + ] + .... +} + +``` + - For UserB ```json { .... "roles": { - "tenants": { - "tenant1": { - "groups": { - "iam": ["developer"], - "devops": ["developer", "devops_role"] - } - } + "groups": { + "iam": ["developer"], + "devops": ["developer", "devops_role"] } } .... } ``` -This also provides a way to isolate certain roles to specific tenants. +The whole idea is that by leveraging namespaces, users can define the structure that they need for their business needs. -- `role_v1:/realm1/tenant1//iam/developer` would only be visible in `tenant1` while -- `role_v1:/realm1/tenant2//iam/somethingelse` would only be visible in `tenant2`. +### Use Case 2 - Isolate roles to specific entities -Or just to specific groups within the realm: +It will also be possible to isole certain roles to specific entities such as tenants. -- `role_v1:/realm1///iam/developer` would only be assignable to users in Group `iam`. -- `role_v1:/realm1///devops/devops` would only be assignable to users in Group `devops`. +- `role_v1:/ud/tenants/tenant1/developer` would only be assignable in `tenant1` while +- `role_v1:/ud/tenants/tenant2/somethingelse` would only be assignable in `tenant2`. -### Use Case 2 - Custom, resource server specific roles +Or just to specific clients within the realm: + +- `role_v1:/kc/clients/client-a/client-admin` would only give users the `client-admin` role when interacting with the `client-a` client. +- `role_v1:/kc/groups/devops/group-admin` would only give users the `group-admin` role when interacting with with the `devops` groups. + +### Use Case 3 - Custom, resource server specific roles Another interesting use case is the creation of roles that are not directly restricted to Keycloak's entities such as Realms, Groups, Clients etc. @@ -148,13 +209,17 @@ Right now in Keycloak, clients are effectively a representation of a Resource Se As previously mentioned, a role would be namespaced to a certain client such as: -- `role_v1:/realm1//clientId//roleName` +- `role_v1:/ud/clients/{client-name}/{role-name}` + +or with a regexp, they could be namespaced to a specific subset of clients: -A different way to be more flexible on giving users the possibility to set their own resource-server specific roles would be to provide a free-form unique role identifier that can be parsed by protocol mappers or just shown in the tokens in a hierarchical way. +- `role_v1:/ud/clients/{regexp-containing-wildcard}/{role-name}` + +A different way to be even more flexible by giving users the possibility to set their own resource-server specific would be using the custom namespaced roles. For example, a free form role could look like: -- `role_v1:/realm1/tenant1///frole_v1:/mycompany/resources/department-a-roles/developer` +- `role_v1:/mycompany/resources/department-a-roles/developer` This could map to a token such as: @@ -162,85 +227,72 @@ This could map to a token such as: { .... "roles": { - "tenants": { - "tenant1": { - "myCompany": { - "resources": { - "department-a-roles": ["developer"] - } - } + "myCompany": { + "resources": { + "department-a-roles": ["developer"] } } } .... } ``` +Or even scope this custom namespace to a specific client or client sets: + +- `role_v1:/ud/clients/{client-name-or-regexp}/frole:/mycompany/resources/department-a-roles/developer` + +So this role can only be mapped to a token issued by those specific clients. #### Advantages of such a structure -Having namespaced claims, following a hierarchical structure, will simplify how we work with roles. And will expand the ways we can assign them to users by giving the possibility to apply roles based on certain contexts. +Having namespaced claims, will simplify how we work with roles. And will expand the ways we can assign them to users by giving the possibility to apply roles based on certain contexts. It would be possible to query a User's roles based on the applied context. Let's assume that, additionally to previous assigned roles to `UserA`, this user also has the following role in `tenant2`: -- `role_v1:/realm1/tenant2//iam/somethingelse` +- `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse` Let's say we want to get all the roles for UserA: -- `getRoles("v1", "/")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant1//devops/developer`, `role_v1:/realm1/tenant1//devops/devops_role` and `role_v1:/realm1/tenant2//iam/somethingelse`. +- `getRoles("v1", "ud","/")` -> Returns: `role_v1:/ud/groups/iam/manager`, `role_v1:/ud/groups/devops/developer`, `role_v1:/ud/groups/devops/devops_role` and `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse`. -Now, we want to get all the roles that a user has in `tenant1`: +Now, we want to get all the roles that a user has in `/groups/devops`: -- `getRoles("v1", "/realm1/tenant1")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant1//devops/developer`, `role_v1:/realm1/tenant1//devops/devops_role` +- `getRoles("v1", "ud", "/groups/devops")` -> Returns: `role_v1:/ud/groups/iam/manager`, `role_v1:/ud/groups/devops/developer`, `role_v1:/ud/groups/devops/devops_role` -Or we could go a level further and query by group: +Or we could filter by tenant: -- `getRoles("v1", "/realm1/tenant1//iam")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`. +- `getRoles("v1", "ud", "/tenants/tenant2")` -> Returns: `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse`. Or even allow for wildcards and regexps in order to get a certain subset, for example, all roles applied to an `iam` group, regardless of the tenant (Note: this could be useful for the dashboard, but probably restricted when generating tokens) -- `getRoles("v1", "/*/iam")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant2//iam/somethingelse`. - -Another advantage is the possibility to show which roles a user has in a specific context in the UI in a tree representation which could be easier to digest. +- `getRoles("v1", "ud", "*/groups/iam")` -> Returns: `role_v1:/ud/groups/iam/manager`, `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse`. ### UX Improvements when creating and assigning roles -With the hierarchical structure of namespaced roles, the UX to create or assign a role could be improved by showing a tree structure of the current entities in Keycloak. +When creating namespaced roles, the UI could help users place a group in a specific context that would build a role namespace in a visual way. -For example, say you want to create a role `developer` in tenant `TenantB`, group `GroupZ`. +A User wants to create the role `developer` in tenant `TenantB`, group `GroupZ`. -The user could be presented with a screen having a tree structure such as: +The UI could ask the user to add layers to the selection by showing some predefined structure: ``` -TenantA -TenantB - | - -- GroupA - | - -- GroupB - | - ... - | - -- GroupZ: - | - -- RoleA - -- RoleB -``` -The user could select the GroupZ level and create the role at that level. - -The resulting role's unique ID would be: - -- `role_v1:/realm1/tenantA//GroupZ/developer` +What would be the root of the role namespace? +Choose between: Realm, Tenant, Client or Group. -But the user would not have to get to this level of complexity. +::: User Selects TenantB + +Want to add more contexts to the role? +As Tenant was previously selected, the UI would show: Client or Group. +``` -**Note**: Need to find a way to also include clients in this assignment, that is trickier because it doesn't necessarily fit the hierarchical view, so it might need to be a different field that users can choose. +During this time, the role namespace would build automatically and show the user the final role namespace. -When assigning a role, the same format could be used. +The resulting role namespace for this use case would be: -If assigning it to a user, any level could be selected, when assigning it to a group, the tree would be filtered to the level of that group only. +- `role_v1:/ud/tenants/tenantB/groups/GroupZ/developer` +But the user would not have to get to this level of complexity. ### Integration with Dynamic Scopes / Rich Authorization Requests @@ -248,19 +300,19 @@ One of the main reasons to have this composable way or defining, assigning and r For example, when a user requests a dynamic scope `group:iam`, a mapper would be able to get al the roles that a user has in that specific group in a very easy way, by calling querying roles for that specific context: -- `getRoles("v1", "/realm1(current realm)/tenant1(if the group is in a tenant)//iam")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`. +- `getRoles("v1", "ud", "/groups/iam")` -> Returns: `role_v1:/ud/groups/iam/manager`. If a user requests a dynamic scope such as: `tenant:tenant1`: -- `getRoles("v1", "/realm1(or current realm)/tenant1")` -> Returns: `role_v1:/realm1/tenant1//iam/manager`, `role_v1:/realm1/tenant1//devops/developer`, `role_v1:/realm1/tenant1//devops/devops_role` +- `getRoles("v1", "ud", "/tenants/tenant1")` -> Returns: `role_v1:/ud/tenants/tenant1/groups/iam/manager`, `role_v1:/ud/tenants/tenant1/groups/devops/developer`, `role_v1:/ud/tenants/tenant1/groups/devops/devops_role` If the client decided to use free-form roles, such as: -- `role_v1:/realm1/tenant1///frole_v1:/mycompany/resources/department-a-roles/developer` +- `role_v1:/mycompany/resources/department-a-roles/developer` A possible query for these roles based on dynamic scopes, if the `mycompany/resources` or `mycompany:resources` dynamic scope was provided, the query could look like: -- `getRoles("v1", "/realm1/tenant1///mycompany/resources")` -> Returns: `frole_v1:/mycompany/resources/department-a-roles/developer` +- `getRoles("v1", "", "/mycompany/resources")` -> Returns: `role_v1:/mycompany/resources/department-a-roles/developer` ### Implementation details @@ -271,7 +323,6 @@ An incremental way to implement namespaced roles would be to add a new field `na Checks would need to be made to make sure that: -- The realm in the namespace matches the role's realm database field. - On role mappings, if the namespace field is present, the following checks could be made: - On user-role mapping: Make sure the user belongs to the group variable in the namespace, if present, same thing with tenants whenever we have them etc. @@ -279,21 +330,20 @@ Checks would need to be made to make sure that: - On group-role mapping: Make sure the group in the namespace is the same as the group being mapped to the role. - On client-role mapping: Make sure the client variable in the namespace matches the client that the role is being mapped to. + - On resource deletion: The system should query all roles with the resource in its namespace and delete them. - - Example: Role `role_v1:/realm1///groupA/developer` exists. User deletes `groupA`. The system will query `getRoles("v1", "/realm1///groupA")` and delete any returned entries. + - Example: Role `role_v1:/ud/groups/groupA/developer` exists. User deletes `groupA`. The system will query `getRoles("v1", "ud, "/groups/groupA")` and delete any returned entries. ### Extensibility -Protocol Mappers would be able to work with a predefined role id format to create any necessary representation in tokens. - -Other mappers could be defined to work with the free form roles formats by receiving configuration on how to access and map them to specific fields in the tokens. +Protocol Mappers would be able to work with preconfigured role namespace formats to create any necessary representation in tokens. -In case a new base entity needs to be added to the fixed part of the role identifier, a new namespace version could be defined such as: +In case we need to evolve these roles in a non backwards compatibility way, we can start creating roles with the breaking change with a different role version preffix: -- `role_v2:/\/\/\/\/\/\` +- `role_v2:{any-possible-change}` And the hypothetical roles query would receive the new version as a parameter: -`getRoles("v2", "/realm1//newField")` +`getRoles("v2", "ud", {new-query})` When creating a role, users could choose which version to use and configure new mappers to either or all versions depending on the query they run. \ No newline at end of file From cccb1415cddedc9032434a218e52a034571150da Mon Sep 17 00:00:00 2001 From: Daniel Gozalo Date: Mon, 11 Oct 2021 18:51:21 +0200 Subject: [PATCH 3/3] feat: draft#3 --- design/namespaced-roles.md | 286 ++++++++++++++++++++----------------- 1 file changed, 158 insertions(+), 128 deletions(-) diff --git a/design/namespaced-roles.md b/design/namespaced-roles.md index 6fce095..dc95837 100644 --- a/design/namespaced-roles.md +++ b/design/namespaced-roles.md @@ -1,139 +1,137 @@ -* **Status**: Draft #1 -* **JIRA**: TBD +* **Status**: Draft #3 * **Github Discussion**: [Namespaced Roles](https://github.com/keycloak/keycloak/discussions/8516) -## Motivation - -In Keycloak, roles are defined at the Realm and Client levels. These roles can then be assigned directly to a User or to a Group, making every user in that group inherit the role. +**Note**: Still trying to find a good fragment name for User Managed roles (`/ud/`). -This is done through role mapping tables in the database, limiting the way you can assign roles to those two use cases. +## Motivation -We will need more flexibility in the future, for example, to assign a role to a user in the context of a specific group (instead of having everyone in the group inherit a role) or, as the tenancy model gets implemented, a way to both define and assign roles in the context of a tenant. +In an attempt to improve Keycloak's authorization capabilities, both for Keycloak administration and for user defined controls, we need to improve how Roles are defined, simplifying some use cases to get rid of unnecessary structures and to flexibilize how users can work with roles in the system. -A few things that we'd also like to change in Keycloak with regard to roles are: +Some of the proposed changes are: -- Avoid the need to create clients to "namespace" roles for specific cases. Right now when creating a new Realm, Keycloak creates some default clients such as the `Realm Management` one that holds Keycloak specific roles to work with the dashboard. +- Remove clients that group realm specific roles. -- The ability to share a group of roles among clients instead of having to define them individually per client. +- Allow users to create a set of roles that can be shared among cients and groups. -- Improve the experience of managing roles. By namespacing them, it would be easier to add custom structures that have business meaning for users. +- Allow users to define their own role namespaces to match their organizational structure and business needs. In order to have this flexibility and still support current use cases, we could create a new kind of role or backport existing roles to this new format: Namespaced roles. ## Specification -A Namespaced Role would contain a version, a path that start with a known particle to identify the nature of the role, and finally the role name. +A Namespaced Role would be defined as a [Unified Resource Identifier (URI)](https://datatracker.ietf.org/doc/html/rfc3986) whose segments would identify the kinds and values of the entities that the roles are scoped to. -For example, Keycloak management specific roles would start with `/kc`. +The formal syntax would look like: -User defined roles, but still used to interact with Keycloak entities would start with `/ud`. +- `/(kc|ud|"")/({entity-type}/{entity-name})*/{role-name}` -Totally custom namespaces created with a specific business meaning for users wouldn't contain any of the previous particles. +The first segment would let the system identify the kind of role. The `/kc/` fragment would be restricted to Keycloak administrative roles, `/ud/` would identify roles managed by users. -Namespaced Roles would always follow a structure that contains a fixed part identifying the entity, followed by a variable part containing its identifier. This pattern could be repeated in a hierarchical way. +If the namespace doesn't contain any of these segments, the system would interpret this role as a custom, user defined namespace. A few examples would be: -`role_v1:/kc/groups/{group-name}/roleName` +`/kc/realms/realm-a/view-clients` -`role_v1:/ud/tenants/{tenant-name}/roleName` +`/ud/groups/group-a/admin` -`role_v1:/ud/tenants/{tenant-name}/clients/{client-name}/roleName` +`/my-company/my-business-unit/my-cost-center/my-role` -Some of the Keycloak supported entities would be: +Keycloak would parse these roles by looking at the entity types before the variables in order to query and assign them. -**Tenant**: There's a proposal to make Keycloak multitenant aware, so even if it's not currently supported, for future compatibility, it should be considered when defining a unique identifier. - - Example: `role_v1:/kc/tenants/tenantA/roleName`. +## Keycloak Admin Roles -**Client**: Roles can be defined and assigned for a certain client in Keycloak, so a unique identifier has to support this possibility. - - Example: `role_v1:/kc/clients/clientId/roleName`. +From Keycloak documentation -Alternatively, if a namespace wants to be created for roles that can be shared among clients, it could be defined such as: `role_v1:/clients/*/roleName`. +``` +Admin users within the master realm can be granted management privileges to one or more other realms in the system. Each realm in {project_name} is represented by a client in the master realm. The name of the client is -realm. These clients each have client-level roles defined which define varying level of access to manage an individual realm. +``` +Namespaced Roles would make it possible to deprecate these clients whose only purpose is to group management roles together. -**Group**: Roles can be assigned to groups or, as intended in this design proposal, assigned to a user in the context of a group. - - Example: `role_v1:/groups/groupA/roleName`. +For example, when a new realm `Realm-A` is created, a client called `realm-a-realm` is created in the `Master` realm with these management roles. -Alternatively, if a namespace wants to be created for roles that can be shared among groups, it could be defined such as: `role_v1:/groups/*/roleName` +Additionally, a client called `realm-management` is created within the new realm with the same roles. -A free-form role identifier would be allowed for resource-server specific roles thay may hold business meaning for users. It would lack the fixed `kc` or `ud` particles and allow any user defined path. For example: +These clients serve no purpose other than grouping these roles together, so they could be replaced by creating a specific namespace. -- `role_v1:/business-units/myBusinessUnit/const-centers/myCostCenter/roleName` +The `realm-a-realm` and the Realm-A's `realm-management` clients could be replaced by defining the following namespace: -**Question**: In the initial proposal, these roles would be preceded by a fixed part that would identify the context where this role would be applied. We could do somethng similar here such as: +- `/kc/realms/realm-a/{role-name}` -- `role_v1:/groups/{group-name}/role-path/business-units/myBusinessUnit/const-centers/myCostCenter/roleName` +In order to give `UserA` the `view-clients` role in `Realm-A`, the following namespaced role could be defined and assigned: -But not convinced on the way to let Keycloak know that that's not a role name but rather a new namespace. +- `/kc/realms/realm-a/view-clients` -## Management Roles +Global admin role(s) could be defined with the following format: -These roles would only exist in the Master realm and would have a meaning for Keycloak, hence the `kc` fragment. +- `/kc/admin/{role-name}` -- `role_v1:/kc/admin/{role-name}` -> Roles to manage the Keycloak instance. +### Fine-grained management permissions -When a new Realm is created in Keycloak, a new client is created in the Master realm to represent a namespace for this new Realm's admin roles. With the exception of the previous one, they will be used to deprecate the Clients that Keycloak creates to group certain management roles together. +Rigt now fine-grained management permissions are defined in Keycloak making use of Authorization Services and Policies. -The rest of these roles would always be scoped to a specific realm that would replace the current client-per-realm. +A possible alternative would be defining Keycloak admin namespaces with the entity types and names that want to be managed. -- `role_v1:/kc/realms/{realm-name}/clients/{client-name}/{role-name}` -> Roles to manage a certain client. -- `role_v1:/kc/realms/{realm-name}/groups/{group-name}/{role-name}` -> Roles to manage a certain group. -- `role_v1:/kc/realms/{realm-name}/tenants/{tenant-name}/{role-name}` -> Roles to manage a certain tenant. -- `role_v1:/kc/realms/{realm-name}/tenants/{tenant-name}/groups/{group-name}/{role-name}` -> Roles to manage a certain group in a certain tenant. +For example, to manage a specific client: -These realm-clients that are created in the Master realm include roles such as: +- `/kc/clients/{client-id}/manage` -- manage-realm, manage-users, manage-authorization, etc. +Restrict User Role Mappings: -We would replace these clients by creating the following namespace: +- `/kc/clients/{client-id}/map-roles` -- `role_v1:/kc/realms/{new-realm-name}/{role-name}` +Managing the membership of a group: -## User Defined Roles +- `/kc/groups/{group-name}/manage-membership` -These roles would have a meaning for Users, hence the `ud (user defined)` fragment. +And a large number of use cases that are contemplated in https://www.keycloak.org/docs/latest/server_admin/index.html#_fine_grain_permissions. -The same format, but defined by users to map roles to these entities would look like: +## User Managed Roles -- `role_v1:/ud/groups/{group-name}/{role-name}` -> Roles that can only be assigned in the context of a specific group. -- `role_v1:/ud/tenants/{tenant-name}/{role-name}` -> Roles that can only be assigned in the context of a specific tenant. -- `role_v1:/ud/tenants/{tenant-name}/groups/{group-name}/{role-name}` -> Roles that can only be assigned in the context of a specific tenant and group. +These roles would have a meaning for Users but would still be understood by Keycloak. -Keycloak would parse these roles by looking at the entity types before the variables in order to query and assign them. +These roles would allow users to define roles in the context of specific entities, but unlike the Keycloak restricted management roles (`/kc/`), these could eventually end up in tokens through Protocol Mappers or used for policies in Authorization Services. + +These roles would start with the `/ud/` segment. For example: -## Use Cases +- `/ud/groups/{group-name}/{role-name}` -> Roles that can only be assigned in the context of a specific group. -As previously mentioned, there are a few use cases that we'll want to support and are currently not possible in Keycloak. +- `/ud/clients/{client-id}/{role-name}` -> Roles that can only be assigned in the context of a spefici client. -### Use Case 1 - Assign a role in the context of a group +- `/ud/{role-name}` -> Roles without any scope other than the `/ud/` namespace. These would replace the current realm roles. -Say we want to have the following group composition in our realm (or our tenant, in the future), most likely imported from a federated LDAP: +The current use cases would still be supported, such as defining a simple role that can be mapped to a User or a Group, but additional scoping could be provided by adding certain segments to the namespaces. -realm/tenant:tenant1 +### Use Case - Assign a role in the context of a group + +Say we want to have the following group composition in our realm, most likely imported from a federated LDAP: + +realm/realm1 - group:iam - group:devops - group:leadership Following the current implementation, the users would need to define roles at the realm level and then assign them to these groups. Everyone in those groups would inherit the roles. -But for a client, these groups may have business meaning or reflect organizational structure within their company, they aren't just a mean to group users together with the same roles, but rather fully functional teams that users can be part of with different roles. +But for a user, these groups may have business meaning or reflect organizational structure within their company, they aren't just a mean to group users together with the same roles, but rather fully functional teams that users can be part of with different roles. So UserA would be a `manager` in `group:iam`, but UserB could be a `developer`in `group:iam`. -Namespaced roles would be specified when created, and loosely assigned based on the namespace, so let's say we have the following roles: +Namespaced roles would be created and loosely assigned based on the group, so let's say we have the following roles: -`role_v1:/ud/groups/iam/manager` -`role_v1:/ud/groups/iam/developer` -`role_v1:/ud/groups/devops/developer` -`role_v1:/ud/groups/devops/devops_role` +`/ud/groups/iam/manager` +`/ud/groups/iam/developer` +`/ud/groups/devops/developer` +`/ud/groups/devops/devops_role` Now these roles are assigned directly to `UserA` and `UserB` so that: -- `UserA` has the `role_v1:/ud/groups/iam/manager` and the `role_v1:/ud/groups/devops/developer` roles. -- `UserB` has the `role_v1:/ud/groups/iam/developer` and the `role_v1:/ud/groups/devops/developer` roles. +- `UserA` has the `/ud/groups/iam/manager` and the `/ud/groups/devops/developer` roles. +- `UserB` has the `/ud/groups/iam/developer` and the `/ud/groups/devops/developer` roles. Also, we should be able to still support giving everyone in a group an inherited role, such as: -- `Group:devops` has the `role_v1:/ud/groups/devops/devops_role` role or `role_v1:/ud/devops_role`. +- Giving `Group:devops` the `/ud/groups/devops/devops_role` role or without the groups namespace fragment `/ud/devops_role`. #### Representation in the tokens @@ -191,35 +189,29 @@ The whole idea is that by leveraging namespaces, users can define the structure ### Use Case 2 - Isolate roles to specific entities -It will also be possible to isole certain roles to specific entities such as tenants. - -- `role_v1:/ud/tenants/tenant1/developer` would only be assignable in `tenant1` while -- `role_v1:/ud/tenants/tenant2/somethingelse` would only be assignable in `tenant2`. +It will also be possible to isole certain roles to specific entities such as groups or clients. -Or just to specific clients within the realm: +- `/ud/groups/groupA/developer` would only be assignable in `group:GroupA` while +- `/ud/groups/groupB/somethingelse` would only be assignable in `group:GroupB`. -- `role_v1:/kc/clients/client-a/client-admin` would only give users the `client-admin` role when interacting with the `client-a` client. -- `role_v1:/kc/groups/devops/group-admin` would only give users the `group-admin` role when interacting with with the `devops` groups. -### Use Case 3 - Custom, resource server specific roles - -Another interesting use case is the creation of roles that are not directly restricted to Keycloak's entities such as Realms, Groups, Clients etc. +## User Defined Roles -Right now in Keycloak, clients are effectively a representation of a Resource Server (among other things). You can assign roles to clients and those roles would only be usable on those clients. +A free-form role identifier would be allowed for resource-server specific roles thay may hold business meaning for users. It would lack the fixed `/kc/` or `/ud/` segments and allow any user defined path. For example: -As previously mentioned, a role would be namespaced to a certain client such as: +- `/business-units/myBusinessUnit/const-centers/myCostCenter/roleName` -- `role_v1:/ud/clients/{client-name}/{role-name}` +**Question**: In the initial proposal, these roles would be preceded by a fixed part that would identify the context where this role would be applied. We could do somethng similar here such as: -or with a regexp, they could be namespaced to a specific subset of clients: +- `/groups/{group-name}/role-path/business-units/myBusinessUnit/const-centers/myCostCenter/roleName` -- `role_v1:/ud/clients/{regexp-containing-wildcard}/{role-name}` +But not convinced on the way to let Keycloak know that that's not a role name but rather a new namespace. -A different way to be even more flexible by giving users the possibility to set their own resource-server specific would be using the custom namespaced roles. +### Representation in tokens -For example, a free form role could look like: +Let's say a user defines the following namespaced role -- `role_v1:/mycompany/resources/department-a-roles/developer` +- `/mycompany/resources/department-a-roles/developer` This could map to a token such as: @@ -236,11 +228,22 @@ This could map to a token such as: .... } ``` -Or even scope this custom namespace to a specific client or client sets: -- `role_v1:/ud/clients/{client-name-or-regexp}/frole:/mycompany/resources/department-a-roles/developer` +### Using Namespace references instead of specific roles + +Having to assign a lot of roles in a specific namespace one by one would be a big user experience nightmare, so we'll need a way to assign a role namespace itself instead of individual roles within it. + +Let's say we defined a namespace such as `/custom-roles-namespace/` with a few roles such as `developer`, `admin` and `devops`. + +Following the specification, these roles would be defined such as: -So this role can only be mapped to a token issued by those specific clients. +- `/custom-roles-namespace/developer` +- `/custom-roles-namespace/admin` +- `/custom-roles-namespace/devops` + +Maybe we want to assign all these roles to a User or a Group. The UI can allow users to assign full namespaces. + +- `mapRole("/custom-roles-namespace/#", "subject-id")` -> Keycloak would query all the roles contained in that namespace and individually assign all these roles to the subjects. #### Advantages of such a structure @@ -248,49 +251,41 @@ Having namespaced claims, will simplify how we work with roles. And will expand It would be possible to query a User's roles based on the applied context. -Let's assume that, additionally to previous assigned roles to `UserA`, this user also has the following role in `tenant2`: +We can define the following methods: -- `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse` +- `getKeycloakManagementRoles(String roleURI)` -> Queries the database for roles within the Keycloak managed namespace (`/kc/`). -Let's say we want to get all the roles for UserA: +- `getUserManagedRoles(String roleURI)` -> Queries the database for roles within the user managed namespace (`/ud/`). -- `getRoles("v1", "ud","/")` -> Returns: `role_v1:/ud/groups/iam/manager`, `role_v1:/ud/groups/devops/developer`, `role_v1:/ud/groups/devops/devops_role` and `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse`. +- `getCustomRoles(String roleURI)` -> Queries the database for roles outside of the Keycloak or User managed namespaces (`/kc/` and `/ud/`). -Now, we want to get all the roles that a user has in `/groups/devops`: +Let's say we want to get all the roles for UserA: -- `getRoles("v1", "ud", "/groups/devops")` -> Returns: `role_v1:/ud/groups/iam/manager`, `role_v1:/ud/groups/devops/developer`, `role_v1:/ud/groups/devops/devops_role` +- `getUserManagedRoles("/")` -> Returns: `/ud/groups/iam/manager`, `/ud/groups/devops/developer` and `/ud/groups/devops/devops_role`. -Or we could filter by tenant: +Now, we want to get all the roles that a user has in `/groups/devops`: -- `getRoles("v1", "ud", "/tenants/tenant2")` -> Returns: `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse`. +- `getUserManagedRoles("/groups/devops")` -> Returns: `/ud/groups/iam/manager`, `/ud/groups/devops/developer`, `/ud/groups/devops/devops_role` -Or even allow for wildcards and regexps in order to get a certain subset, for example, all roles applied to an `iam` group, regardless of the tenant (Note: this could be useful for the dashboard, but probably restricted when generating tokens) +We may also need to know whether a user can manage a realm: -- `getRoles("v1", "ud", "*/groups/iam")` -> Returns: `role_v1:/ud/groups/iam/manager`, `role_v1:/ud/tenants/tenant2/groups/iam/somethingelse`. +- `getKeycloakManagementRoles("/realms/realm-a/manage-realm")`. ### UX Improvements when creating and assigning roles When creating namespaced roles, the UI could help users place a group in a specific context that would build a role namespace in a visual way. -A User wants to create the role `developer` in tenant `TenantB`, group `GroupZ`. +A User wants to create the role `developer` in group `GroupZ`. -The UI could ask the user to add layers to the selection by showing some predefined structure: +The UI could ask the user to add layers to the selection by showing some predefined structure. Asking the user repeatedly to add more context to the namespace, if possible. -``` -What would be the root of the role namespace? -Choose between: Realm, Tenant, Client or Group. - -::: User Selects TenantB - -Want to add more contexts to the role? -As Tenant was previously selected, the UI would show: Client or Group. -``` +For example, it would make sense to ask for extra context if the first one was at a Realm level, but it wouldn't probably ask for extra context if the previous selection was a group. During this time, the role namespace would build automatically and show the user the final role namespace. The resulting role namespace for this use case would be: -- `role_v1:/ud/tenants/tenantB/groups/GroupZ/developer` +- `/ud/groups/GroupZ/developer` But the user would not have to get to this level of complexity. @@ -300,50 +295,85 @@ One of the main reasons to have this composable way or defining, assigning and r For example, when a user requests a dynamic scope `group:iam`, a mapper would be able to get al the roles that a user has in that specific group in a very easy way, by calling querying roles for that specific context: -- `getRoles("v1", "ud", "/groups/iam")` -> Returns: `role_v1:/ud/groups/iam/manager`. - -If a user requests a dynamic scope such as: `tenant:tenant1`: - -- `getRoles("v1", "ud", "/tenants/tenant1")` -> Returns: `role_v1:/ud/tenants/tenant1/groups/iam/manager`, `role_v1:/ud/tenants/tenant1/groups/devops/developer`, `role_v1:/ud/tenants/tenant1/groups/devops/devops_role` +- `getUserManagedRoles("/groups/iam")` -> Returns: `/ud/groups/iam/manager`. If the client decided to use free-form roles, such as: -- `role_v1:/mycompany/resources/department-a-roles/developer` +- `/mycompany/resources/department-a-roles/developer` A possible query for these roles based on dynamic scopes, if the `mycompany/resources` or `mycompany:resources` dynamic scope was provided, the query could look like: -- `getRoles("v1", "", "/mycompany/resources")` -> Returns: `role_v1:/mycompany/resources/department-a-roles/developer` - +- `getCustomRoles("/mycompany/resources")` -> Returns: `/mycompany/resources/department-a-roles/developer` ### Implementation details Roles are a core entity within Keycloak and making changes to them have the potential to break significant parts of the code and functionality. -An incremental way to implement namespaced roles would be to add a new field `namespace` in the `keycloak_role` database table, containig the whole id string of the role. +An incremental way to implement namespaced roles would be to add a new field `namespace` in the `keycloak_role` database table, containig the whole namespace URI string of the role. Checks would need to be made to make sure that: - On role mappings, if the namespace field is present, the following checks could be made: - - On user-role mapping: Make sure the user belongs to the group variable in the namespace, if present, same thing with tenants whenever we have them etc. + - On user-role mapping: Make sure the user belongs to the group variable in the namespace, if present. - On group-role mapping: Make sure the group in the namespace is the same as the group being mapped to the role. - On client-role mapping: Make sure the client variable in the namespace matches the client that the role is being mapped to. - On resource deletion: The system should query all roles with the resource in its namespace and delete them. - - Example: Role `role_v1:/ud/groups/groupA/developer` exists. User deletes `groupA`. The system will query `getRoles("v1", "ud, "/groups/groupA")` and delete any returned entries. + - Example: Role `/ud/groups/groupA/developer` exists. User deletes `groupA`. The system will query `getUserManagedRoles("/groups/groupA")` and delete any returned entries. + +#### Migration + +Current roles could potentially be migrated to a structure that resembles a unscoped User Managed Roles. + +For example, a Realm level role such as `developer` could be migrated to `/ud/developer` by adding the `/ud/` fragment the the `namespace` database field for the migrated role. + +A Client level role such as `uma_protection` in client with ID 123-123-123-123 could be migrated to `/ud/clients/123-123-123-123/uma_protection` by adding the `/ud/clients/123-123-123-123/` URI to the `namespace` database field for the migrated role. + +Additionally, Keycloak management roles could follow the same pattern. + +The role `view-users` created in the `realm-management` client for realm `RealmB` can be migrated to `/kc/realms/RealmB/view-users`. + +### Possible future additions + +#### Tenants + +We are working on a proposal that will enable multi-tenancy for Keycloak. + +This specification considers that there may be new entities being added to Keycloak that would need to be added to role namespaces, for direct or hierarchical role scoping. + +These are some use cases where tenants could be considered: + +- Create a role to manage a specific tenant: + - `/kc/tenants/tenant-a/manage-tenant` + +- Create a role scoped to a group, but within a specific tenant: + - `/ud/tenants/tenant-a/groups/groupA/groupScopedRole` + +When we have a hierarchical structure such as the previous one, we could also allow for wildcard queries: + +- `getUserManagedRoles("*/groups/iam")` -> Returns all roles for any group called IAM in any tenant. + +#### Namespaced Custom Roles + +The initial implementation of Custom Roles wouldn't contain a context or scope within their URI. + +A Role would be defined such as: + +- `/some/free-form/namespace/uri/role` -### Extensibility +With this format, it's impossible to restrict these roles or to add them to individual users within a group's context, so in the future, we may support adding an additional namespace URI such as: -Protocol Mappers would be able to work with preconfigured role namespace formats to create any necessary representation in tokens. +- `/ud/groups/groupB/cRole:/some/free-form/namespace/uri/role` -In case we need to evolve these roles in a non backwards compatibility way, we can start creating roles with the breaking change with a different role version preffix: +This way we could scope these custom roles to specific contexts just like we'd do with User Manged and Keycloak Managed roles. -- `role_v2:{any-possible-change}` +#### Role Versions -And the hypothetical roles query would receive the new version as a parameter: +In the initial iteration, roles namespaces wouldn't contain any version, but it would be benefitial to add them in case we need to change the structure or behaviour in the future. -`getRoles("v2", "ud", {new-query})` +We could treat existing roles as v1 and add a new URI fragment to define a version: -When creating a role, users could choose which version to use and configure new mappers to either or all versions depending on the query they run. \ No newline at end of file +- `role_v2:/(kc|ud|"")/({entity-type}/{entity-name})*/{role-name}` or `/version/v2/(kc|ud|"")/({entity-type}/{entity-name})*/{role-name}` \ No newline at end of file