User permissions migration

User permissions migration allows migrating all existing user-permission relations from mod-permissions for users with authentication information in Keycloak.

This migration is idempotent and can be repeated multiple times providing the same results

image-20241016-142207.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> start group PermissionMigrationService.migratePermissions { :log that migration started;<<task>> group UserPermissionsLoader.loadUserPermissions { :load all users from Keycloak //users loaded in batches, size is controlled by configuration properties//;<<task>> :extract user ids from loaded keycloak users and store it as ""userIds"" //Folio user id is stored as ""user_id"" attribute in KeycloakUser entity//;<<input>> :create an empty list with user permissions as ""List<UserPermission> userPermissionsData""; while (""userIds"" has next element) is (yes) :get next user id from ""users""; :load user permissions from ""mod-permissions"" by ""userId"" as ""userPermissions"" //""/users/{userId}/permissions"" endpoint is used//;<<input>> :extract permission names from ""userPermissions"" as ""permissionNames"";<<task>> if (""permissionNames"" is empty) then (yes) :log that user has no permissions;<<output>> else (no) :remove duplicates from ""permissionNames"" and store it as ""uniquePermissionNames"";<<task>> :sort alphabetically ""uniquePermissionNames"" as ""sortedUniquePermissionNames"";<<task>> :concatenate ""sortedUniquePermissionNames"" using delimiter ""'|'"" as ""concatenatedPermissionString"";<<task>> :generate ""roleName"" as sha1 hash from ""concatenatedPermissionString"";<<task>> :create ""userPermissionsValue"" from ""userId"", ""roleName"", ""uniquePermissionNames"";<<task>> :add ""userPermissionsValue"" to ""userPermissionsData"" list;<<output>> end if end while (no) :return ""userPermissionsData"";<<output>> } group MigrationRoleCreator.createRoles { :extract unique role names from ""userPermissionsData"" as ""uniqueRoleNames"";<<input>> :create roles using ""RoleService"" batch API as ""createdRoles"";<<task>> if (""createdRoles"" size is equal to ""uniqueRoleNames"" size) then (yes) :return ""createdRoles"";<<output>> else (no) :retrieve roles by ""uniqueRoleNames"" from ""RoleService"" as ""foundRoles"";<<output>> :return ""foundRoles"" as ""createdRoles"";<<output>> end if } group validateAndGetUserPermissionsWithRoles { :group ""createdRoles"" by ""roleName"" as ""createdRolesByName"";<<task>> :for each value in ""userPermissionsData"" retrieve role object by ""roleName"" from ""createdRolesByName"";<<task>> if (all roles are not found by all ""roleNames"") then (yes) :thrown MigrationException;<<output>> end else (no) :return updated ""userPermissionsData"";<<output>> end if } group MigrationRoleCreator.assignUsers { :group users by ""roleName"" as map where key = role identifier and value is a collection of user ids //""userId"" and generated ""roleName"" are extracted from ""userPermissionsData""; :create a user-role relation for each ""Pair(userId, roleId)"" in ""UserRoleService""; if (errors occurred during user-role relation assignment) then (yes) :thrown MigrationException;<<output>> end else (no) :log the number of created user-role relations;<<output>> end if } group RolePermissionAssignor.assignPermissions { :define a set with visited role identifiers as ""visitedRoleIds"";<<task>> while (""userPermissionsData"" has next value) is (yes) :get next ""userPermissions"" from ""userPermissionsData"";<<input>> :get role id as ""roleId"" from ""userPermissions"";<<input>> if (""visitedRoleIds"" contains ""roleId"") then (no) :get list of permission names as ""permissions"" from ""userPermissions"";<<input>> :find capabilities as ""foundCapabilities"" by ""permissions"" using ""CapabilityService"";<<task>> :extract ""capabilityPermissionNames"" from ""foundCapabilities"";<<task>> :define ""notFoundPermissions"" as difference between ""permissions"" and ""capabilityPermissionNames"";<<input>> if (""foundCapabilities"" collection is not empty) then (yes) :assign ""foundCapabilities"" to a role with ""roleId"";<<task>> end if :find capability sets as ""foundCapabilitySets"" by ""permissions"" using ""CapabilitySetService"";<<task>> :extract ""capabilitySetPermissionNames"" from ""foundCapabilitySets"";<<task>> :define ""notFoundPermissionsInSets"" as difference between ""notFoundPermissions"" and ""capabilitySetPermissionNames"";<<input>> if (""foundCapabilitySets"" collection is not empty) then (yes) :assign ""foundCapabilitySets"" to a role with ""roleId"";<<task>> end if :log assigned permissions for role with ""roleId"";<<output>> if (""notFoundPermissionsInSets"" collection is not empty) then (yes) :[warn] log ""notFoundPermissionsInSets"" were not assigned for role with ""roleId"";<<output>> end if else (yes) end if end while } :log that migration finished;<<task>> } end @enduml