@@ -27,6 +27,7 @@ import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClient
2727import org.eclipse.apoapsis.ortserver.clients.keycloak.RoleName
2828import org.eclipse.apoapsis.ortserver.clients.keycloak.UserId
2929import org.eclipse.apoapsis.ortserver.clients.keycloak.UserName
30+ import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable
3031import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission
3132import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission
3233import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission
@@ -35,8 +36,20 @@ import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Pr
3536import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.RepositoryRole
3637import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Role
3738import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser
39+ import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole as DbOrganizationRole
40+ import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole as DbProductRole
41+ import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole as DbRepositoryRole
42+ import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role as DbRole
43+ import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService as DbAuthorizationService
3844import org.eclipse.apoapsis.ortserver.dao.dbQuery
45+ import org.eclipse.apoapsis.ortserver.dao.repositories.organization.OrganizationsTable
46+ import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable
47+ import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable
48+ import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
3949import org.eclipse.apoapsis.ortserver.model.HierarchyId
50+ import org.eclipse.apoapsis.ortserver.model.OrganizationId
51+ import org.eclipse.apoapsis.ortserver.model.ProductId
52+ import org.eclipse.apoapsis.ortserver.model.RepositoryId
4053import org.eclipse.apoapsis.ortserver.model.repositories.OrganizationRepository
4154import org.eclipse.apoapsis.ortserver.model.repositories.ProductRepository
4255import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository
@@ -53,7 +66,7 @@ internal const val ROLE_DESCRIPTION = "This role is auto-generated, do not edit
5366/* *
5467 * An implementation of [AuthorizationService], based on [Keycloak](https://www.keycloak.org/).
5568 */
56- @Suppress(" TooManyFunctions" )
69+ @Suppress(" TooManyFunctions" , " LargeClass " )
5770class KeycloakAuthorizationService (
5871 private val keycloakClient : KeycloakClient ,
5972 private val db : Database ,
@@ -65,7 +78,13 @@ class KeycloakAuthorizationService(
6578 * A prefix for Keycloak group names, to be used when multiple instances of ORT Server share the same Keycloak
6679 * realm.
6780 */
68- private val keycloakGroupPrefix : String
81+ private val keycloakGroupPrefix : String ,
82+
83+ /* *
84+ * The reworked authorization service that stores authorization data in the database. This is used for the
85+ * migration functionality.
86+ */
87+ private val dbAuthorizationService : DbAuthorizationService
6988) : AuthorizationService {
7089 override suspend fun createOrganizationPermissions (organizationId : Long ) {
7190 OrganizationPermission .getRolesForOrganization(organizationId).forEach { roleName ->
@@ -780,4 +799,165 @@ class KeycloakAuthorizationService(
780799 return keycloakClient.getUserClientRoles(UserId (userId))
781800 .mapTo(mutableSetOf ()) { role -> role.name.value }
782801 }
802+
803+ override suspend fun migrateRolesToDb (): Boolean {
804+ if (! canMigrate()) return false
805+
806+ logger.warn(" Starting migration of Keycloak roles to database-based roles." )
807+
808+ val organizationIds = db.dbQuery {
809+ OrganizationsTable .select(OrganizationsTable .id)
810+ .map { it[OrganizationsTable .id].value }
811+ }
812+
813+ logger.info(" Migrating {} organizations." , organizationIds.size)
814+ organizationIds.forEach { organizationId ->
815+ migrateOrganizationRolesToDb(organizationId)
816+ }
817+
818+ logger.info(" Migrating superusers" )
819+ migrateUsersInGroupToDb(
820+ GroupName (keycloakGroupPrefix + Superuser .GROUP_NAME ),
821+ DbOrganizationRole .ADMIN ,
822+ CompoundHierarchyId .WILDCARD
823+ )
824+
825+ return true
826+ }
827+
828+ /* *
829+ * Migrate the access rights for the organization with the given [organizationId] to the new database-based roles.
830+ * This includes the migration of all products and repositories belonging to the organization.
831+ */
832+ private suspend fun migrateOrganizationRolesToDb (organizationId : Long ) {
833+ logger.info(" Migrating roles for organization '{}'." , organizationId)
834+ val organizationHierarchyId = CompoundHierarchyId .forOrganization(OrganizationId (organizationId))
835+
836+ migrateElementRolesToDb(
837+ oldRoles = OrganizationRole .entries,
838+ newRoles = DbOrganizationRole .entries,
839+ id = OrganizationId (organizationId),
840+ newHierarchyID = organizationHierarchyId
841+ )
842+
843+ val productIds = db.dbQuery {
844+ ProductsTable .select(ProductsTable .id)
845+ .where { ProductsTable .organizationId eq organizationId }
846+ .map { it[ProductsTable .id].value }
847+ }
848+
849+ logger.info(" Migrating {} products for organization '{}'." , productIds.size, organizationId)
850+ productIds.forEach { productId ->
851+ val productHierarchyId = CompoundHierarchyId .forProduct(
852+ OrganizationId (organizationId),
853+ ProductId (productId)
854+ )
855+ migrateProductRolesToDb(productHierarchyId)
856+ }
857+ }
858+
859+ /* *
860+ * Migrate the access rights for the product with the given [productHierarchyId] to the new database-based roles.
861+ * This includes the migration of all repositories belonging to the product.
862+ */
863+ private suspend fun migrateProductRolesToDb (productHierarchyId : CompoundHierarchyId ) {
864+ val productId = requireNotNull(productHierarchyId.productId)
865+ logger.info(" Migrating roles for product '{}'." , productId)
866+
867+ migrateElementRolesToDb(
868+ oldRoles = ProductRole .entries,
869+ newRoles = DbProductRole .entries,
870+ id = productId,
871+ productHierarchyId
872+ )
873+
874+ val repositoryIds = db.dbQuery {
875+ RepositoriesTable .select(RepositoriesTable .id)
876+ .where { RepositoriesTable .productId eq productId.value }
877+ .map { it[RepositoriesTable .id].value }
878+ }
879+
880+ logger.info(" Migrating {} repositories for product '{}'." , repositoryIds.size, productId)
881+ repositoryIds.forEach { repositoryId ->
882+ val repositoryHierarchyId = CompoundHierarchyId .forRepository(
883+ requireNotNull(productHierarchyId.organizationId),
884+ productId,
885+ RepositoryId (repositoryId)
886+ )
887+ migrateRepositoryRolesToDb(repositoryHierarchyId)
888+ }
889+ }
890+
891+ /* *
892+ * Migrate the access rights for the repository with the given [repositoryId] to the new database-based roles.
893+ */
894+ private suspend fun migrateRepositoryRolesToDb (repositoryId : CompoundHierarchyId ) {
895+ logger.info(" Migrating roles for repository '{}'." , repositoryId.repositoryId)
896+
897+ migrateElementRolesToDb(
898+ oldRoles = RepositoryRole .entries,
899+ newRoles = DbRepositoryRole .entries,
900+ id = requireNotNull(repositoryId.repositoryId),
901+ repositoryId
902+ )
903+ }
904+
905+ /* *
906+ * Migrate all users in the given [oldRoles] to the corresponding [newRoles] for the hierarchy element with the
907+ * given [id] and [newHierarchyID].
908+ */
909+ private suspend fun <ID : HierarchyId > migrateElementRolesToDb (
910+ oldRoles : Collection <Role <* , ID >>,
911+ newRoles : Collection <DbRole >,
912+ id : ID ,
913+ newHierarchyID : CompoundHierarchyId
914+ ) {
915+ oldRoles.zip(newRoles).forEach { (oldRole, newRole) ->
916+ migrateUsersForRoleToDb(oldRole, newRole, id, newHierarchyID)
917+ }
918+ }
919+
920+ /* *
921+ * Migrate all users assigned to the given [oldRole] for the hierarchy element with the given [id] to the new
922+ * [newRole] in the database, using the provided [newHierarchyID].
923+ */
924+ private suspend fun <ID : HierarchyId > migrateUsersForRoleToDb (
925+ oldRole : Role <* , ID >,
926+ newRole : DbRole ,
927+ id : ID ,
928+ newHierarchyID : CompoundHierarchyId
929+ ) {
930+ val groupName = GroupName (keycloakGroupPrefix + oldRole.groupName(id))
931+ migrateUsersInGroupToDb(groupName, newRole, newHierarchyID)
932+ }
933+
934+ /* *
935+ * Migrate all users in the Keycloak group with the given [groupName] (which represents a role) to the given
936+ * [newRole] for the hierarchy element with the given [newHierarchyID].
937+ */
938+ private suspend fun migrateUsersInGroupToDb (
939+ groupName : GroupName ,
940+ newRole : DbRole ,
941+ newHierarchyID : CompoundHierarchyId
942+ ) {
943+ runCatching {
944+ keycloakClient.getGroupMembers(groupName).forEach { user ->
945+ dbAuthorizationService.assignRole(
946+ userId = user.username.value,
947+ role = newRole,
948+ compoundHierarchyId = newHierarchyID
949+ )
950+ }
951+ }.onFailure { exception ->
952+ logger.error(" Failed to load users in group '${groupName.value} ' during migration." , exception)
953+ }
954+ }
955+
956+ /* *
957+ * Return a flag whether the migration of access rights to the new database structures is possible. This is the
958+ * case
959+ */
960+ private suspend fun canMigrate (): Boolean = db.dbQuery {
961+ RoleAssignmentsTable .select(RoleAssignmentsTable .id).count() == 0L
962+ }
783963}
0 commit comments