Link management between Capabilities/CapabilitySets and Users/Roles

Overview

This page describes algorithms that assign capabilities to a user and role, capability sets to a user, and role.

Capabilities and capability sets are assigned individually

Example:

There is a capability: foo.item.view [endpoints: GET /foo/item/{id}], foo.item.create [endpoints: POST /foo/item], foo.item.update [endpoints: PUT /foo/item/{id}] and capability set foo.item.manage uniting foo.item.view, foo.item.create and foo.item.update.

Usage Scenarios

Scenario #1 (role-capability and role-capabilitySet management)

  1. User assigned capability set foo.item.manage to a role Foo management role (sampleRoleId). The following resources will be created:

  • Entities in mod-roles-keycloak database:

    • policy: [policyId, 'Policy for role: sampleRoleId']

    • roleCapabilitySet: [policyId, fooItemManageCapabilitySetId]

  • Entities in Keycloak:

    • Policy: [policyId, 'Policy for role: sampleRoleId']

    • Permission: [name: 'GET access for role: sampleRoleId to /foo/item/{id}', scope: 'GET', resource: '/foo/item/{id}', policy: policyId]

    • Permission: [name: 'POST access for role: sampleRoleId to /foo/item', scope: 'POST', resource: '/foo/item', policy: policyId]

    • Permission: [PUT access for role: sampleRoleId to /foo/item/{id}, scope: 'GET', resource: '/foo/item/{id}', policy: policyId]

  1. Then a user assigns explicitly foo.item.view capability to a role with a sampleRoleId. The following resources will be created:

  • Entities in mod-roles-keycloak database:

    • roleCapability: [policyId, fooItemViewCapabilityId]

Note that keycloak entities are not affected, because permission: Permission: [name: 'GET access for role: sampleRoleId to /foo/item/{id}', scope: 'GET', resource: '/foo/item/{id}', policy: policyId] already exists

  1. Then a user removes assignment foo.item.manage from a role with a sampleRoleId. The following resources will be deleted:

  • Entities in mod-roles-keycloak database:

    • roleCapabilitySet: [policyId, fooItemManageCapabilitySetId]

  • Entities in Keycloak:

    • Permission: [name: 'POST access for role: sampleRoleId to /foo/item', scope: 'POST', resource: '/foo/item', policy: policyId]

    • Permission: [PUT access for role: sampleRoleId to /foo/item/{id}, scope: 'GET', resource: '/foo/item/{id}', policy: policyId]

Not that Permission: [name: 'GET access for role: sampleRoleId to /foo/item/{id}', scope: 'GET', resource: '/foo/item/{id}', policy: policyId] is not deleted, because it was explicitly assigned to a role via relation roleCapability: [policyId, fooItemViewCapabilityId]

The same process works for user-capability, user-capabilitySet link management

Capability Relation Management

Role

Capability assignment process

safeCreate flag is used only internally, for outside requests a user must always assign a set of new capability ids, if not - an update operation allows the creation and delete capabilities in a single request

 

role-capability-assigment-20241014-152818.png
@startuml !theme mono !pragma useVerticalIf on skinparam conditionStyle inside skinparam defaultTextAlignment center skinparam defaultFontSize 12 <style> activityDiagram { activity { MaximumWidth 600 backgroundColor #f0f0f0 LineColor DimGrey } diamond { HorizontalAlignment center } group { LineColor LightGrey FontSize 10 } } </style> group RoleCapabilityServiceImpl { :get role by id;<<input>> :get ""roleCapabilities"" by ""roleId"";<<task>> :extract existing ""capabilityIds"" from ""roleCapabilities"";<<task>> if (not ""safeCreate"" AND ""capabilityIds"" not empty)) then (yes) :throw EntityExistsException;<<output>> end else (no) :get difference between new and existing ""capabilityIds"" sets as ""newCapabilityIds"";<<input>> end if group assignCapabilities { :get assigned capabilities through capability sets as ""assignedCapabilityIds"";<<input>> :store union of ""assignedCapabilityIds"" and ""newCapabilityIds"" as ""assignedCapabilityIds"";<<input>> group #LightSteelBlue CapabilityEndpointService.getByCapabilityIds { :get difference with ""newCapabilityIds"" and ""assignedCapabilityIds"" as ""changedIdentifiers"";<<input>> :get changed capability endpoints by querying capabilities by ""changedIdentifiers"" and extracting distinct list of assigned endpoints and store it as ""changedCapabilityEndpoints"";<<task>> :get assigned capability endpoints by querying capabilities by ""assignedIds"" and extracting distinct list of assigned endpoints and store it as ""assignedCapabilityEndpoints"";<<task>> :return subtraction ""assignedCapabilityEndpoints"" from ""changedCapabilityEndpoints"";<<output>> } :rolePermissionsService.createPermissions for ""roleId"" and ""endpoints"";<<task>> group #LightSteelBlue CapabilityEndpointService.getByCapabilityIds { if (""endpoints"" are empty) then (yes) :do nothing;<<output>> else (no) :get role by ""roleId"";<<input>> :generate policy name for ""role"";<<input>> :getOrCreate role policy by name //Policy name template:// ""Policy for role: {{roleId}}"";<<task>> :create permissions in Keycloak with role policy, list of endpoint and using permissionNameGenerator //Permission name template:// ""{{httpMethod}} access for role '{{roleId}}' to '{{path}}'""; end if } :generate RoleCapabilityEntity for ""newIds"" and store them as ""entities"";<<task>> :upsert entities to table ""role_capability"" in database;<<output>> :convert ""RoleCapabilityEntity"" to ""RoleCapability"" and return them as ""PageResult"";<<output>> } } @enduml

Capability remove process

image-20241015-153314.png
@startuml !theme mono !pragma useVerticalIf on skinparam conditionStyle inside skinparam defaultTextAlignment center skinparam defaultFontSize 12 <style> activityDiagram { activity { MaximumWidth 600 backgroundColor #f0f0f0 LineColor DimGrey } diamond { HorizontalAlignment center } group { LineColor LightGrey FontSize 10 } } </style> group RoleCapabilityServiceImpl { :get existing ""roleCapabilities"" by ""roleId"";<<task>> :extract existing ""capabilityIds"" from ""roleCapabilities"";<<task>> if (""capabilityIds"" is empty) then (yes) :do nothing;<<output>> end else (no) :get intersection between ""capabilityIds"" and existing ""capabilityIds"" sets as ""deprecatedIds"";<<input>> :get subtraction between ""assignedCapabilityIds"" and existing ""deprecatedIds"" sets as ""assignedIds"";<<input>> end if group removeCapabilities { :get assigned capabilities through capability sets as ""assignedCapabilityIds"";<<input>> :store union of ""assignedCapabilityIds"" and ""assignedIds"" as ""assignedCapabilityIds"";<<task>> group #LightSteelBlue CapabilityEndpointService.getByCapabilityIds { :get difference with ""deprecatedIds"" and ""assignedIds"" as ""changedIdentifiers"";<<input>> :get changed capability endpoints by querying capabilities by ""changedIdentifiers"" and extracting distinct list of assigned endpoints and store it as ""changedCapabilityEndpoints"";<<task>> :get assigned capability endpoints by querying capabilities by ""assignedIds"" and extracting distinct list of assigned endpoints and store it as ""assignedCapabilityEndpoints"";<<task>> :return subtraction ""assignedCapabilityEndpoints"" from ""changedCapabilityEndpoints"";<<output>> } :rolePermissionsService.deletePermissions for ""roleId"" and ""endpoints"";<<task>> group #LightSteelBlue CapabilityEndpointService.getByCapabilityIds { if (""endpoints"" are empty) then (yes) :do nothing;<<output>> else (no) :get role by ""roleId"";<<input>> :generate policy name for ""role"";<<input>> :get policy by ""name"" and type == ""ROLE"";<<task>> :delete permissions in Keycloak with role policy, list of endpoint and using permissionNameGenerator //Permission name template:// ""{{httpMethod}} access for role '{{roleId}}' to '{{path}}'""; end if } :delete entities from table ""role_capability"" in database by ""roleId"" and ""deprecatedIds"";<<output>> } } @enduml

Capability update process

A capability update process combines capability assignment and capability remove processes, where unmodified capabilities and corresponding keycloak permissions remain intact.

CapabilitySet assignment process

@startuml !theme mono !pragma useVerticalIf on skinparam conditionStyle inside skinparam defaultTextAlignment center skinparam defaultFontSize 12 <style> activityDiagram { activity { MaximumWidth 600 backgroundColor #f0f0f0 LineColor DimGrey } diamond { HorizontalAlignment center } group { LineColor LightGrey FontSize 10 } } </style> group RoleCapabilityServiceImpl { :get role by id;<<input>> :get ""roleCapabilitySets"" by ""roleId"";<<task>> :extract existing ""capabilitySetIds"" from ""roleCapabilitySets"";<<task>> if (not ""safeCreate"" AND ""capabilitySetIds"" not empty)) then (yes) :throw EntityExistsException;<<output>> end else (no) :get difference between new and existing ""capabilitySetIds"" sets as ""newSetIds"";<<input>> end if group #LightSteelBlue assignCapabilitySets { group getChangedEndpoints { :retrieve ""Capability"" entities by ""roleId"" as ""directlyAssignedCapabilities"";<<input>> :extract endpoints from ""directlyAssignedCapabilities"" and store them as ""excludedEndpoints"";<<task>> group capabilityEndpointService.getByCapabilitySetIds { :retrieve ""Capability"" entities by ""newSetIds"" as ""changedCapabilities"";<<input>> :retrieve ""Capability"" entities by ""assignedSetIds"" as ""assignedCapabilities"";<<input>> :extract endpoints from ""changedCapabilities"" and store them as ""changedCapabilitySetEndpoints"";<<task>> :extract endpoints from ""assignedCapabilities"" and store them as ""assignedCapabilitySetEndpoints"";<<task>> :return subtraction ""assignedCapabilitySetEndpoints"" and ""excludedEndpoints"" from ""changedCapabilitySetEndpoints"" as ""List<Endpoint"";<<output>> } } :create keycloak permissions in ""RolePermissionsService"" for ""roleId"" and ""endpoints"";<<task>> :generate ""RoleCapabilitySetEntity"" for ""newSetIds"" and store them as ""entities"";<<task>> :upsert entities to table ""role_capability_set"" in database;<<output>> :convert ""RoleCapabilitySetEntity"" to ""RoleCapabilitySet"" and return them as ""PageResult"";<<output>> } } @enduml

CapabilitySet removal process

 

User

Capability assignment process

safeCreate flag is used only internally, for outside requests a user must always assign a set of new capability ids, if not - an update operation allows the creation and delete capabilities in a single request

Capability removal process

Capability update process

A capability update process combines capability assignment and capability remove processes, where unmodified capabilities and corresponding keycloak permissions remain intact.

CapabilitySet assignment process

CapabilitySet removal process