poapsis.ortserver.services.authorization-service.0.1.0-RC6.source-code.DefaultAuthorizationService.kt Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2023 The ORT Server Authors (See )
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/
package org.eclipse.apoapsis.ortserver.services
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.apoapsis.ortserver.clients.keycloak.GroupName
import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClient
import org.eclipse.apoapsis.ortserver.clients.keycloak.RoleName
import org.eclipse.apoapsis.ortserver.clients.keycloak.UserName
import org.eclipse.apoapsis.ortserver.dao.dbQuery
import org.eclipse.apoapsis.ortserver.model.authorization.OrganizationPermission
import org.eclipse.apoapsis.ortserver.model.authorization.OrganizationRole
import org.eclipse.apoapsis.ortserver.model.authorization.ProductPermission
import org.eclipse.apoapsis.ortserver.model.authorization.ProductRole
import org.eclipse.apoapsis.ortserver.model.authorization.RepositoryPermission
import org.eclipse.apoapsis.ortserver.model.authorization.RepositoryRole
import org.eclipse.apoapsis.ortserver.model.authorization.Superuser
import org.eclipse.apoapsis.ortserver.model.repositories.OrganizationRepository
import org.eclipse.apoapsis.ortserver.model.repositories.ProductRepository
import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository
import org.eclipse.apoapsis.ortserver.model.util.removePrefixOrNull
import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory
private val logger = LoggerFactory.getLogger(DefaultAuthorizationService::class.java)
internal const val ROLE_DESCRIPTION = "This role is auto-generated, do not edit or remove."
/**
* The default implementation of [AuthorizationService], using a [keycloakClient] to manage Keycloak roles.
*/
@Suppress("TooManyFunctions")
class DefaultAuthorizationService(
private val keycloakClient: KeycloakClient,
private val db: Database,
private val organizationRepository: OrganizationRepository,
private val productRepository: ProductRepository,
private val repositoryRepository: RepositoryRepository,
/**
* A prefix for Keycloak group names, to be used when multiple instances of ORT Server share the same Keycloak
* realm.
*/
private val keycloakGroupPrefix: String
) : AuthorizationService {
override suspend fun createOrganizationPermissions(organizationId: Long) {
OrganizationPermission.getRolesForOrganization(organizationId).forEach { roleName ->
keycloakClient.createRole(name = RoleName(roleName), description = ROLE_DESCRIPTION)
}
}
override suspend fun deleteOrganizationPermissions(organizationId: Long) {
OrganizationPermission.getRolesForOrganization(organizationId).forEach { roleName ->
keycloakClient.deleteRole(RoleName(roleName))
}
}
override suspend fun createOrganizationRoles(organizationId: Long) {
OrganizationRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(organizationId))
keycloakClient.createRole(name = roleName, description = ROLE_DESCRIPTION)
role.permissions.forEach { permission ->
val compositeRole = keycloakClient.getRole(RoleName(permission.roleName(organizationId)))
keycloakClient.addCompositeRole(roleName, compositeRole.id)
}
val groupName = GroupName(keycloakGroupPrefix + role.groupName(organizationId))
keycloakClient.createGroup(groupName)
keycloakClient.addGroupClientRole(keycloakClient.getGroup(groupName).id, keycloakClient.getRole(roleName))
}
}
override suspend fun deleteOrganizationRoles(organizationId: Long) {
OrganizationRole.entries.forEach { role ->
keycloakClient.deleteRole(RoleName(role.roleName(organizationId)))
keycloakClient.deleteGroup(
keycloakClient.getGroup(GroupName(keycloakGroupPrefix + role.groupName(organizationId))).id
)
}
}
override suspend fun createProductPermissions(productId: Long) {
ProductPermission.getRolesForProduct(productId).forEach { roleName ->
keycloakClient.createRole(name = RoleName(roleName), description = ROLE_DESCRIPTION)
}
}
override suspend fun deleteProductPermissions(productId: Long) {
ProductPermission.getRolesForProduct(productId).forEach { roleName ->
keycloakClient.deleteRole(RoleName(roleName))
}
}
override suspend fun createProductRoles(productId: Long) {
val product = checkNotNull(productRepository.get(productId))
val organization = checkNotNull(organizationRepository.get(product.organizationId))
ProductRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(productId))
keycloakClient.createRole(name = roleName, description = ROLE_DESCRIPTION)
role.permissions.forEach { permission ->
val compositeRole = keycloakClient.getRole(RoleName(permission.roleName(productId)))
keycloakClient.addCompositeRole(roleName, compositeRole.id)
}
OrganizationRole.entries.find { it.includedProductRole == role }?.let { orgRole ->
val parentRole = keycloakClient.getRole(RoleName(orgRole.roleName(organization.id)))
val childRole = keycloakClient.getRole(roleName)
keycloakClient.addCompositeRole(parentRole.name, childRole.id)
}
val groupName = GroupName(keycloakGroupPrefix + role.groupName(productId))
keycloakClient.createGroup(groupName)
keycloakClient.addGroupClientRole(keycloakClient.getGroup(groupName).id, keycloakClient.getRole(roleName))
}
}
override suspend fun deleteProductRoles(productId: Long) {
ProductRole.entries.forEach { role ->
keycloakClient.deleteRole(RoleName(role.roleName(productId)))
keycloakClient.deleteGroup(
keycloakClient.getGroup(GroupName(keycloakGroupPrefix + role.groupName(productId))).id
)
}
}
override suspend fun createRepositoryPermissions(repositoryId: Long) {
RepositoryPermission.getRolesForRepository(repositoryId).forEach { roleName ->
keycloakClient.createRole(name = RoleName(roleName), description = ROLE_DESCRIPTION)
}
}
override suspend fun deleteRepositoryPermissions(repositoryId: Long) {
RepositoryPermission.getRolesForRepository(repositoryId).forEach { roleName ->
keycloakClient.deleteRole(RoleName(roleName))
}
}
override suspend fun createRepositoryRoles(repositoryId: Long) {
val repository = checkNotNull(repositoryRepository.get(repositoryId))
val product = checkNotNull(productRepository.get(repository.productId))
RepositoryRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(repositoryId))
keycloakClient.createRole(name = roleName, description = ROLE_DESCRIPTION)
role.permissions.forEach { permission ->
val compositeRole = keycloakClient.getRole(RoleName(permission.roleName(repositoryId)))
keycloakClient.addCompositeRole(roleName, compositeRole.id)
}
ProductRole.entries.find { it.includedRepositoryRole == role }?.let { productRole ->
val parentRole = keycloakClient.getRole(RoleName(productRole.roleName(product.id)))
val childRole = keycloakClient.getRole(roleName)
keycloakClient.addCompositeRole(parentRole.name, childRole.id)
}
val groupName = GroupName(keycloakGroupPrefix + role.groupName(repositoryId))
keycloakClient.createGroup(groupName)
keycloakClient.addGroupClientRole(keycloakClient.getGroup(groupName).id, keycloakClient.getRole(roleName))
}
}
override suspend fun deleteRepositoryRoles(repositoryId: Long) {
RepositoryRole.entries.forEach { role ->
keycloakClient.deleteRole(RoleName(role.roleName(repositoryId)))
keycloakClient.deleteGroup(
keycloakClient.getGroup(GroupName(keycloakGroupPrefix + role.groupName(repositoryId))).id
)
}
}
override suspend fun synchronizePermissions() {
withContext(Dispatchers.IO) {
val roles = keycloakClient.getRoles().mapTo(mutableSetOf()) { it.name.value }
synchronizeOrganizationPermissions(roles)
synchronizeProductPermissions(roles)
synchronizeRepositoryPermissions(roles)
}
}
/**
* Synchronize the [organization permissions][OrganizationPermission] with the Keycloak roles by adding missing
* roles and removing obsolete roles that start with the
* [organization permission prefix][OrganizationPermission.rolePrefix], based on the provided list of existing
* Keycloak [roles].
*/
private suspend fun synchronizeOrganizationPermissions(roles: Set) {
logger.info("Synchronizing Keycloak roles for organization permissions.")
runCatching {
val organizationIds = db.dbQuery { organizationRepository.list() }.mapTo(mutableSetOf()) { it.id }
organizationIds.forEach { organizationId ->
val requiredRoles = OrganizationPermission.getRolesForOrganization(organizationId)
val rolePrefix = OrganizationPermission.rolePrefix(organizationId)
synchronizeKeycloakRoles(roles, requiredRoles, rolePrefix)
}
removeRoles(
roles.filter {
val roleOrganizationId = OrganizationPermission.extractOrganizationIdFromRole(it)
roleOrganizationId != null && roleOrganizationId !in organizationIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak roles for organization permissions.", it)
}.getOrThrow()
}
/**
* Synchronize the [product permissions][ProductPermission] with the Keycloak roles by adding missing roles and
* removing obsolete roles that start with the
* [product permission prefix][ProductPermission.rolePrefix], based on the provided list of existing
* Keycloak [roles].
*/
private suspend fun synchronizeProductPermissions(roles: Set) {
logger.info("Synchronizing Keycloak roles for product permissions.")
runCatching {
val productIds = db.dbQuery { productRepository.list() }.mapTo(mutableSetOf()) { it.id }
productIds.forEach { productId ->
val requiredRoles = ProductPermission.getRolesForProduct(productId)
val rolePrefix = ProductPermission.rolePrefix(productId)
synchronizeKeycloakRoles(roles, requiredRoles, rolePrefix)
}
removeRoles(
roles.filter {
val roleProductId = ProductPermission.extractProductIdFromRole(it)
roleProductId != null && roleProductId !in productIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak roles for product permissions.", it)
}.getOrThrow()
}
/**
* Synchronize the [repository permissions][RepositoryPermission] with the Keycloak roles by adding missing roles
* and removing obsolete roles that start with the
* [repository permission prefix][RepositoryPermission.rolePrefix], based on the provided list of existing
* Keycloak [roles].
*/
private suspend fun synchronizeRepositoryPermissions(roles: Set) {
logger.info("Synchronizing Keycloak roles for repository permissions.")
runCatching {
val repositoryIds = db.dbQuery { repositoryRepository.list() }.mapTo(mutableSetOf()) { it.id }
repositoryIds.forEach { repository ->
val requiredRoles = RepositoryPermission.getRolesForRepository(repository)
val rolePrefix = RepositoryPermission.rolePrefix(repository)
synchronizeKeycloakRoles(roles, requiredRoles, rolePrefix)
}
removeRoles(
roles.filter {
val roleRepositoryId = RepositoryPermission.extractRepositoryIdFromRole(it)
roleRepositoryId != null && roleRepositoryId !in repositoryIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak roles for repository permissions.", it)
}.getOrThrow()
}
override suspend fun synchronizeRoles() {
withContext(Dispatchers.IO) {
val roles = keycloakClient.getRoles().mapTo(mutableSetOf()) { it.name.value }
val groups = keycloakClient.getGroups().map { it.name.value }
.filterTo(mutableSetOf()) { it.startsWith(keycloakGroupPrefix) }
synchronizeOrganizationRoles(roles)
synchronizeOrganizationGroups(groups)
synchronizeProductRoles(roles)
synchronizeProductGroups(groups)
synchronizeRepositoryRoles(roles)
synchronizeRepositoryGroups(groups)
}
}
override suspend fun ensureSuperuserAndSynchronizeRolesAndPermissions() {
runCatching {
logger.info("Ensuring superuser role and group.")
this.ensureSuperuser()
logger.info("Synchronizing Keycloak permissions.")
this.synchronizePermissions()
logger.info("Synchronizing Keycloak roles.")
this.synchronizeRoles()
}.onSuccess {
logger.info("Synchronized Keycloak permissions and roles.")
}.onFailure {
logger.error("Error while synchronizing Keycloak permissions and roles.", it)
}
}
/**
* Synchronize the [organization roles][OrganizationRole] with the Keycloak roles by adding missing roles, removing
* obsolete roles that start with the [organization role prefix][OrganizationRole.rolePrefix], and adding the
* [defined permission roles][OrganizationRole.permissions] as composite roles, based on the provided list of
* existing Keycloak [roles].
*/
private suspend fun synchronizeOrganizationRoles(roles: Set) {
logger.info("Synchronizing Keycloak roles for organization roles.")
runCatching {
val organizationIds = db.dbQuery { organizationRepository.list().mapTo(mutableSetOf()) { it.id } }
organizationIds.forEach { organizationId ->
// Make sure that all roles exist.
val requiredRoles = OrganizationRole.getRolesForOrganization(organizationId)
val rolePrefix = OrganizationRole.rolePrefix(organizationId)
synchronizeKeycloakRoles(roles, requiredRoles, rolePrefix)
// Make sure that roles have the correct composite roles.
OrganizationRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(organizationId))
val requiredCompositeRoles = role.permissions.map { it.roleName(organizationId) }
val actualCompositeRoles = keycloakClient.getCompositeRoles(roleName).map { it.name.value }
val compositeRolePrefix = OrganizationPermission.rolePrefix(organizationId)
synchronizeKeycloakCompositeRoles(
roleName,
requiredCompositeRoles,
actualCompositeRoles,
compositeRolePrefix
)
}
}
// Make sure that roles for non-existing organizations are removed.
removeRoles(
roles.filter {
val roleOrganizationId = OrganizationRole.extractOrganizationIdFromRole(it)
roleOrganizationId != null && roleOrganizationId !in organizationIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak roles for organization roles.", it)
}.getOrThrow()
}
/**
* Synchronize the [product roles][ProductRole] with the Keycloak roles by adding missing roles, removing obsolete
* roles that start with the [product role prefix][ProductRole.rolePrefix], adding the product roles as composites
* to the related [organization roles][OrganizationRole.includedProductRole], and adding the
* [defined permission roles][ProductRole.permissions] as composite roles, based on the provided list of
* existing Keycloak [roles].
*/
private suspend fun synchronizeProductRoles(roles: Set) {
logger.info("Synchronizing Keycloak roles for product roles.")
runCatching {
val products = db.dbQuery { productRepository.list() }
products.forEach { product ->
// Make sure that all roles exist.
val requiredRoles = ProductRole.getRolesForProduct(product.id)
val rolePrefix = ProductRole.rolePrefix(product.id)
synchronizeKeycloakRoles(roles, requiredRoles, rolePrefix)
// Make sure that roles have the correct composite roles.
ProductRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(product.id))
val requiredCompositeRoles = role.permissions.map { it.roleName(product.id) }
val actualCompositeRoles = keycloakClient.getCompositeRoles(roleName).map { it.name.value }
val compositeRolePrefix = ProductPermission.rolePrefix(product.id)
synchronizeKeycloakCompositeRoles(
roleName,
requiredCompositeRoles,
actualCompositeRoles,
compositeRolePrefix
)
}
// Make sure that the roles are added as composites to the related organization roles.
ProductRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(product.id))
OrganizationRole.entries.find { it.includedProductRole == role }
?.let { organizationRole ->
val organizationRoleName = RoleName(organizationRole.roleName(product.organizationId))
val compositeRoles =
keycloakClient.getCompositeRoles(organizationRoleName).map { it.name.value }
synchronizeKeycloakCompositeRoles(
organizationRoleName,
listOf(roleName.value),
compositeRoles,
rolePrefix
)
}
}
}
// Make sure that roles for non-existing products are removed.
val productIds = products.mapTo(mutableSetOf()) { it.id }
removeRoles(
roles.filter {
val roleProductId = ProductRole.extractProductIdFromRole(it)
roleProductId != null && roleProductId !in productIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak roles for product roles.", it)
}.getOrThrow()
}
/**
* Synchronize the [repository roles][RepositoryRole] with the Keycloak roles by adding missing roles, removing
* obsolete roles that start with the [repository role prefix][RepositoryRole.rolePrefix], adding the repository
* roles as composites to the related [product roles][ProductRole.includedRepositoryRole], and adding the
* [defined permission roles][RepositoryRole.permissions] as composite roles, based on the provided list of
* existing Keycloak [roles].
*/
private suspend fun synchronizeRepositoryRoles(roles: Set) {
logger.info("Synchronizing Keycloak roles for repository roles.")
runCatching {
val repositories = db.dbQuery { repositoryRepository.list() }
repositories.forEach { repository ->
// Make sure that all roles exist.
val requiredRoles = RepositoryRole.getRolesForRepository(repository.id)
val rolePrefix = RepositoryRole.rolePrefix(repository.id)
synchronizeKeycloakRoles(roles, requiredRoles, rolePrefix)
// Make sure that roles have the correct composite roles.
RepositoryRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(repository.id))
val requiredCompositeRoles = role.permissions.map { it.roleName(repository.id) }
val actualCompositeRoles = keycloakClient.getCompositeRoles(roleName).map { it.name.value }
val compositeRolePrefix = RepositoryPermission.rolePrefix(repository.id)
synchronizeKeycloakCompositeRoles(
roleName,
requiredCompositeRoles,
actualCompositeRoles,
compositeRolePrefix
)
}
// Make sure that the roles are added as composites to the related product roles.
RepositoryRole.entries.forEach { role ->
val roleName = RoleName(role.roleName(repository.id))
ProductRole.entries.find { it.includedRepositoryRole == role }
?.let { productRole ->
val productRoleName = RoleName(productRole.roleName(repository.productId))
val compositeRoles = keycloakClient.getCompositeRoles(productRoleName).map { it.name.value }
synchronizeKeycloakCompositeRoles(
productRoleName,
listOf(roleName.value),
compositeRoles,
rolePrefix
)
}
}
}
// Make sure that roles for non-existing repositories are removed.
val repositoryIds = repositories.mapTo(mutableSetOf()) { it.id }
removeRoles(
roles.filter {
val roleRepositoryId = RepositoryRole.extractRepositoryIdFromRole(it)
roleRepositoryId != null && roleRepositoryId !in repositoryIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak roles for repository roles.", it)
}.getOrThrow()
}
/**
* Synchronize the [organization roles][OrganizationRole] with the Keycloak groups by adding missing groups and
* removing obsolete groups that start with the
* [organization group prefix][OrganizationRole.groupPrefix], based on the provided list of existing Keycloak
* [groups].
*/
private suspend fun synchronizeOrganizationGroups(groups: Set) {
logger.info("Synchronizing Keycloak groups for organization roles.")
runCatching {
val organizationIds = db.dbQuery { organizationRepository.list().mapTo(mutableSetOf()) { it.id } }
organizationIds.forEach { organizationId ->
// Make sure that all groups exist.
val requiredGroups =
OrganizationRole.getGroupsForOrganization(organizationId).map { keycloakGroupPrefix + it }
val groupPrefix = keycloakGroupPrefix + OrganizationRole.groupPrefix(organizationId)
synchronizeKeycloakGroups(groups, requiredGroups, groupPrefix)
// Make sure that groups have the correct role assigned.
OrganizationRole.entries.forEach { role ->
synchronizeKeycloakGroupRole(
keycloakGroupPrefix + role.groupName(organizationId),
role.roleName(organizationId),
OrganizationRole.groupPrefix(organizationId)
)
}
}
// Make sure that groups for non-existing organizations are removed.
removeGroups(
groups.filter {
val groupId = it.removePrefixOrNull(keycloakGroupPrefix)
?.let(OrganizationRole::extractOrganizationIdFromGroup)
groupId != null && groupId !in organizationIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak groups for organization roles.", it)
}.getOrThrow()
}
/**
* Synchronize the [product roles][ProductRole] with the Keycloak groups by adding missing groups and removing
* obsolete groups that start with the
* [product group prefix][ProductRole.groupPrefix], based on the provided list of existing Keycloak [groups].
*/
private suspend fun synchronizeProductGroups(groups: Set) {
logger.info("Synchronizing Keycloak groups for product roles.")
runCatching {
val productIds = db.dbQuery { productRepository.list().mapTo(mutableSetOf()) { it.id } }
productIds.forEach { productId ->
// Make sure that all groups exist.
val requiredGroups = ProductRole.getGroupsForProduct(productId).map { keycloakGroupPrefix + it }
val groupPrefix = keycloakGroupPrefix + ProductRole.groupPrefix(productId)
synchronizeKeycloakGroups(groups, requiredGroups, groupPrefix)
// Make sure that groups have the correct role assigned.
ProductRole.entries.forEach { role ->
synchronizeKeycloakGroupRole(
keycloakGroupPrefix + role.groupName(productId),
role.roleName(productId),
ProductRole.groupPrefix(productId)
)
}
}
// Make sure that groups for non-existing products are removed.
removeGroups(
groups.filter {
val groupId = it.removePrefixOrNull(keycloakGroupPrefix)
?.let(ProductRole::extractProductIdFromGroup)
groupId != null && groupId !in productIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak groups for product roles.", it)
}.getOrThrow()
}
/**
* Synchronize the [repository roles][RepositoryRole] with the Keycloak groups by adding missing groups and
* removing obsolete groups that start with the
* [repository group prefix][RepositoryRole.groupPrefix], based on the provided list of existing Keycloak
* [groups].
*/
private suspend fun synchronizeRepositoryGroups(groups: Set) {
logger.info("Synchronizing Keycloak groups for repository roles.")
runCatching {
val repositoryIds = db.dbQuery { repositoryRepository.list().mapTo(mutableSetOf()) { it.id } }
repositoryIds.forEach { repositoryId ->
// Make sure that all groups exist.
val requiredGroups =
RepositoryRole.getGroupsForRepository(repositoryId).map { keycloakGroupPrefix + it }
val groupPrefix = keycloakGroupPrefix + RepositoryRole.groupPrefix(repositoryId)
synchronizeKeycloakGroups(groups, requiredGroups, groupPrefix)
// Make sure that groups have the correct role assigned.
RepositoryRole.entries.forEach { role ->
synchronizeKeycloakGroupRole(
keycloakGroupPrefix + role.groupName(repositoryId),
role.roleName(repositoryId),
RepositoryRole.groupPrefix(repositoryId)
)
}
}
// Make sure that groups for non-existing repositories are removed.
removeGroups(
groups.filter {
val groupId = it.removePrefixOrNull(keycloakGroupPrefix)
?.let(RepositoryRole::extractRepositoryIdFromGroup)
groupId != null && groupId !in repositoryIds
}
)
}.onFailure {
logger.error("Error while synchronizing Keycloak groups for repository roles.", it)
}.getOrThrow()
}
/**
* Create Keycloak roles for all roles in [requiredRoles] that are not contained in [roles], and delete all Keycloak
* roles which are not contained in [roles] and start with [rolePrefix].
*/
private suspend fun synchronizeKeycloakRoles(roles: Set, requiredRoles: List, rolePrefix: String) {
val missingRoles = requiredRoles.filter { it !in roles }
if (missingRoles.isNotEmpty()) {
logger.info("Creating missing roles: ${missingRoles.joinToString()}")
}
missingRoles.forEach { role ->
keycloakClient.createRole(RoleName(role), ROLE_DESCRIPTION)
}
removeRoles(roles.filter { it.startsWith(rolePrefix) }.filter { it !in requiredRoles })
}
/**
* Remove the given [unneededRoles] from Keycloak.
*/
private suspend fun removeRoles(unneededRoles: Collection) {
if (unneededRoles.isNotEmpty()) {
logger.info("Removing unneeded roles: ${unneededRoles.joinToString()}")
}
unneededRoles.forEach { role ->
keycloakClient.deleteRole(RoleName(role))
}
}
/**
* Add all roles in [requiredCompositeRoles] that are not contained in [actualCompositeRoles] as composite roles to
* the provided [role][roleName], and remove all composite roles from the provided [role][roleName] which are not
* contained in [requiredCompositeRoles] and start with [rolePrefix].
*/
private suspend fun synchronizeKeycloakCompositeRoles(
roleName: RoleName,
requiredCompositeRoles: List,
actualCompositeRoles: List,
rolePrefix: String
) {
val missingCompositeRoles = requiredCompositeRoles.filter { it !in actualCompositeRoles }
if (missingCompositeRoles.isNotEmpty()) {
logger.info("Adding missing composite roles to ${roleName.value}: ${missingCompositeRoles.joinToString()}")
}
missingCompositeRoles.forEach { role ->
keycloakClient.addCompositeRole(roleName, keycloakClient.getRole(RoleName(role)).id)
}
val unneededCompositeRoles =
actualCompositeRoles.filter { it.startsWith(rolePrefix) }.filter { it !in requiredCompositeRoles }
if (unneededCompositeRoles.isNotEmpty()) {
logger.info(
"Removing unneeded composite roles from ${roleName.value}: ${unneededCompositeRoles.joinToString()}"
)
}
unneededCompositeRoles.forEach { role ->
keycloakClient.removeCompositeRole(roleName, keycloakClient.getRole(RoleName(role)).id)
}
}
override suspend fun ensureSuperuser() {
val roleName = RoleName(Superuser.ROLE_NAME)
val groupName = GroupName(keycloakGroupPrefix + Superuser.GROUP_NAME)
runCatching {
keycloakClient.getRole(roleName)
}.onFailure {
logger.info("Creating '${roleName.value}' role.")
keycloakClient.createRole(name = roleName, description = ROLE_DESCRIPTION)
}
runCatching {
keycloakClient.getGroup(groupName)
}.onFailure {
logger.info("Creating '${groupName.value}' group.")
keycloakClient.createGroup(groupName)
}
val group = keycloakClient.getGroup(groupName)
val roles = keycloakClient.getGroupClientRoles(group.id).map { it.name }
if (roleName !in roles) {
logger.info("Adding role '${roleName.value}' to group '${groupName.value}'.")
val role = keycloakClient.getRole(roleName)
keycloakClient.addGroupClientRole(group.id, role)
}
}
/**
* Create Keycloak groups for all groups in [requiredGroups] that are not contained in [groups], and delete all
* Keycloak groups which are not contained in [groups] and start with [groupPrefix].
*/
private suspend fun synchronizeKeycloakGroups(
groups: Set,
requiredGroups: List,
groupPrefix: String
) {
val missingGroups = requiredGroups.filter { it !in groups }
if (missingGroups.isNotEmpty()) {
logger.info("Creating missing groups: ${missingGroups.joinToString()}")
}
missingGroups.forEach { group ->
keycloakClient.createGroup(GroupName(group))
}
removeGroups(groups.filter { it.startsWith(groupPrefix) }.filter { it !in requiredGroups })
}
/**
* Remove the given [unneededGroups] from Keycloak.
*/
private suspend fun removeGroups(unneededGroups: Collection) {
if (unneededGroups.isNotEmpty()) {
logger.info("Removing unneeded groups: ${unneededGroups.joinToString()}")
}
unneededGroups.forEach { group ->
keycloakClient.deleteGroup(keycloakClient.getGroup(GroupName(group)).id)
}
}
/**
* Add the provided [role][roleName] to the provided [group][groupName] if the group does not already have that
* role, and remove all roles from the provided [group][groupName] that start with [groupPrefix].
*/
private suspend fun synchronizeKeycloakGroupRole(groupName: String, roleName: String, groupPrefix: String) {
val group = keycloakClient.getGroup(GroupName(groupName))
val actualGroupRoles = keycloakClient.getGroupClientRoles(group.id)
if (roleName !in actualGroupRoles.map { it.name.value }) {
keycloakClient.addGroupClientRole(group.id, keycloakClient.getRole(RoleName(roleName)))
}
actualGroupRoles.filter { it.name.value.startsWith(groupPrefix) }.filter { it.name.value != roleName }.forEach {
keycloakClient.removeGroupClientRole(group.id, it)
}
}
override suspend fun addUserToGroup(
username: String,
groupName: String
) {
val group = keycloakGroupPrefix + groupName // Allow multiple ORT instances to share the same Keycloak realm
keycloakClient.addUserToGroup(UserName(username), GroupName(group))
}
override suspend fun removeUserFromGroup(
username: String,
groupName: String
) {
val group = keycloakGroupPrefix + groupName // Allow multiple ORT instances to share the same Keycloak realm
keycloakClient.removeUserFromGroup(UserName(username), GroupName(group))
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy