diff --git a/components/admin-config/backend/build.gradle.kts b/components/admin-config/backend/build.gradle.kts index 380e912077..c7fb951452 100644 --- a/components/admin-config/backend/build.gradle.kts +++ b/components/admin-config/backend/build.gradle.kts @@ -42,7 +42,7 @@ dependencies { implementation(libs.exposedCore) implementation(libs.exposedKotlinDatetime) - routesImplementation(projects.components.authorizationKeycloak.backend) + routesImplementation(projects.components.authorization.backend) routesImplementation(projects.shared.apiModel) routesImplementation(projects.shared.ktorUtils) diff --git a/components/admin-config/backend/src/routes/kotlin/routes/GetConfigByKey.kt b/components/admin-config/backend/src/routes/kotlin/routes/GetConfigByKey.kt index bc59353835..f4cc8661ef 100644 --- a/components/admin-config/backend/src/routes/kotlin/routes/GetConfigByKey.kt +++ b/components/admin-config/backend/src/routes/kotlin/routes/GetConfigByKey.kt @@ -28,7 +28,7 @@ import io.ktor.server.routing.Route import org.eclipse.apoapsis.ortserver.components.adminconfig.Config import org.eclipse.apoapsis.ortserver.components.adminconfig.ConfigKey import org.eclipse.apoapsis.ortserver.components.adminconfig.ConfigTable -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireAuthenticated +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal import org.eclipse.apoapsis.ortserver.shared.ktorutils.jsonBody import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireParameter import org.eclipse.apoapsis.ortserver.shared.ktorutils.respondError @@ -68,7 +68,7 @@ internal fun Route.getConfigByKey(db: Database) = get("admin/config/{key}", { } } }) { - requireAuthenticated() + requirePrincipal() val keyParameter = call.requireParameter("key") diff --git a/components/admin-config/backend/src/routes/kotlin/routes/InsertOrUpdateConfig.kt b/components/admin-config/backend/src/routes/kotlin/routes/InsertOrUpdateConfig.kt index e02ee39dea..a8065823c7 100644 --- a/components/admin-config/backend/src/routes/kotlin/routes/InsertOrUpdateConfig.kt +++ b/components/admin-config/backend/src/routes/kotlin/routes/InsertOrUpdateConfig.kt @@ -19,8 +19,6 @@ package org.eclipse.apoapsis.ortserver.components.adminconfig.routes -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond @@ -29,7 +27,8 @@ import io.ktor.server.routing.Route import org.eclipse.apoapsis.ortserver.components.adminconfig.Config import org.eclipse.apoapsis.ortserver.components.adminconfig.ConfigKey import org.eclipse.apoapsis.ortserver.components.adminconfig.ConfigTable -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.shared.ktorutils.jsonBody import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireParameter import org.eclipse.apoapsis.ortserver.shared.ktorutils.respondError @@ -69,9 +68,7 @@ internal fun Route.setConfigByKey(db: Database) = post("admin/config/{key}", { description = "The config key is invalid." } } -}) { - requireSuperuser() - +}, requireSuperuser()) { val keyParameter = call.requireParameter("key") val key = runCatching { diff --git a/components/authorization-keycloak/backend/build.gradle.kts b/components/authorization-keycloak/backend/build.gradle.kts index b0aa818b44..3adf4ff904 100644 --- a/components/authorization-keycloak/backend/build.gradle.kts +++ b/components/authorization-keycloak/backend/build.gradle.kts @@ -26,6 +26,7 @@ group = "org.eclipse.apoapsis.ortserver.components.authorization.keycloak" dependencies { api(projects.clients.keycloak) + api(projects.components.authorization.backend) api(projects.components.authorizationKeycloak.apiModel) api(projects.model) diff --git a/components/authorization-keycloak/backend/src/main/kotlin/migration/RolesToDbMigration.kt b/components/authorization-keycloak/backend/src/main/kotlin/migration/RolesToDbMigration.kt new file mode 100644 index 0000000000..f67f80a97b --- /dev/null +++ b/components/authorization-keycloak/backend/src/main/kotlin/migration/RolesToDbMigration.kt @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2025 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.components.authorization.keycloak.migration + +import org.eclipse.apoapsis.ortserver.clients.keycloak.GroupName +import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClient +import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Role +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole as DbOrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole as DbProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole as DbRepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role as DbRole +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.dao.dbQuery +import org.eclipse.apoapsis.ortserver.dao.repositories.organization.OrganizationsTable +import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable +import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.HierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId +import org.jetbrains.exposed.sql.Database +import org.slf4j.LoggerFactory + +private val logger = LoggerFactory.getLogger(RolesToDbMigration::class.java) + +/** + * A class implementing a migration that moves roles management to the database. + * + * The migration iterates over the existing organizations, products, and repositories and the associating groups in + * Keycloak that represent the user roles on these entities. It then creates corresponding role assignment entries in + * the database. The migration needs to be executed once to set up the data structures for the new authorization + * component. + */ +class RolesToDbMigration( + private val keycloakClient: KeycloakClient, + private val db: Database, + + /** + * A prefix for Keycloak group names, to be used when multiple instances of ORT Server share the same Keycloak + * realm. + */ + private val keycloakGroupPrefix: String, + + /** + * The reworked authorization service that stores authorization data in the database. This is used for the + * migration functionality. + */ + private val authorizationService: AuthorizationService + +) { + /** + * Perform a one-time migration of roles stored in Keycloak to the database. The migration happens if and only if + * the table with role assignments is empty. It is then populated with data corresponding to the current set of + * groups existing in Keycloak. The return value indicates whether a migration was performed. + */ + suspend fun migrateRolesToDb(): Boolean { + if (!canMigrate()) return false + + logger.warn( + "Starting migration of Keycloak roles to database-based roles using group prefix '{}'.", + keycloakGroupPrefix + ) + + val organizationIds = db.dbQuery { + OrganizationsTable.select(OrganizationsTable.id) + .map { it[OrganizationsTable.id].value } + } + + logger.info("Migrating {} organizations.", organizationIds.size) + organizationIds.forEach { organizationId -> + migrateOrganizationRolesToDb(organizationId) + } + + logger.info("Migrating superusers") + migrateUsersInGroupToDb( + GroupName(keycloakGroupPrefix + Superuser.GROUP_NAME), + DbOrganizationRole.ADMIN, + CompoundHierarchyId.WILDCARD + ) + + return true + } + + /** + * Migrate the access rights for the organization with the given [organizationId] to the new database-based roles. + * This includes the migration of all products and repositories belonging to the organization. + */ + private suspend fun migrateOrganizationRolesToDb(organizationId: Long) { + logger.info("Migrating roles for organization '{}'.", organizationId) + val organizationHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(organizationId)) + + migrateElementRolesToDb( + oldRoles = OrganizationRole.entries, + newRoles = DbOrganizationRole.entries, + id = OrganizationId(organizationId), + newHierarchyID = organizationHierarchyId + ) + + val productIds = db.dbQuery { + ProductsTable.select(ProductsTable.id) + .where { ProductsTable.organizationId eq organizationId } + .map { it[ProductsTable.id].value } + } + + logger.info("Migrating {} products for organization '{}'.", productIds.size, organizationId) + productIds.forEach { productId -> + val productHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(organizationId), + ProductId(productId) + ) + migrateProductRolesToDb(productHierarchyId) + } + } + + /** + * Migrate the access rights for the product with the given [productHierarchyId] to the new database-based roles. + * This includes the migration of all repositories belonging to the product. + */ + private suspend fun migrateProductRolesToDb(productHierarchyId: CompoundHierarchyId) { + val productId = requireNotNull(productHierarchyId.productId) + logger.info("Migrating roles for product '{}'.", productId) + + migrateElementRolesToDb( + oldRoles = ProductRole.entries, + newRoles = DbProductRole.entries, + id = productId, + productHierarchyId + ) + + val repositoryIds = db.dbQuery { + RepositoriesTable.select(RepositoriesTable.id) + .where { RepositoriesTable.productId eq productId.value } + .map { it[RepositoriesTable.id].value } + } + + logger.info("Migrating {} repositories for product '{}'.", repositoryIds.size, productId) + repositoryIds.forEach { repositoryId -> + val repositoryHierarchyId = CompoundHierarchyId.forRepository( + requireNotNull(productHierarchyId.organizationId), + productId, + RepositoryId(repositoryId) + ) + migrateRepositoryRolesToDb(repositoryHierarchyId) + } + } + + /** + * Migrate the access rights for the repository with the given [repositoryId] to the new database-based roles. + */ + private suspend fun migrateRepositoryRolesToDb(repositoryId: CompoundHierarchyId) { + logger.info("Migrating roles for repository '{}'.", repositoryId.repositoryId) + + migrateElementRolesToDb( + oldRoles = RepositoryRole.entries, + newRoles = DbRepositoryRole.entries, + id = requireNotNull(repositoryId.repositoryId), + repositoryId + ) + } + + /** + * Migrate all users in the given [oldRoles] to the corresponding [newRoles] for the hierarchy element with the + * given [id] and [newHierarchyID]. + */ + private suspend fun migrateElementRolesToDb( + oldRoles: Collection>, + newRoles: Collection, + id: ID, + newHierarchyID: CompoundHierarchyId + ) { + oldRoles.zip(newRoles).forEach { (oldRole, newRole) -> + migrateUsersForRoleToDb(oldRole, newRole, id, newHierarchyID) + } + } + + /** + * Migrate all users assigned to the given [oldRole] for the hierarchy element with the given [id] to the new + * [newRole] in the database, using the provided [newHierarchyID]. + */ + private suspend fun migrateUsersForRoleToDb( + oldRole: Role<*, ID>, + newRole: DbRole, + id: ID, + newHierarchyID: CompoundHierarchyId + ) { + val groupName = GroupName(keycloakGroupPrefix + oldRole.groupName(id)) + migrateUsersInGroupToDb(groupName, newRole, newHierarchyID) + } + + /** + * Migrate all users in the Keycloak group with the given [groupName] (which represents a role) to the given + * [newRole] for the hierarchy element with the given [newHierarchyID]. + */ + private suspend fun migrateUsersInGroupToDb( + groupName: GroupName, + newRole: DbRole, + newHierarchyID: CompoundHierarchyId + ) { + runCatching { + keycloakClient.getGroupMembers(groupName).forEach { user -> + authorizationService.assignRole( + userId = user.username.value, + role = newRole, + compoundHierarchyId = newHierarchyID + ) + } + }.onFailure { exception -> + logger.error("Failed to load users in group '${groupName.value}' during migration.", exception) + } + } + + /** + * Return a flag whether the migration of access rights to the new database structures is possible. This is the + * case + */ + private suspend fun canMigrate(): Boolean = db.dbQuery { + RoleAssignmentsTable.select(RoleAssignmentsTable.id).count() == 0L + } +} diff --git a/components/authorization-keycloak/backend/src/test/kotlin/migration/RolesToDbMigrationTest.kt b/components/authorization-keycloak/backend/src/test/kotlin/migration/RolesToDbMigrationTest.kt new file mode 100644 index 0000000000..b1c504a600 --- /dev/null +++ b/components/authorization-keycloak/backend/src/test/kotlin/migration/RolesToDbMigrationTest.kt @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2025 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.components.authorization.keycloak.migration + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.beEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldStartWith +import io.mockk.coEvery +import io.mockk.mockk +import org.eclipse.apoapsis.ortserver.clients.keycloak.GroupName +import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClient +import org.eclipse.apoapsis.ortserver.clients.keycloak.User +import org.eclipse.apoapsis.ortserver.clients.keycloak.UserId +import org.eclipse.apoapsis.ortserver.clients.keycloak.UserName +import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole as DbOrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole as DbProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole as DbRepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.dao.dbQuery +import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId +import org.jetbrains.exposed.sql.insert + +class RolesToDbMigrationTest : StringSpec() { + private val dbExtension = extension(DatabaseTestExtension()) + + /** + * Create a test instance of [RolesToDbMigration] with the given dependencies. + */ + private fun createMigration( + keycloakClient: KeycloakClient, + authorizationService: AuthorizationService + ): RolesToDbMigration = + RolesToDbMigration( + keycloakClient = keycloakClient, + db = dbExtension.db, + keycloakGroupPrefix = GROUP_PREFIX, + authorizationService = authorizationService + ) + + init { + "role assignments for all hierarchy elements should be migrated correctly" { + val orgAdminUser = "OrgAdmin" + val productAdminUser = "ProductAdmin" + val user1 = "test-user1" + val user2 = "test-user2" + val user3 = "test-user3" + val repositoryId = CompoundHierarchyId.forRepository( + OrganizationId(dbExtension.fixtures.organization.id), + ProductId(dbExtension.fixtures.product.id), + RepositoryId(dbExtension.fixtures.repository.id) + ) + val productId = repositoryId.parent!! + val organizationId = productId.parent!! + val repository2 = dbExtension.fixtures.createRepository(url = "https://example.com/repo2.git") + val repository2Id = CompoundHierarchyId.forRepository( + OrganizationId(dbExtension.fixtures.organization.id), + ProductId(dbExtension.fixtures.product.id), + RepositoryId(repository2.id) + ) + val organization2 = dbExtension.fixtures.createOrganization("Org2") + val organization2Id = CompoundHierarchyId.forOrganization(OrganizationId(organization2.id)) + val product2 = dbExtension.fixtures.createProduct("Product2", organizationId = organization2.id) + val product2Id = CompoundHierarchyId.forProduct( + OrganizationId(organization2.id), + ProductId(product2.id) + ) + + val groupsWithUsers = mapOf( + OrganizationRole.ADMIN.groupName(dbExtension.fixtures.organization.id) to listOf(orgAdminUser), + OrganizationRole.WRITER.groupName(dbExtension.fixtures.organization.id) to listOf( + productAdminUser, + user1 + ), + OrganizationRole.READER.groupName(dbExtension.fixtures.organization.id) to listOf(user2), + ProductRole.ADMIN.groupName(dbExtension.fixtures.product.id) to listOf(productAdminUser), + ProductRole.WRITER.groupName(dbExtension.fixtures.product.id) to listOf(user1), + ProductRole.READER.groupName(dbExtension.fixtures.product.id) to listOf(user2), + RepositoryRole.ADMIN.groupName(dbExtension.fixtures.repository.id) to emptyList(), + RepositoryRole.WRITER.groupName(dbExtension.fixtures.repository.id) to listOf( + user1, + user2 + ), + RepositoryRole.READER.groupName(dbExtension.fixtures.repository.id) to emptyList(), + RepositoryRole.ADMIN.groupName(repository2.id) to emptyList(), + RepositoryRole.WRITER.groupName(repository2.id) to emptyList(), + RepositoryRole.READER.groupName(repository2.id) to listOf(user1, user2), + OrganizationRole.ADMIN.groupName(organization2.id) to listOf(orgAdminUser), + OrganizationRole.WRITER.groupName(organization2.id) to emptyList(), + OrganizationRole.READER.groupName(organization2.id) to listOf( + user3, + productAdminUser + ), + ProductRole.ADMIN.groupName(product2.id) to emptyList(), + ProductRole.WRITER.groupName(product2.id) to listOf(user3), + ProductRole.READER.groupName(product2.id) to emptyList(), + + Superuser.GROUP_NAME to emptyList() + ) + + val expectedAssignments = listOf( + RoleAssignment( + userId = orgAdminUser, + role = DbOrganizationRole.ADMIN, + hierarchyId = organizationId + ), + RoleAssignment( + userId = productAdminUser, + role = DbOrganizationRole.WRITER, + hierarchyId = organizationId + ), + RoleAssignment( + userId = user1, + role = DbOrganizationRole.WRITER, + hierarchyId = organizationId + ), + RoleAssignment( + userId = user2, + role = DbOrganizationRole.READER, + hierarchyId = organizationId + ), + RoleAssignment( + userId = productAdminUser, + role = DbProductRole.ADMIN, + hierarchyId = productId + ), + RoleAssignment( + userId = user1, + role = DbProductRole.WRITER, + hierarchyId = productId + ), + RoleAssignment( + userId = user2, + role = DbProductRole.READER, + hierarchyId = productId + ), + RoleAssignment( + userId = user1, + role = DbRepositoryRole.WRITER, + hierarchyId = repositoryId + ), + RoleAssignment( + userId = user2, + role = DbRepositoryRole.WRITER, + hierarchyId = repositoryId + ), + RoleAssignment( + userId = user1, + role = DbRepositoryRole.READER, + hierarchyId = repository2Id + ), + RoleAssignment( + userId = user2, + role = DbRepositoryRole.READER, + hierarchyId = repository2Id + ), + RoleAssignment( + userId = orgAdminUser, + role = DbOrganizationRole.ADMIN, + hierarchyId = organization2Id + ), + RoleAssignment( + userId = productAdminUser, + role = DbOrganizationRole.READER, + hierarchyId = organization2Id + ), + RoleAssignment( + userId = user3, + role = DbOrganizationRole.READER, + hierarchyId = organization2Id + ), + RoleAssignment( + userId = user3, + role = DbProductRole.WRITER, + hierarchyId = product2Id + ) + ) + + val keycloakClient = createKeycloakClientForGroups(groupsWithUsers) + val assignments = mutableListOf() + val dbAuthorizationService = createDbAuthorizationServiceMock(assignments) + + val authorizationService = createMigration( + keycloakClient = keycloakClient, + authorizationService = dbAuthorizationService + ) + authorizationService.migrateRolesToDb() shouldBe true + + assignments shouldContainExactlyInAnyOrder expectedAssignments + } + + "exceptions when querying Keycloak group members should be ignored" { + val user = "some-user" + val groupsWithUsers = mapOf( + OrganizationRole.WRITER.groupName(dbExtension.fixtures.organization.id) to listOf(user), + ) + + val keycloakClient = createKeycloakClientForGroups(groupsWithUsers) + val assignments = mutableListOf() + val dbAuthorizationService = createDbAuthorizationServiceMock(assignments) + + val authorizationService = createMigration( + keycloakClient = keycloakClient, + authorizationService = dbAuthorizationService + ) + authorizationService.migrateRolesToDb() shouldBe true + + assignments shouldHaveSize 1 + } + + "superuser role assignments should be migrated correctly" { + val superuser1 = "SuperMan" + val superuser2 = "BatMan" + + val groupsWithUsers = mapOf( + Superuser.GROUP_NAME to listOf(superuser1, superuser2) + ) + + val expectedAssignments = listOf( + RoleAssignment( + userId = superuser1, + role = DbOrganizationRole.ADMIN, + hierarchyId = CompoundHierarchyId.WILDCARD + ), + RoleAssignment( + userId = superuser2, + role = DbOrganizationRole.ADMIN, + hierarchyId = CompoundHierarchyId.WILDCARD + ) + ) + + val keycloakClient = createKeycloakClientForGroups(groupsWithUsers) + val assignments = mutableListOf() + val dbAuthorizationService = createDbAuthorizationServiceMock(assignments) + + val authorizationService = createMigration( + keycloakClient = keycloakClient, + authorizationService = dbAuthorizationService + ) + authorizationService.migrateRolesToDb() shouldBe true + + assignments shouldContainExactlyInAnyOrder expectedAssignments + } + + "migration should be skipped if the DB already contains role assignments" { + dbExtension.fixtures.repository.id // This forces the creation of hierarchy elements. + + dbExtension.db.dbQuery { + RoleAssignmentsTable.insert { + it[userId] = "some-user-id" + it[organizationRole] = "READER" + it[organizationId] = dbExtension.fixtures.organization.id + } + } + + val keycloakClient = createKeycloakClientForGroups(emptyMap()) + val assignments = mutableListOf() + val dbAuthorizationService = createDbAuthorizationServiceMock(assignments) + + val authorizationService = createMigration( + keycloakClient = keycloakClient, + authorizationService = dbAuthorizationService + ) + authorizationService.migrateRolesToDb() shouldBe false + + assignments should beEmpty() + } + } +} + +/** The prefix for group names in Keycloak. */ +private const val GROUP_PREFIX = "namespace_" + +/** + * Create a mock [KeycloakClient] that is prepared to answer requests for the members of groups. The known groups + * and their users are provided by the given [groupsWithUsers]. + */ +private fun createKeycloakClientForGroups(groupsWithUsers: Map>): KeycloakClient = + mockk { + coEvery { getGroupMembers(any()) } answers { + val groupName = firstArg() + groupName shouldStartWith GROUP_PREFIX + groupsWithUsers[groupName.removePrefix(GROUP_PREFIX)]?.mapTo(mutableSetOf()) { userId -> + User( + id = UserId("$userId-id"), + username = UserName(userId) + ) + } ?: throw IllegalArgumentException("Unknown group: $groupName") + } + } + +/** + * A data class to store the properties of a role assignment requested on the authorization service. + */ +private data class RoleAssignment( + /** The subject user ID. */ + val userId: String, + + /** The role that is assigned. */ + val role: Role, + + /** The ID of the hierarchy element. */ + val hierarchyId: CompoundHierarchyId +) + +/** + * Create a mock authorization service that is prepared to store all assignments passed to it in the given + * [assignments] list. + */ +private fun createDbAuthorizationServiceMock(assignments: MutableList): AuthorizationService = + mockk { + coEvery { assignRole(any(), any(), any()) } answers { + val assignment = RoleAssignment( + userId = firstArg(), + role = secondArg(), + hierarchyId = thirdArg() + ) + assignments += assignment + } + } diff --git a/components/authorization/backend/build.gradle.kts b/components/authorization/backend/build.gradle.kts index 7d8e22214d..358539fb9e 100644 --- a/components/authorization/backend/build.gradle.kts +++ b/components/authorization/backend/build.gradle.kts @@ -40,7 +40,10 @@ dependencies { implementation(libs.exposedCore) implementation(libs.exposedJdbc) + routesImplementation(libs.ktorOpenApi) + testImplementation(testFixtures(projects.dao)) + testImplementation(testFixtures(projects.shared.ktorUtils)) testImplementation(ktorLibs.client.contentNegotiation) testImplementation(ktorLibs.client.core) diff --git a/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt b/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt index 3e7f2d7b6c..8a1868d722 100644 --- a/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt +++ b/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt @@ -21,6 +21,7 @@ package org.eclipse.apoapsis.ortserver.components.authorization.routes +import com.auth0.jwk.JwkProviderBuilder import com.auth0.jwt.interfaces.Payload import io.github.smiley4.ktoropenapi.config.RouteConfig @@ -30,8 +31,13 @@ import io.github.smiley4.ktoropenapi.patch import io.github.smiley4.ktoropenapi.post import io.github.smiley4.ktoropenapi.put +import io.ktor.server.application.Application import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.jwt.jwt import io.ktor.server.auth.principal +import io.ktor.server.config.ApplicationConfig import io.ktor.server.routing.Route import io.ktor.server.routing.RouteSelector import io.ktor.server.routing.RouteSelectorEvaluation @@ -40,9 +46,44 @@ import io.ktor.server.routing.RoutingPipelineCall import io.ktor.server.routing.RoutingResolveContext import io.ktor.util.AttributeKey +import java.net.URI +import java.util.concurrent.TimeUnit + import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +/** + * Configure the authentication for this server application. + * + * This function sets up the Ktor plugins for authentication using JWT tokens. It configures the creation of an + * [OrtServerPrincipal] instance for authorized requests. + */ +fun Application.configureAuthentication(config: ApplicationConfig, authorizationService: AuthorizationService) { + val issuer = config.property("jwt.issuer").getString() + val jwksUri = URI.create(config.property("jwt.jwksUri").getString()).toURL() + val configuredRealm = config.property("jwt.realm").getString() + val requiredAudience = config.property("jwt.audience").getString() + val jwkProvider = JwkProviderBuilder(jwksUri) + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build() + + install(Authentication) { + jwt(AuthenticationProviders.TOKEN_PROVIDER) { + realm = configuredRealm + verifier(jwkProvider, issuer) { + acceptLeeway(10) + } + + validate { credential -> + credential.payload.takeIf { it.audience.contains(requiredAudience) }?.let { + createAuthorizedPrincipal(authorizationService, credential.payload) + } + } + } + } +} + /** * Create an [OrtServerPrincipal] for this [ApplicationCall]. If an [AuthorizationChecker] is present in the current * context, use it to an [EffectiveRole] and perform an authorization check. Result is *null* if this check fails. diff --git a/components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt b/components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt index c3a498b887..eacd9d25fa 100644 --- a/components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt +++ b/components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt @@ -21,6 +21,7 @@ package org.eclipse.apoapsis.ortserver.components.authorization.routes import com.auth0.jwt.interfaces.Payload +import io.ktor.server.application.ApplicationCall import io.ktor.server.auth.principal import io.ktor.server.routing.RoutingContext @@ -110,3 +111,10 @@ class OrtServerPrincipal( val effectiveRole: EffectiveRole get() = role ?: throw AuthorizationException() } + +/** + * A convenience extension property to obtain the [OrtServerPrincipal] from an [ApplicationCall] that has already been + * authenticated. + */ +val ApplicationCall.ortServerPrincipal: OrtServerPrincipal + get() = requireNotNull(principal()) diff --git a/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt b/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt index 31202f0692..d2f12a3a5a 100644 --- a/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt +++ b/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt @@ -217,7 +217,7 @@ class DbAuthorizationService( return HierarchyFilter( transitiveIncludes = includes, nonTransitiveIncludes = permissions.implicitIncludes().filterContainedIn(containedInId), - isWildcard = permissions.isSuperuser() + isWildcard = permissions.isSuperuser() && containedInId == null ) } diff --git a/components/authorization/backend/src/routes/kotlin/routes/Routing.kt b/components/authorization/backend/src/routes/kotlin/routes/Routing.kt new file mode 100644 index 0000000000..48fbe9fb6a --- /dev/null +++ b/components/authorization/backend/src/routes/kotlin/routes/Routing.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 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.components.authorization.routes + +import io.ktor.server.routing.Route + +import org.eclipse.apoapsis.ortserver.components.authorization.routes.userinfo.getSuperuser + +fun Route.authorizationRoutes() { + getSuperuser() +} diff --git a/components/authorization/backend/src/routes/kotlin/routes/userinfo/GetSuperuser.kt b/components/authorization/backend/src/routes/kotlin/routes/userinfo/GetSuperuser.kt new file mode 100644 index 0000000000..f6d41c1edf --- /dev/null +++ b/components/authorization/backend/src/routes/kotlin/routes/userinfo/GetSuperuser.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 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.components.authorization.routes.userinfo + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respond +import io.ktor.server.routing.Route + +import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.AuthorizationChecker +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId + +internal fun Route.getSuperuser() = get("/authorization/superuser", { + operationId = "getSuperuser" + summary = "Check if the current user is a superuser" + tags = listOf("Authorization") + + response { + HttpStatusCode.OK to { + description = "Success" + body { + description = "Whether the current user is a superuser." + } + } + } +}, userInfoChecker()) { + val isSuperuser = requirePrincipal().effectiveRole.isSuperuser + call.respond(HttpStatusCode.OK, isSuperuser.toString()) +} + +private fun userInfoChecker(): AuthorizationChecker = + object : AuthorizationChecker { + override suspend fun loadEffectiveRole( + service: AuthorizationService, + userId: String, + call: ApplicationCall + ): EffectiveRole { + return service.getEffectiveRole(userId, CompoundHierarchyId.WILDCARD) + } + } diff --git a/components/authorization/backend/src/test/kotlin/routes/userinfo/GetSuperuserTest.kt b/components/authorization/backend/src/test/kotlin/routes/userinfo/GetSuperuserTest.kt new file mode 100644 index 0000000000..4f9124aa8d --- /dev/null +++ b/components/authorization/backend/src/test/kotlin/routes/userinfo/GetSuperuserTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 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.components.authorization.routes.userinfo + +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.kotest.matchers.shouldBe + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode + +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.authorizationRoutes +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.shared.ktorutils.AbstractAuthorizationTest + +class GetSuperuserTest : AbstractAuthorizationTest({ + lateinit var authorizationService: AuthorizationService + + beforeEach { + authorizationService = DbAuthorizationService(dbExtension.db) + } + + "GET /authorization/superuser" should { + "return 'true' for a superuser" { + authorizationTestApplication(routes = { authorizationRoutes() }) { _, client -> + authorizationService.assignRole( + TEST_USER, + OrganizationRole.ADMIN, + CompoundHierarchyId.WILDCARD + ) + + val response = client.get("/authorization/superuser") + response shouldHaveStatus HttpStatusCode.OK + response.bodyAsText() shouldBe "true" + } + } + + "return 'false' for a normal user" { + authorizationTestApplication(routes = { authorizationRoutes() }) { _, client -> + val orgId = CompoundHierarchyId.forOrganization(OrganizationId(dbExtension.fixtures.organization.id)) + authorizationService.assignRole( + TEST_USER, + OrganizationRole.ADMIN, + orgId + ) + + val response = client.get("/authorization/superuser") + response shouldHaveStatus HttpStatusCode.OK + response.bodyAsText() shouldBe "false" + } + } + + "require an authenticated user" { + requestShouldRequireAuthentication({ authorizationRoutes() }) { + get("/authorization/superuser") + } + } + } +}) + +private const val TEST_USER = "test-user" diff --git a/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt b/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt index 90b6ae7824..8e3d06ab56 100644 --- a/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt +++ b/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt @@ -1120,6 +1120,7 @@ class DbAuthorizationServiceTest : WordSpec() { containedIn = repositoryCompoundId.productId ) + filter.isWildcard shouldBe false filter.transitiveIncludes.entries.shouldBeSingleton { (key, value) -> key shouldBe CompoundHierarchyId.PRODUCT_LEVEL value shouldBe setOf(repositoryCompoundId.parent) diff --git a/components/infrastructure-services/backend/build.gradle.kts b/components/infrastructure-services/backend/build.gradle.kts index 4710ef5c6a..5ababee670 100644 --- a/components/infrastructure-services/backend/build.gradle.kts +++ b/components/infrastructure-services/backend/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(libs.exposedCore) - routesImplementation(projects.components.authorizationKeycloak.backend) + routesImplementation(projects.components.authorization.backend) routesImplementation(projects.shared.apiMappings) routesImplementation(projects.shared.ktorUtils) diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/DeleteOrganizationInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/DeleteOrganizationInfrastructureService.kt index 91fc1a8933..7a394305c1 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/DeleteOrganizationInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/DeleteOrganizationInfrastructureService.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.organization -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter @@ -53,9 +52,7 @@ internal fun Route.deleteOrganizationInfrastructureService( description = "Success" } } -}) { - requirePermission(OrganizationPermission.WRITE) - +}, requirePermission(OrganizationPermission.WRITE)) { val orgId = call.requireIdParameter("organizationId") val serviceName = call.requireParameter("serviceName") diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureService.kt index 8c8d540fbe..460b156704 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureService.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.organization -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToApi @@ -67,9 +66,7 @@ internal fun Route.getOrganizationInfrastructureService( } } } -}) { - requirePermission(OrganizationPermission.READ) - +}, requirePermission(OrganizationPermission.READ)) { val organizationId = call.requireIdParameter("organizationId") val serviceName = call.requireParameter("serviceName") diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureServices.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureServices.kt index 46da203924..894e23e662 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureServices.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/GetOrganizationInfrastructureServices.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.organization -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.InfrastructureService as ModelInfrastructureService import org.eclipse.apoapsis.ortserver.model.OrganizationId @@ -89,9 +88,7 @@ internal fun Route.getOrganizationInfrastructureServices( } } } -}) { - requirePermission(OrganizationPermission.READ) - +}, requirePermission(OrganizationPermission.READ)) { val orgId = call.requireIdParameter("organizationId") val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PatchOrganizationInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PatchOrganizationInfrastructureService.kt index c03de6b96f..110379098a 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PatchOrganizationInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PatchOrganizationInfrastructureService.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.organization -import io.github.smiley4.ktoropenapi.patch - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PatchInfrastructureService import org.eclipse.apoapsis.ortserver.model.OrganizationId @@ -85,9 +84,7 @@ internal fun Route.patchOrganizationInfrastructureService( } } } -}) { - requirePermission(OrganizationPermission.WRITE) - +}, requirePermission(OrganizationPermission.WRITE)) { val organizationId = call.requireIdParameter("organizationId") val serviceName = call.requireParameter("serviceName") val updateService = call.receive() diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PostOrganizationInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PostOrganizationInfrastructureService.kt index 701dabaad1..d1372bf84d 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PostOrganizationInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/organization/PostOrganizationInfrastructureService.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.organization -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PostInfrastructureService import org.eclipse.apoapsis.ortserver.model.OrganizationId @@ -77,9 +76,7 @@ internal fun Route.postOrganizationInfrastructureService( } } } -}) { - requirePermission(OrganizationPermission.WRITE) - +}, requirePermission(OrganizationPermission.WRITE)) { val organizationId = call.requireIdParameter("organizationId") val createService = call.receive() diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/DeleteProductInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/DeleteProductInfrastructureService.kt index b09a5610b6..bed9189575 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/DeleteProductInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/DeleteProductInfrastructureService.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.product -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter @@ -53,9 +52,7 @@ internal fun Route.deleteProductInfrastructureService( description = "Success" } } -}) { - requirePermission(ProductPermission.WRITE) - +}, requirePermission(ProductPermission.WRITE)) { val prodId = call.requireIdParameter("productId") val serviceName = call.requireParameter("serviceName") diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureService.kt index 8dff010f47..fc19f2b031 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureService.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.product -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToApi @@ -67,9 +66,7 @@ internal fun Route.getProductInfrastructureService( } } } -}) { - requirePermission(ProductPermission.READ) - +}, requirePermission(ProductPermission.READ)) { val productId = call.requireIdParameter("productId") val serviceName = call.requireParameter("serviceName") diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureServices.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureServices.kt index ebd1532fa7..93e63f972c 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureServices.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/GetProductInfrastructureServices.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.product -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.InfrastructureService as ModelInfrastructureService import org.eclipse.apoapsis.ortserver.model.ProductId @@ -89,9 +88,7 @@ internal fun Route.getProductInfrastructureServices( } } } -}) { - requirePermission(ProductPermission.READ) - +}, requirePermission(ProductPermission.READ)) { val prodId = call.requireIdParameter("productId") val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PatchProductInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PatchProductInfrastructureService.kt index 18e756d157..7a3f0b8ef9 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PatchProductInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PatchProductInfrastructureService.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.product -import io.github.smiley4.ktoropenapi.patch - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PatchInfrastructureService import org.eclipse.apoapsis.ortserver.model.ProductId @@ -85,9 +84,7 @@ internal fun Route.patchProductInfrastructureService( } } } -}) { - requirePermission(ProductPermission.WRITE) - +}, requirePermission(ProductPermission.WRITE)) { val productId = call.requireIdParameter("productId") val serviceName = call.requireParameter("serviceName") val updateService = call.receive() diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PostProductInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PostProductInfrastructureService.kt index 32ecc7c01b..e23d6278ca 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PostProductInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/product/PostProductInfrastructureService.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.product -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PostInfrastructureService import org.eclipse.apoapsis.ortserver.model.ProductId @@ -77,9 +76,7 @@ internal fun Route.postProductInfrastructureService( } } } -}) { - requirePermission(ProductPermission.WRITE) - +}, requirePermission(ProductPermission.WRITE)) { val productId = call.requireIdParameter("productId") val createService = call.receive() diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/DeleteRepositoryInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/DeleteRepositoryInfrastructureService.kt index eeb927ec73..44b82b10d9 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/DeleteRepositoryInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/DeleteRepositoryInfrastructureService.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.repository -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter @@ -53,9 +52,7 @@ internal fun Route.deleteRepositoryInfrastructureService( description = "Success" } } -}) { - requirePermission(RepositoryPermission.WRITE) - +}, requirePermission(RepositoryPermission.WRITE)) { val repositoryId = call.requireIdParameter("repositoryId") val serviceName = call.requireParameter("serviceName") diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureService.kt index bf24af669e..eb5180c200 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureService.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.repository -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToApi @@ -67,9 +66,7 @@ internal fun Route.getRepositoryInfrastructureService( } } } -}) { - requirePermission(RepositoryPermission.READ) - +}, requirePermission(RepositoryPermission.READ)) { val repositoryId = call.requireIdParameter("repositoryId") val serviceName = call.requireParameter("serviceName") diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureServices.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureServices.kt index 56a8dac586..27a4db8952 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureServices.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/GetRepositoryInfrastructureServices.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.repository -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.model.InfrastructureService as ModelInfrastructureService import org.eclipse.apoapsis.ortserver.model.RepositoryId @@ -89,9 +88,7 @@ internal fun Route.getRepositoryInfrastructureServices( } } } -}) { - requirePermission(RepositoryPermission.READ) - +}, requirePermission(RepositoryPermission.READ)) { val repositoryId = call.requireIdParameter("repositoryId") val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PatchRepositoryInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PatchRepositoryInfrastructureService.kt index 950edebd1e..9c21521005 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PatchRepositoryInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PatchRepositoryInfrastructureService.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.repository -import io.github.smiley4.ktoropenapi.patch - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PatchInfrastructureService import org.eclipse.apoapsis.ortserver.model.RepositoryId @@ -85,9 +84,7 @@ internal fun Route.patchRepositoryInfrastructureService( } } } -}) { - requirePermission(RepositoryPermission.WRITE) - +}, requirePermission(RepositoryPermission.WRITE)) { val repositoryId = call.requireIdParameter("repositoryId") val serviceName = call.requireParameter("serviceName") val updateService = call.receive() diff --git a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PostRepositoryInfrastructureService.kt b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PostRepositoryInfrastructureService.kt index c03c3a05c3..d707b09c7f 100644 --- a/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PostRepositoryInfrastructureService.kt +++ b/components/infrastructure-services/backend/src/routes/kotlin/routes/repository/PostRepositoryInfrastructureService.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.infrastructureservices.routes.repository -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PostInfrastructureService import org.eclipse.apoapsis.ortserver.model.RepositoryId @@ -77,9 +76,7 @@ internal fun Route.postRepositoryInfrastructureService( } } } -}) { - requirePermission(RepositoryPermission.WRITE) - +}, requirePermission(RepositoryPermission.WRITE)) { val repositoryId = call.requireIdParameter("repositoryId") val createService = call.receive() diff --git a/components/infrastructure-services/backend/src/test/kotlin/routes/InfrastructureServicesAuthorizationTest.kt b/components/infrastructure-services/backend/src/test/kotlin/routes/InfrastructureServicesAuthorizationTest.kt index b31d1db57a..40e7dfc902 100644 --- a/components/infrastructure-services/backend/src/test/kotlin/routes/InfrastructureServicesAuthorizationTest.kt +++ b/components/infrastructure-services/backend/src/test/kotlin/routes/InfrastructureServicesAuthorizationTest.kt @@ -26,13 +26,18 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.HttpStatusCode -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PatchInfrastructureService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.PostInfrastructureService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.infrastructureServicesRoutes import org.eclipse.apoapsis.ortserver.components.secrets.SecretService +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.shared.apimodel.asPresent @@ -40,14 +45,27 @@ import org.eclipse.apoapsis.ortserver.shared.ktorutils.AbstractAuthorizationTest class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ var orgId = 0L + var prodId = 0L var repoId = 0L + lateinit var orgHierarchyId: CompoundHierarchyId + lateinit var prodHierarchyId: CompoundHierarchyId + lateinit var repoHierarchyId: CompoundHierarchyId lateinit var infrastructureServiceService: InfrastructureServiceService beforeEach { orgId = dbExtension.fixtures.organization.id + prodId = dbExtension.fixtures.product.id repoId = dbExtension.fixtures.repository.id - - authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions() + orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(orgId)) + prodHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(orgId), + ProductId(prodId) + ) + repoHierarchyId = CompoundHierarchyId.forRepository( + OrganizationId(orgId), + ProductId(prodId), + RepositoryId(repoId) + ) infrastructureServiceService = InfrastructureServiceService( dbExtension.db, @@ -63,8 +81,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.WRITE" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = OrganizationPermission.WRITE.roleName(orgId), - successStatus = HttpStatusCode.NotFound + role = OrganizationRole.WRITER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = orgHierarchyId ) { delete("/organizations/$orgId/infrastructure-services/name") } @@ -75,8 +94,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.READ" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = OrganizationPermission.READ.roleName(orgId), - successStatus = HttpStatusCode.NotFound + role = OrganizationRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = orgHierarchyId ) { get("/organizations/$orgId/infrastructure-services/not-found") } @@ -87,7 +107,8 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.READ" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = OrganizationPermission.READ.roleName(orgId), + role = OrganizationRole.READER, + hierarchyId = orgHierarchyId ) { get("/organizations/$orgId/infrastructure-services") } @@ -98,8 +119,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.WRITE" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = OrganizationPermission.WRITE.roleName(orgId), - successStatus = HttpStatusCode.NotFound + role = OrganizationRole.WRITER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = orgHierarchyId ) { patch("/organizations/$orgId/infrastructure-services/name") { setBody( @@ -117,8 +139,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.WRITE" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = OrganizationPermission.WRITE.roleName(orgId), - successStatus = HttpStatusCode.InternalServerError + role = OrganizationRole.WRITER, + successStatus = HttpStatusCode.InternalServerError, + hierarchyId = orgHierarchyId ) { post("/organizations/$orgId/infrastructure-services") { setBody( @@ -135,12 +158,94 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ } } + "DeleteProductInfrastructureService" should { + "require ProductPermission.WRITE" { + requestShouldRequireRole( + routes = { infrastructureServicesRoutes(infrastructureServiceService) }, + role = ProductRole.WRITER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = prodHierarchyId + ) { + delete("/products/$prodId/infrastructure-services/name") + } + } + } + + "GetProductInfrastructureService" should { + "require ProductPermission.READ" { + requestShouldRequireRole( + routes = { infrastructureServicesRoutes(infrastructureServiceService) }, + role = ProductRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = prodHierarchyId + ) { + get("/products/$prodId/infrastructure-services/not-found") + } + } + } + + "GetProductInfrastructureServices" should { + "require ProductPermission.READ" { + requestShouldRequireRole( + routes = { infrastructureServicesRoutes(infrastructureServiceService) }, + role = ProductRole.READER, + hierarchyId = prodHierarchyId + ) { + get("/products/$prodId/infrastructure-services") + } + } + } + + "PatchProductInfrastructureService" should { + "require ProductPermission.WRITE" { + requestShouldRequireRole( + routes = { infrastructureServicesRoutes(infrastructureServiceService) }, + role = ProductRole.WRITER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = prodHierarchyId + ) { + patch("/products/$prodId/infrastructure-services/name") { + setBody( + PatchInfrastructureService( + description = null.asPresent(), + url = "https://repo2.example.org/test2".asPresent() + ) + ) + } + } + } + } + + "PostProductInfrastructureService" should { + "require ProductPermission.WRITE" { + requestShouldRequireRole( + routes = { infrastructureServicesRoutes(infrastructureServiceService) }, + role = ProductRole.WRITER, + successStatus = HttpStatusCode.InternalServerError, + hierarchyId = prodHierarchyId + ) { + post("/products/$prodId/infrastructure-services") { + setBody( + PostInfrastructureService( + "testRepository", + "https://repo.example.org/test", + "test description", + "userSecret", + "passSecret" + ) + ) + } + } + } + } + "DeleteRepositoryInfrastructureService" should { "require RepositoryPermission.WRITE" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = RepositoryPermission.WRITE.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.WRITER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { delete("/repositories/$repoId/infrastructure-services/name") } @@ -151,8 +256,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.READ" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = RepositoryPermission.READ.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { get("/repositories/$repoId/infrastructure-services/not-found") } @@ -163,7 +269,8 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.READ" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = RepositoryPermission.READ.roleName(repoId) + role = RepositoryRole.READER, + hierarchyId = repoHierarchyId ) { get("/repositories/$repoId/infrastructure-services") } @@ -174,8 +281,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.WRITE" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = RepositoryPermission.WRITE.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.WRITER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { patch("/repositories/$repoId/infrastructure-services/name") { setBody( @@ -193,8 +301,9 @@ class InfrastructureServicesAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.WRITE" { requestShouldRequireRole( routes = { infrastructureServicesRoutes(infrastructureServiceService) }, - role = RepositoryPermission.WRITE.roleName(repoId), - successStatus = HttpStatusCode.InternalServerError + role = RepositoryRole.WRITER, + successStatus = HttpStatusCode.InternalServerError, + hierarchyId = repoHierarchyId ) { post("/repositories/$repoId/infrastructure-services") { setBody( diff --git a/components/plugin-manager/backend/build.gradle.kts b/components/plugin-manager/backend/build.gradle.kts index 96db2ca97a..021b7b563b 100644 --- a/components/plugin-manager/backend/build.gradle.kts +++ b/components/plugin-manager/backend/build.gradle.kts @@ -56,7 +56,7 @@ dependencies { implementation(ortLibs.reporter) implementation(ortLibs.scanner) - routesImplementation(projects.components.authorizationKeycloak.backend) + routesImplementation(projects.components.authorization.backend) routesImplementation(projects.shared.ktorUtils) routesImplementation(ktorLibs.server.auth) diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/AddTemplateToOrganization.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/AddTemplateToOrganization.kt index b818f9dc13..183d5ac0bf 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/AddTemplateToOrganization.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/AddTemplateToOrganization.kt @@ -22,16 +22,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.components.pluginmanager.TemplateError @@ -82,10 +79,8 @@ internal fun Route.addTemplateToOrganization( description = "The template is already assigned to the organization." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/CreateTemplate.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/CreateTemplate.kt index 39c9ee910c..164f90d6bd 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/CreateTemplate.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/CreateTemplate.kt @@ -22,17 +22,14 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionTemplate import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionType import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService @@ -87,10 +84,8 @@ internal fun Route.createTemplate( description = "The specified plugin is not installed or the template could not be created." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/DeleteTemplate.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/DeleteTemplate.kt index cf8883ee68..eedf6685b5 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/DeleteTemplate.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/DeleteTemplate.kt @@ -22,16 +22,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.components.pluginmanager.TemplateError @@ -75,10 +72,8 @@ internal fun Route.deleteTemplate( description = "The specified plugin or template was not found." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/DisableGlobalTemplate.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/DisableGlobalTemplate.kt index 7711566301..5b2d30c429 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/DisableGlobalTemplate.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/DisableGlobalTemplate.kt @@ -22,16 +22,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.components.pluginmanager.TemplateError @@ -76,10 +73,8 @@ internal fun Route.disableGlobalTemplate( description = "The specified plugin template does not exist." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/DisablePlugin.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/DisablePlugin.kt index bcd96c6dd4..b882f9c8f5 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/DisablePlugin.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/DisablePlugin.kt @@ -19,16 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginDisabled import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginEvent import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginEventStore @@ -67,9 +64,7 @@ internal fun Route.disablePlugin(eventStore: PluginEventStore) = post("admin/plu description = "The plugin is already disabled." } } -}) { - requireSuperuser() - +}, requireSuperuser()) { val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = normalizePluginId(pluginType, call.requireParameter("pluginId")) @@ -78,7 +73,7 @@ internal fun Route.disablePlugin(eventStore: PluginEventStore) = post("admin/plu return@post } - val userId = checkNotNull(call.principal()).getUserId() + val userId = requirePrincipal().userId val plugin = eventStore.getPlugin(pluginType, pluginId) diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/EnableGlobalTemplate.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/EnableGlobalTemplate.kt index 1e835df5d9..611da11052 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/EnableGlobalTemplate.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/EnableGlobalTemplate.kt @@ -22,16 +22,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.components.pluginmanager.TemplateError @@ -76,10 +73,8 @@ internal fun Route.enableGlobalTemplate( description = "The specified plugin template does not exist." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/EnablePlugin.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/EnablePlugin.kt index 29c56224e2..23ed24c91a 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/EnablePlugin.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/EnablePlugin.kt @@ -19,16 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginEnabled import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginEvent import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginEventStore @@ -67,9 +64,7 @@ internal fun Route.enablePlugin(eventStore: PluginEventStore) = post("admin/plug description = "The plugin is already enabled." } } -}) { - requireSuperuser() - +}, requireSuperuser()) { val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = normalizePluginId(pluginType, call.requireParameter("pluginId")) @@ -78,7 +73,7 @@ internal fun Route.enablePlugin(eventStore: PluginEventStore) = post("admin/plug return@post } - val userId = checkNotNull(call.principal()).getUserId() + val userId = requirePrincipal().userId val plugin = eventStore.getPlugin(pluginType, pluginId) diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/GetInstalledPlugins.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/GetInstalledPlugins.kt index e6d1139cb2..af7fcc1b59 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/GetInstalledPlugins.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/GetInstalledPlugins.kt @@ -19,13 +19,12 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginDescriptor import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOption import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionType @@ -84,8 +83,6 @@ internal fun Route.getInstalledPlugins(pluginService: PluginService) = get("admi } } } -}) { - requireSuperuser() - +}, requireSuperuser()) { call.respond(HttpStatusCode.OK, pluginService.getPlugins()) } diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/GetPluginsForRepository.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/GetPluginsForRepository.kt index b22435b48c..8173082279 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/GetPluginsForRepository.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/GetPluginsForRepository.kt @@ -22,14 +22,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionType import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType @@ -96,9 +95,7 @@ internal fun Route.getPluginsForRepository( } } } -}) { - requirePermission(RepositoryPermission.READ) - +}, requirePermission(RepositoryPermission.READ)) { val repositoryId = call.requireIdParameter("repositoryId") pluginTemplateService.getPluginsForRepository(repositoryId).onSuccess { diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplate.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplate.kt index d6fecdd644..fa3adc9c5c 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplate.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplate.kt @@ -22,13 +22,12 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionTemplate import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionType import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplate @@ -92,9 +91,7 @@ internal fun Route.getTemplate( description = "The specified plugin template does not exist." } } -}) { - requireSuperuser() - +}, requireSuperuser()) { val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplates.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplates.kt index 62335eadd4..519cf279cf 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplates.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/GetTemplates.kt @@ -22,13 +22,12 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionTemplate import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionType import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplate @@ -89,9 +88,7 @@ internal fun Route.getTemplates( description = "The specified plugin does not exist." } } -}) { - requireSuperuser() - +}, requireSuperuser()) { val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/RemoveTemplateFromOrganization.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/RemoveTemplateFromOrganization.kt index 90a14ef915..591d01da6a 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/RemoveTemplateFromOrganization.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/RemoveTemplateFromOrganization.kt @@ -22,16 +22,13 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.components.pluginmanager.TemplateError @@ -82,10 +79,8 @@ internal fun Route.removeTemplateFromOrganization( description = "The template is not assigned to the organization." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/plugin-manager/backend/src/routes/kotlin/routes/UpdateTemplateOptions.kt b/components/plugin-manager/backend/src/routes/kotlin/routes/UpdateTemplateOptions.kt index 1e8c4ef649..64de8dd5cd 100644 --- a/components/plugin-manager/backend/src/routes/kotlin/routes/UpdateTemplateOptions.kt +++ b/components/plugin-manager/backend/src/routes/kotlin/routes/UpdateTemplateOptions.kt @@ -22,17 +22,14 @@ package org.eclipse.apoapsis.ortserver.components.pluginmanager.routes import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess -import io.github.smiley4.ktoropenapi.put - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.put +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionTemplate import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionType import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService @@ -87,10 +84,8 @@ internal fun Route.updateTemplateOptions( description = "The specified plugin is not installed or the template could not be updated." } } -}) { - requireSuperuser() - - val userId = checkNotNull(call.principal()).getUserId() +}, requireSuperuser()) { + val userId = requirePrincipal().userId val pluginType = enumValueOf(call.requireParameter("pluginType")) val pluginId = call.requireParameter("pluginId") val templateName = call.requireParameter("templateName") diff --git a/components/secrets/backend/build.gradle.kts b/components/secrets/backend/build.gradle.kts index 1a154e6b5d..7f2a29c719 100644 --- a/components/secrets/backend/build.gradle.kts +++ b/components/secrets/backend/build.gradle.kts @@ -37,7 +37,7 @@ dependencies { routesApi(ktorLibs.server.core) routesApi(ktorLibs.server.requestValidation) - routesImplementation(projects.components.authorizationKeycloak.backend) + routesImplementation(projects.components.authorization.backend) routesImplementation(projects.model) routesImplementation(projects.services.hierarchyService) routesImplementation(projects.shared.apiMappings) diff --git a/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecret.kt b/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecret.kt index dad3aeca6d..4955879964 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecret.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.organization -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -61,9 +60,7 @@ internal fun Route.getOrganizationSecret(secretService: SecretService) = } } } - }) { - requirePermission(OrganizationPermission.READ) - + }, requirePermission(OrganizationPermission.READ)) { val organizationId = OrganizationId(call.requireIdParameter("organizationId")) val secretName = call.requireParameter("secretName") diff --git a/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecrets.kt b/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecrets.kt index 48ca8b4812..ce8d2b817d 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecrets.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/organization/GetOrganizationSecrets.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.organization -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -79,9 +78,7 @@ internal fun Route.getOrganizationSecrets(secretService: SecretService) = } } } - }) { - requirePermission(OrganizationPermission.READ) - + }, requirePermission(OrganizationPermission.READ)) { val orgId = call.requireIdParameter("organizationId") val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) diff --git a/components/secrets/backend/src/routes/kotlin/routes/organization/PatchOrganizationSecret.kt b/components/secrets/backend/src/routes/kotlin/routes/organization/PatchOrganizationSecret.kt index 2ff40a7f74..6ce6051647 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/organization/PatchOrganizationSecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/organization/PatchOrganizationSecret.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.organization -import io.github.smiley4.ktoropenapi.patch - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.PatchSecret import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService @@ -76,9 +75,7 @@ internal fun Route.patchOrganizationSecret(secretService: SecretService) = } } } - }) { - requirePermission(OrganizationPermission.WRITE_SECRETS) - + }, requirePermission(OrganizationPermission.WRITE_SECRETS)) { val organizationId = OrganizationId(call.requireIdParameter("organizationId")) val secretName = call.requireParameter("secretName") val updateSecret = call.receive() diff --git a/components/secrets/backend/src/routes/kotlin/routes/organization/PostOrganizationSecret.kt b/components/secrets/backend/src/routes/kotlin/routes/organization/PostOrganizationSecret.kt index 58a9aaee52..8bf471e4ec 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/organization/PostOrganizationSecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/organization/PostOrganizationSecret.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.organization -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.PostSecret import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService @@ -69,9 +68,7 @@ internal fun Route.postOrganizationSecret(secretService: SecretService) = } } } - }) { - requirePermission(OrganizationPermission.WRITE_SECRETS) - + }, requirePermission(OrganizationPermission.WRITE_SECRETS)) { val organizationId = call.requireIdParameter("organizationId") val createSecret = call.receive() diff --git a/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecret.kt b/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecret.kt index 724cc70521..410b29ccf6 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecret.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.product -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -60,9 +59,7 @@ internal fun Route.getProductSecret(secretService: SecretService) = } } } - }) { - requirePermission(ProductPermission.READ) - + }, requirePermission(ProductPermission.READ)) { val productId = ProductId(call.requireIdParameter("productId")) val secretName = call.requireParameter("secretName") diff --git a/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecrets.kt b/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecrets.kt index 49426ebe8e..f3d60b27e3 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecrets.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/product/GetProductSecrets.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.product -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -77,9 +76,7 @@ internal fun Route.getProductSecrets(secretService: SecretService) = } } } - }) { - requirePermission(ProductPermission.READ) - + }, requirePermission(ProductPermission.READ)) { val productId = ProductId(call.requireIdParameter("productId")) val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) diff --git a/components/secrets/backend/src/routes/kotlin/routes/product/PatchProductSecret.kt b/components/secrets/backend/src/routes/kotlin/routes/product/PatchProductSecret.kt index 4fa8e65a42..7c4d10afeb 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/product/PatchProductSecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/product/PatchProductSecret.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.product -import io.github.smiley4.ktoropenapi.patch - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.PatchSecret import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService @@ -73,9 +72,7 @@ internal fun Route.patchProductSecret(secretService: SecretService) = } } } - }) { - requirePermission(ProductPermission.WRITE_SECRETS) - + }, requirePermission(ProductPermission.WRITE_SECRETS)) { val productId = ProductId(call.requireIdParameter("productId")) val secretName = call.requireParameter("secretName") val updateSecret = call.receive() diff --git a/components/secrets/backend/src/routes/kotlin/routes/product/PostProductSecret.kt b/components/secrets/backend/src/routes/kotlin/routes/product/PostProductSecret.kt index edcaf3a85f..3316924511 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/product/PostProductSecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/product/PostProductSecret.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.product -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.PostSecret import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService @@ -67,9 +66,7 @@ internal fun Route.postProductSecret(secretService: SecretService) = } } } - }) { - requirePermission(ProductPermission.WRITE_SECRETS) - + }, requirePermission(ProductPermission.WRITE_SECRETS)) { val productId = call.requireIdParameter("productId") val createSecret = call.receive() diff --git a/components/secrets/backend/src/routes/kotlin/routes/repository/GetAvailableRepositorySecrets.kt b/components/secrets/backend/src/routes/kotlin/routes/repository/GetAvailableRepositorySecrets.kt index 3198deb175..4877a9e6a8 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/repository/GetAvailableRepositorySecrets.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/repository/GetAvailableRepositorySecrets.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.repository -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -66,9 +65,7 @@ internal fun Route.getAvailableRepositorySecrets( } } } -}) { - requirePermission(RepositoryPermission.READ) - +}, requirePermission(RepositoryPermission.READ)) { val repositoryId = call.requireIdParameter("repositoryId") val hierarchy = repositoryService.getHierarchy(repositoryId) diff --git a/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecret.kt b/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecret.kt index 09396e9860..56337689f4 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecret.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.repository -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -60,9 +59,7 @@ internal fun Route.getRepositorySecret(secretService: SecretService) = } } } - }) { - requirePermission(RepositoryPermission.READ) - + }, requirePermission(RepositoryPermission.READ)) { val repositoryId = RepositoryId(call.requireIdParameter("repositoryId")) val secretName = call.requireParameter("secretName") diff --git a/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecrets.kt b/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecrets.kt index e84eadf7fc..16c6482d11 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecrets.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/repository/GetRepositorySecrets.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.repository -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi @@ -77,9 +76,7 @@ internal fun Route.getRepositorySecrets(secretService: SecretService) = } } } - }) { - requirePermission(RepositoryPermission.READ) - + }, requirePermission(RepositoryPermission.READ)) { val repositoryId = RepositoryId(call.requireIdParameter("repositoryId")) val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) diff --git a/components/secrets/backend/src/routes/kotlin/routes/repository/PatchRepositorySecret.kt b/components/secrets/backend/src/routes/kotlin/routes/repository/PatchRepositorySecret.kt index d1899cb264..09b36d6c15 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/repository/PatchRepositorySecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/repository/PatchRepositorySecret.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.repository -import io.github.smiley4.ktoropenapi.patch - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.PatchSecret import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService @@ -73,9 +72,7 @@ internal fun Route.patchRepositorySecret(secretService: SecretService) = } } } - }) { - requirePermission(RepositoryPermission.WRITE_SECRETS) - + }, requirePermission(RepositoryPermission.WRITE_SECRETS)) { val repositoryId = RepositoryId(call.requireIdParameter("repositoryId")) val secretName = call.requireParameter("secretName") val updateSecret = call.receive() diff --git a/components/secrets/backend/src/routes/kotlin/routes/repository/PostRepositorySecret.kt b/components/secrets/backend/src/routes/kotlin/routes/repository/PostRepositorySecret.kt index a579548e53..3f7024f383 100644 --- a/components/secrets/backend/src/routes/kotlin/routes/repository/PostRepositorySecret.kt +++ b/components/secrets/backend/src/routes/kotlin/routes/repository/PostRepositorySecret.kt @@ -19,15 +19,14 @@ package org.eclipse.apoapsis.ortserver.components.secrets.routes.repository -import io.github.smiley4.ktoropenapi.post - import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.secrets.PostSecret import org.eclipse.apoapsis.ortserver.components.secrets.Secret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService @@ -67,9 +66,7 @@ internal fun Route.postRepositorySecret(secretService: SecretService) = } } } - }) { - requirePermission(RepositoryPermission.WRITE_SECRETS) - + }, requirePermission(RepositoryPermission.WRITE_SECRETS)) { val repositoryId = call.requireIdParameter("repositoryId") val createSecret = call.receive() diff --git a/components/secrets/backend/src/test/kotlin/SecretsIntegrationTest.kt b/components/secrets/backend/src/test/kotlin/SecretsIntegrationTest.kt index a369635b45..b1e0340efc 100644 --- a/components/secrets/backend/src/test/kotlin/SecretsIntegrationTest.kt +++ b/components/secrets/backend/src/test/kotlin/SecretsIntegrationTest.kt @@ -22,8 +22,6 @@ package org.eclipse.apoapsis.ortserver.components.secrets import io.ktor.client.HttpClient import io.ktor.server.testing.ApplicationTestBuilder -import io.mockk.mockk - import org.eclipse.apoapsis.ortserver.model.repositories.SecretRepository import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting @@ -50,8 +48,7 @@ abstract class SecretsIntegrationTest(body: SecretsIntegrationTest.() -> Unit) : dbExtension.fixtures.scannerJobRepository, dbExtension.fixtures.evaluatorJobRepository, dbExtension.fixtures.reporterJobRepository, - dbExtension.fixtures.notifierJobRepository, - mockk() + dbExtension.fixtures.notifierJobRepository ) secretRepository = dbExtension.fixtures.secretRepository secretService = SecretService( diff --git a/components/secrets/backend/src/test/kotlin/routes/SecretsAuthorizationTest.kt b/components/secrets/backend/src/test/kotlin/routes/SecretsAuthorizationTest.kt index 13f7db261d..5f6a5d8b77 100644 --- a/components/secrets/backend/src/test/kotlin/routes/SecretsAuthorizationTest.kt +++ b/components/secrets/backend/src/test/kotlin/routes/SecretsAuthorizationTest.kt @@ -25,13 +25,17 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.HttpStatusCode -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole import org.eclipse.apoapsis.ortserver.components.secrets.PatchSecret import org.eclipse.apoapsis.ortserver.components.secrets.PostSecret import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.components.secrets.secretsRoutes +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.services.RepositoryService @@ -42,6 +46,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ var orgId = 0L var prodId = 0L var repoId = 0L + lateinit var orgHierarchyId: CompoundHierarchyId + lateinit var productHierarchyId: CompoundHierarchyId + lateinit var repoHierarchyId: CompoundHierarchyId lateinit var repositoryService: RepositoryService lateinit var secretService: SecretService @@ -50,7 +57,16 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ prodId = dbExtension.fixtures.product.id repoId = dbExtension.fixtures.repository.id - authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions() + orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(orgId)) + productHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(orgId), + ProductId(prodId) + ) + repoHierarchyId = CompoundHierarchyId.forRepository( + OrganizationId(orgId), + ProductId(prodId), + RepositoryId(repoId) + ) repositoryService = RepositoryService( dbExtension.db, @@ -61,8 +77,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ dbExtension.fixtures.scannerJobRepository, dbExtension.fixtures.evaluatorJobRepository, dbExtension.fixtures.reporterJobRepository, - dbExtension.fixtures.notifierJobRepository, - authorizationService + dbExtension.fixtures.notifierJobRepository ) secretService = SecretService( @@ -76,8 +91,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = RepositoryPermission.READ.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { get("/repositories/$repoId/secrets/availableSecrets") } @@ -88,8 +104,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = OrganizationPermission.READ.roleName(orgId), - successStatus = HttpStatusCode.NotFound + role = OrganizationRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = orgHierarchyId ) { get("/organizations/$orgId/secrets/name") } @@ -100,8 +117,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require ProductPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = ProductPermission.READ.roleName(prodId), - successStatus = HttpStatusCode.NotFound + role = ProductRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = productHierarchyId ) { get("/products/$prodId/secrets/name") } @@ -112,8 +130,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = RepositoryPermission.READ.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.READER, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { get("/repositories/$repoId/secrets/name") } @@ -124,7 +143,8 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = OrganizationPermission.READ.roleName(orgId) + role = OrganizationRole.READER, + hierarchyId = orgHierarchyId ) { get("/organizations/$orgId/secrets") } @@ -135,7 +155,8 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require ProductPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = ProductPermission.READ.roleName(prodId) + role = ProductRole.READER, + hierarchyId = productHierarchyId ) { get("/products/$prodId/secrets") } @@ -146,7 +167,8 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.READ" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = RepositoryPermission.READ.roleName(repoId) + role = RepositoryRole.READER, + hierarchyId = repoHierarchyId ) { get("/repositories/$repoId/secrets") } @@ -157,8 +179,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = OrganizationPermission.WRITE_SECRETS.roleName(orgId), - successStatus = HttpStatusCode.NotFound + role = OrganizationRole.ADMIN, + successStatus = HttpStatusCode.NotFound, + hierarchyId = orgHierarchyId ) { val updateSecret = PatchSecret("value".asPresent(), "description".asPresent()) patch("/organizations/$orgId/secrets/name") { setBody(updateSecret) } @@ -170,8 +193,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require ProductPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = ProductPermission.WRITE_SECRETS.roleName(prodId), - successStatus = HttpStatusCode.NotFound + role = ProductRole.ADMIN, + successStatus = HttpStatusCode.NotFound, + hierarchyId = productHierarchyId ) { val updateSecret = PatchSecret("value".asPresent(), "description".asPresent()) patch("/products/$prodId/secrets/name") { setBody(updateSecret) } @@ -183,8 +207,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = RepositoryPermission.WRITE_SECRETS.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.ADMIN, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { val updateSecret = PatchSecret("value".asPresent(), "description".asPresent()) patch("/repositories/$repoId/secrets/name") { setBody(updateSecret) } @@ -196,8 +221,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = OrganizationPermission.WRITE_SECRETS.roleName(orgId), - successStatus = HttpStatusCode.Created + role = OrganizationRole.ADMIN, + successStatus = HttpStatusCode.Created, + hierarchyId = orgHierarchyId ) { val createSecret = PostSecret("name", "value", "description") post("/organizations/$orgId/secrets") { setBody(createSecret) } @@ -209,8 +235,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require ProductPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = ProductPermission.WRITE_SECRETS.roleName(prodId), - successStatus = HttpStatusCode.Created + role = ProductRole.ADMIN, + successStatus = HttpStatusCode.Created, + hierarchyId = productHierarchyId ) { val createSecret = PostSecret("name", "value", "description") post("/products/$prodId/secrets") { setBody(createSecret) } @@ -222,8 +249,9 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsRoutes(repositoryService, secretService) }, - role = RepositoryPermission.WRITE_SECRETS.roleName(repoId), - successStatus = HttpStatusCode.Created + role = RepositoryRole.ADMIN, + successStatus = HttpStatusCode.Created, + hierarchyId = repoHierarchyId ) { val createSecret = PostSecret("name", "value", "description") post("/repositories/$repoId/secrets") { setBody(createSecret) } diff --git a/compositions/secrets-routes/build.gradle.kts b/compositions/secrets-routes/build.gradle.kts index 84a51bbe4d..870f1b05c4 100644 --- a/compositions/secrets-routes/build.gradle.kts +++ b/compositions/secrets-routes/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { api(ktorLibs.server.core) - implementation(projects.components.authorizationKeycloak.backend) + implementation(projects.components.authorization.backend) implementation(projects.components.infrastructureServices.backend) implementation(projects.components.secrets.apiModel) implementation(projects.components.secrets.backend) { diff --git a/compositions/secrets-routes/src/main/kotlin/routes/DeleteOrganizationSecret.kt b/compositions/secrets-routes/src/main/kotlin/routes/DeleteOrganizationSecret.kt index b4b52105cb..bb9d8f5e11 100644 --- a/compositions/secrets-routes/src/main/kotlin/routes/DeleteOrganizationSecret.kt +++ b/compositions/secrets-routes/src/main/kotlin/routes/DeleteOrganizationSecret.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.compositions.secretsroutes.routes -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.model.OrganizationId @@ -57,9 +56,7 @@ internal fun Route.deleteOrganizationSecret( description = "Success" } } -}) { - requirePermission(OrganizationPermission.WRITE_SECRETS) - +}, requirePermission(OrganizationPermission.WRITE_SECRETS)) { val organizationId = OrganizationId(call.requireIdParameter("organizationId")) val secretName = call.requireParameter("secretName") diff --git a/compositions/secrets-routes/src/main/kotlin/routes/DeleteProductSecret.kt b/compositions/secrets-routes/src/main/kotlin/routes/DeleteProductSecret.kt index fb00400868..5e7126d6e4 100644 --- a/compositions/secrets-routes/src/main/kotlin/routes/DeleteProductSecret.kt +++ b/compositions/secrets-routes/src/main/kotlin/routes/DeleteProductSecret.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.compositions.secretsroutes.routes -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.model.ProductId @@ -56,9 +55,7 @@ internal fun Route.deleteProductSecret( description = "Success" } } -}) { - requirePermission(ProductPermission.WRITE_SECRETS) - +}, requirePermission(ProductPermission.WRITE_SECRETS)) { val productId = ProductId(call.requireIdParameter("productId")) val secretName = call.requireParameter("secretName") diff --git a/compositions/secrets-routes/src/main/kotlin/routes/DeleteRepositorySecret.kt b/compositions/secrets-routes/src/main/kotlin/routes/DeleteRepositorySecret.kt index bbba9e6a96..6965b8f938 100644 --- a/compositions/secrets-routes/src/main/kotlin/routes/DeleteRepositorySecret.kt +++ b/compositions/secrets-routes/src/main/kotlin/routes/DeleteRepositorySecret.kt @@ -19,14 +19,13 @@ package org.eclipse.apoapsis.ortserver.compositions.secretsroutes.routes -import io.github.smiley4.ktoropenapi.delete - import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.routing.Route -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.model.RepositoryId @@ -56,9 +55,7 @@ internal fun Route.deleteRepositorySecret( description = "Success" } } -}) { - requirePermission(RepositoryPermission.WRITE_SECRETS) - +}, requirePermission(RepositoryPermission.WRITE_SECRETS)) { val repositoryId = RepositoryId(call.requireIdParameter("repositoryId")) val secretName = call.requireParameter("secretName") diff --git a/compositions/secrets-routes/src/test/kotlin/SecretsRoutesAuthorizationTest.kt b/compositions/secrets-routes/src/test/kotlin/SecretsRoutesAuthorizationTest.kt index f1b565c625..1bd8e93a20 100644 --- a/compositions/secrets-routes/src/test/kotlin/SecretsRoutesAuthorizationTest.kt +++ b/compositions/secrets-routes/src/test/kotlin/SecretsRoutesAuthorizationTest.kt @@ -22,11 +22,15 @@ package org.eclipse.apoapsis.ortserver.compositions.secretsroutes import io.ktor.client.request.delete import io.ktor.http.HttpStatusCode -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.secrets.SecretService +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.shared.ktorutils.AbstractAuthorizationTest @@ -35,6 +39,9 @@ class SecretsRoutesAuthorizationTest : AbstractAuthorizationTest({ var orgId = 0L var prodId = 0L var repoId = 0L + lateinit var orgHierarchyId: CompoundHierarchyId + lateinit var prodHierarchyId: CompoundHierarchyId + lateinit var repoHierarchyId: CompoundHierarchyId lateinit var infrastructureServiceService: InfrastructureServiceService lateinit var secretService: SecretService @@ -43,7 +50,16 @@ class SecretsRoutesAuthorizationTest : AbstractAuthorizationTest({ prodId = dbExtension.fixtures.product.id repoId = dbExtension.fixtures.repository.id - authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions() + orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(orgId)) + prodHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(orgId), + ProductId(prodId) + ) + repoHierarchyId = CompoundHierarchyId.forRepository( + OrganizationId(orgId), + ProductId(prodId), + RepositoryId(repoId) + ) secretService = SecretService( dbExtension.db, @@ -60,8 +76,9 @@ class SecretsRoutesAuthorizationTest : AbstractAuthorizationTest({ "require OrganizationPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsCompositionRoutes(infrastructureServiceService, secretService) }, - role = OrganizationPermission.WRITE_SECRETS.roleName(orgId), - successStatus = HttpStatusCode.NotFound + role = OrganizationRole.ADMIN, + successStatus = HttpStatusCode.NotFound, + hierarchyId = orgHierarchyId ) { delete("/organizations/$orgId/secrets/name") } @@ -72,8 +89,9 @@ class SecretsRoutesAuthorizationTest : AbstractAuthorizationTest({ "require ProductPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsCompositionRoutes(infrastructureServiceService, secretService) }, - role = ProductPermission.WRITE_SECRETS.roleName(prodId), - successStatus = HttpStatusCode.NotFound + role = ProductRole.ADMIN, + successStatus = HttpStatusCode.NotFound, + hierarchyId = prodHierarchyId ) { delete("/products/$prodId/secrets/name") } @@ -84,8 +102,9 @@ class SecretsRoutesAuthorizationTest : AbstractAuthorizationTest({ "require RepositoryPermission.WRITE_SECRETS" { requestShouldRequireRole( routes = { secretsCompositionRoutes(infrastructureServiceService, secretService) }, - role = RepositoryPermission.WRITE_SECRETS.roleName(repoId), - successStatus = HttpStatusCode.NotFound + role = RepositoryRole.ADMIN, + successStatus = HttpStatusCode.NotFound, + hierarchyId = repoHierarchyId ) { delete("/repositories/$repoId/secrets/name") } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e46b896c7c..69e09b217f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { requireCapability("$group:routes:$version") } } + implementation(projects.components.authorization.backend) implementation(projects.components.authorizationKeycloak.backend) implementation(projects.components.infrastructureServices.backend) implementation(projects.components.infrastructureServices.backend) { diff --git a/core/src/main/kotlin/Application.kt b/core/src/main/kotlin/Application.kt index 90ecfc8bcd..69cedfdb4e 100644 --- a/core/src/main/kotlin/Application.kt +++ b/core/src/main/kotlin/Application.kt @@ -21,7 +21,7 @@ package org.eclipse.apoapsis.ortserver.core import io.ktor.server.application.Application -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.configureAuthentication +import org.eclipse.apoapsis.ortserver.components.authorization.routes.configureAuthentication import org.eclipse.apoapsis.ortserver.core.plugins.* import org.koin.ktor.ext.get diff --git a/core/src/main/kotlin/api/AdminRoute.kt b/core/src/main/kotlin/api/AdminRoute.kt index 6d8b2c6df5..8d2807e77f 100644 --- a/core/src/main/kotlin/api/AdminRoute.kt +++ b/core/src/main/kotlin/api/AdminRoute.kt @@ -19,10 +19,7 @@ package org.eclipse.apoapsis.ortserver.core.api -import io.github.smiley4.ktoropenapi.delete import io.github.smiley4.ktoropenapi.get -import io.github.smiley4.ktoropenapi.patch -import io.github.smiley4.ktoropenapi.post import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive @@ -30,60 +27,39 @@ import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.route -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - import org.eclipse.apoapsis.ortserver.api.v1.mapping.mapToApi import org.eclipse.apoapsis.ortserver.api.v1.model.PatchSection import org.eclipse.apoapsis.ortserver.api.v1.model.PostUser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireAuthenticated -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.UserService +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.service.UserService import org.eclipse.apoapsis.ortserver.core.apiDocs.deleteUser import org.eclipse.apoapsis.ortserver.core.apiDocs.getSection import org.eclipse.apoapsis.ortserver.core.apiDocs.getUsers import org.eclipse.apoapsis.ortserver.core.apiDocs.patchSection import org.eclipse.apoapsis.ortserver.core.apiDocs.postUser -import org.eclipse.apoapsis.ortserver.core.apiDocs.runPermissionsSync import org.eclipse.apoapsis.ortserver.services.ContentManagementService import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireParameter import org.koin.ktor.ext.inject fun Route.admin() = route("admin") { - route("sync-roles") { - val authorizationService by inject() - - get(runPermissionsSync) { - requireSuperuser() - - withContext(Dispatchers.IO) { - launch { - authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions() - } - - call.respond(HttpStatusCode.Accepted) - } - } - } /** * For CRUD operations for users. */ route("users") { val userService by inject() - get(getUsers) { - requireSuperuser() - + get(getUsers, requireSuperuser()) { val users = userService.getUsers().map { user -> user.mapToApi() } call.respond(users) } - post(postUser) { - requireSuperuser() - + post(postUser, requireSuperuser()) { val createUser = call.receive() userService.createUser( username = createUser.username, @@ -97,9 +73,7 @@ fun Route.admin() = route("admin") { call.respond(HttpStatusCode.Created) } - delete(deleteUser) { - requireSuperuser() - + delete(deleteUser, requireSuperuser()) { val username = call.requireParameter("username") userService.deleteUser(username) @@ -115,7 +89,7 @@ fun Route.admin() = route("admin") { route("sections/{sectionId}") { get(getSection) { - requireAuthenticated() + requirePrincipal() val id = call.requireParameter("sectionId") @@ -125,9 +99,7 @@ fun Route.admin() = route("admin") { call.respond(HttpStatusCode.OK, section.mapToApi()) } - patch(patchSection) { - requireSuperuser() - + patch(patchSection, requireSuperuser()) { val id = call.requireParameter("sectionId") val updateSection = call.receive() diff --git a/core/src/main/kotlin/api/OrganizationsRoute.kt b/core/src/main/kotlin/api/OrganizationsRoute.kt index 67c2a172f1..2be0422731 100644 --- a/core/src/main/kotlin/api/OrganizationsRoute.kt +++ b/core/src/main/kotlin/api/OrganizationsRoute.kt @@ -19,13 +19,10 @@ package org.eclipse.apoapsis.ortserver.core.api -import io.github.smiley4.ktoropenapi.delete import io.github.smiley4.ktoropenapi.get -import io.github.smiley4.ktoropenapi.patch -import io.github.smiley4.ktoropenapi.post -import io.github.smiley4.ktoropenapi.put import io.ktor.http.HttpStatusCode +import io.ktor.server.auth.principal import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route @@ -38,14 +35,20 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.PatchOrganization import org.eclipse.apoapsis.ortserver.api.v1.model.PostOrganization import org.eclipse.apoapsis.ortserver.api.v1.model.PostProduct import org.eclipse.apoapsis.ortserver.api.v1.model.Username -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.api.OrganizationRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.hasPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.mapToModel -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.UserService +import org.eclipse.apoapsis.ortserver.components.authorization.api.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.put +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.UserService import org.eclipse.apoapsis.ortserver.core.api.UserWithGroupsHelper.mapToApi import org.eclipse.apoapsis.ortserver.core.api.UserWithGroupsHelper.sortAndPage import org.eclipse.apoapsis.ortserver.core.apiDocs.deleteOrganization @@ -61,6 +64,7 @@ import org.eclipse.apoapsis.ortserver.core.apiDocs.postOrganization import org.eclipse.apoapsis.ortserver.core.apiDocs.postProduct import org.eclipse.apoapsis.ortserver.core.apiDocs.putOrganizationRoleToUser import org.eclipse.apoapsis.ortserver.core.utils.vulnerabilityForRunsFilters +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.model.Product import org.eclipse.apoapsis.ortserver.model.VulnerabilityWithAccumulatedData @@ -99,13 +103,14 @@ fun Route.organizations() = route("organizations") { get(getOrganizations) { val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) val filter = call.filterParameter("filter") + val principal = requireNotNull(call.principal()) val filteredOrganizations = organizationService - .listOrganizations( + .listOrganizationsForUser( + principal.username, parameters = pagingOptions.copy(limit = null, offset = null).mapToModel(), filter = filter?.mapToModel() - ) - .data.filter { hasPermission(it.id, OrganizationPermission.READ) } + ).data val pagedOrganizations = filteredOrganizations.paginate(pagingOptions) .map { it.mapToApi() } @@ -118,9 +123,7 @@ fun Route.organizations() = route("organizations") { call.respond(HttpStatusCode.OK, pagedResponse) } - post(postOrganization) { - requireSuperuser() - + post(postOrganization, requireSuperuser()) { val createOrganization = call.receive() val createdOrganization = @@ -130,9 +133,7 @@ fun Route.organizations() = route("organizations") { } route("{organizationId}") { - get(getOrganization) { - requirePermission(OrganizationPermission.READ) - + get(getOrganization, requirePermission(OrganizationPermission.READ)) { val id = call.requireIdParameter("organizationId") val organization = organizationService.getOrganization(id) @@ -141,9 +142,7 @@ fun Route.organizations() = route("organizations") { ?: call.respond(HttpStatusCode.NotFound) } - patch(patchOrganization) { - requirePermission(OrganizationPermission.WRITE) - + patch(patchOrganization, requirePermission(OrganizationPermission.WRITE)) { val organizationId = call.requireIdParameter("organizationId") val org = call.receive() @@ -156,9 +155,7 @@ fun Route.organizations() = route("organizations") { call.respond(HttpStatusCode.OK, updatedOrg.mapToApi()) } - delete(deleteOrganization) { - requirePermission(OrganizationPermission.DELETE) - + delete(deleteOrganization, requirePermission(OrganizationPermission.DELETE)) { val id = call.requireIdParameter("organizationId") organizationService.deleteOrganization(id) @@ -167,16 +164,16 @@ fun Route.organizations() = route("organizations") { } route("products") { - get(getOrganizationProducts) { - requirePermission(OrganizationPermission.READ_PRODUCTS) - + get(getOrganizationProducts, requirePermission(OrganizationPermission.READ_PRODUCTS)) { val orgId = call.requireIdParameter("organizationId") val pagingOptions = call.pagingOptions(SortProperty("name", SortDirection.ASCENDING)) val filter = call.filterParameter("filter") + val principal = requirePrincipal() val productsForOrganization = - organizationService.listProductsForOrganization( + organizationService.listProductsForOrganizationAndUser( orgId, + principal.username, pagingOptions.mapToModel(), filter?.mapToModel() ) @@ -186,9 +183,7 @@ fun Route.organizations() = route("organizations") { call.respond(HttpStatusCode.OK, pagedResponse) } - post(postProduct) { - requirePermission(OrganizationPermission.CREATE_PRODUCT) - + post(postProduct, requirePermission(OrganizationPermission.CREATE_PRODUCT)) { val createProduct = call.receive() val orgId = call.requireIdParameter("organizationId") @@ -201,9 +196,7 @@ fun Route.organizations() = route("organizations") { route("roles") { route("{role}") { - put(putOrganizationRoleToUser) { - requirePermission(OrganizationPermission.MANAGE_GROUPS) - + put(putOrganizationRoleToUser, requirePermission(OrganizationPermission.MANAGE_GROUPS)) { val user = call.receive() val organizationId = call.requireIdParameter("organizationId") val role = call.requireEnumParameter("role").mapToModel() @@ -213,32 +206,46 @@ fun Route.organizations() = route("organizations") { return@put } - authorizationService.addUserRole(user.username, OrganizationId(organizationId), role) - call.respond(HttpStatusCode.NoContent) + if (!userService.userExists(user.username)) { + call.respondError( + HttpStatusCode.NotFound, + "Could not find user with username '${user.username}'." + ) + } else { + authorizationService.assignRole( + user.username, + role, + CompoundHierarchyId.forOrganization(OrganizationId(organizationId)) + ) + call.respond(HttpStatusCode.NoContent) + } } - delete(deleteOrganizationRoleFromUser) { - requirePermission(OrganizationPermission.MANAGE_GROUPS) - + delete(deleteOrganizationRoleFromUser, requirePermission(OrganizationPermission.MANAGE_GROUPS)) { val organizationId = call.requireIdParameter("organizationId") - val role = call.requireEnumParameter("role").mapToModel() val username = call.requireParameter("username") + call.requireEnumParameter("role") if (organizationService.getOrganization(organizationId) == null) { call.respondError(HttpStatusCode.NotFound, "Organization with ID '$organizationId' not found.") return@delete } - authorizationService.removeUserRole(username, OrganizationId(organizationId), role) - call.respond(HttpStatusCode.NoContent) + if (!userService.userExists(username)) { + call.respondError(HttpStatusCode.NotFound, "Could not find user with username '$username'.") + } else { + authorizationService.removeAssignment( + username, + CompoundHierarchyId.forOrganization(OrganizationId(organizationId)) + ) + call.respond(HttpStatusCode.NoContent) + } } } } route("vulnerabilities") { - get(getOrganizationVulnerabilities) { - requirePermission(OrganizationPermission.READ) - + get(getOrganizationVulnerabilities, requirePermission(OrganizationPermission.READ)) { val organizationId = call.requireIdParameter("organizationId") val pagingOptions = call.pagingOptions(SortProperty("rating", SortDirection.DESCENDING)) val filters = call.vulnerabilityForRunsFilters() @@ -265,9 +272,7 @@ fun Route.organizations() = route("organizations") { route("statistics") { route("runs") { - get(getOrganizationRunStatistics) { - requirePermission(OrganizationPermission.READ) - + get(getOrganizationRunStatistics, requirePermission(OrganizationPermission.READ)) { val orgId = call.requireIdParameter("organizationId") val repositoryIds = organizationService.getRepositoryIdsForOrganization(orgId) @@ -364,14 +369,13 @@ fun Route.organizations() = route("organizations") { } route("users") { - get(getOrganizationUsers) { - requirePermission(OrganizationPermission.READ) - - val orgId = call.requireIdParameter("organizationId") + get(getOrganizationUsers, requirePermission(OrganizationPermission.READ)) { + val orgId = CompoundHierarchyId.forOrganization( + OrganizationId(call.requireIdParameter("organizationId")) + ) val pagingOptions = call.pagingOptions(SortProperty("username", SortDirection.ASCENDING)) - val users = userService.getUsersHavingRightsForOrganization(orgId).mapToApi() - + val users = authorizationService.listUsers(orgId).mapToApi(userService) call.respond( PagedResponse(users.sortAndPage(pagingOptions), pagingOptions.toPagingData(users.size.toLong())) ) diff --git a/core/src/main/kotlin/api/ProductsRoute.kt b/core/src/main/kotlin/api/ProductsRoute.kt index beff813018..7eb5f53002 100644 --- a/core/src/main/kotlin/api/ProductsRoute.kt +++ b/core/src/main/kotlin/api/ProductsRoute.kt @@ -19,14 +19,7 @@ package org.eclipse.apoapsis.ortserver.core.api -import io.github.smiley4.ktoropenapi.delete -import io.github.smiley4.ktoropenapi.get -import io.github.smiley4.ktoropenapi.patch -import io.github.smiley4.ktoropenapi.post -import io.github.smiley4.ktoropenapi.put - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route @@ -40,18 +33,19 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.PatchProduct import org.eclipse.apoapsis.ortserver.api.v1.model.PostRepository import org.eclipse.apoapsis.ortserver.api.v1.model.PostRepositoryRun import org.eclipse.apoapsis.ortserver.api.v1.model.Username -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.api.ProductRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getFullName -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUsername -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.hasRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.mapToModel -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.UserService +import org.eclipse.apoapsis.ortserver.components.authorization.api.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel +import org.eclipse.apoapsis.ortserver.components.authorization.routes.ortServerPrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.put +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.UserService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.core.api.UserWithGroupsHelper.mapToApi import org.eclipse.apoapsis.ortserver.core.api.UserWithGroupsHelper.sortAndPage @@ -70,7 +64,6 @@ import org.eclipse.apoapsis.ortserver.core.services.OrchestratorService import org.eclipse.apoapsis.ortserver.core.utils.getPluginConfigs import org.eclipse.apoapsis.ortserver.core.utils.hasKeepAliveWorkerFlag import org.eclipse.apoapsis.ortserver.core.utils.vulnerabilityForRunsFilters -import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.model.Repository import org.eclipse.apoapsis.ortserver.model.UserDisplayName import org.eclipse.apoapsis.ortserver.model.VulnerabilityWithAccumulatedData @@ -107,9 +100,7 @@ fun Route.products() = route("products/{productId}") { val userService by inject() val orchestratorService by inject() - get(getProduct) { - requirePermission(ProductPermission.READ) - + get(getProduct, requirePermission(ProductPermission.READ)) { val id = call.requireIdParameter("productId") val product = productService.getProduct(id) @@ -121,9 +112,7 @@ fun Route.products() = route("products/{productId}") { } } - patch(patchProduct) { - requirePermission(ProductPermission.WRITE) - + patch(patchProduct, requirePermission(ProductPermission.WRITE)) { val id = call.requireIdParameter("productId") val updateProduct = call.receive() @@ -133,9 +122,7 @@ fun Route.products() = route("products/{productId}") { call.respond(HttpStatusCode.OK, updatedProduct.mapToApi()) } - delete(deleteProduct) { - requirePermission(ProductPermission.DELETE) - + delete(deleteProduct, requirePermission(ProductPermission.DELETE)) { val id = call.requireIdParameter("productId") productService.deleteProduct(id) @@ -144,24 +131,26 @@ fun Route.products() = route("products/{productId}") { } route("repositories") { - get(getProductRepositories) { - requirePermission(ProductPermission.READ_REPOSITORIES) + get(getProductRepositories, requirePermission(ProductPermission.READ_REPOSITORIES)) { val filter = call.filterParameter("filter") val productId = call.requireIdParameter("productId") val pagingOptions = call.pagingOptions(SortProperty("url", SortDirection.ASCENDING)) + val principal = requirePrincipal() - val repositoriesForProduct = - productService.listRepositoriesForProduct(productId, pagingOptions.mapToModel(), filter?.mapToModel()) + val repositoriesForProduct = productService.listRepositoriesForProductAndUser( + productId, + principal.username, + pagingOptions.mapToModel(), + filter?.mapToModel() + ) val pagedResponse = repositoriesForProduct.mapToApi(Repository::mapToApi) call.respond(HttpStatusCode.OK, pagedResponse) } - post(postRepository) { - requirePermission(ProductPermission.CREATE_REPOSITORY) - + post(postRepository, requirePermission(ProductPermission.CREATE_REPOSITORY)) { val id = call.requireIdParameter("productId") val createRepository = call.receive() val repository = productService.createRepository( @@ -180,9 +169,7 @@ fun Route.products() = route("products/{productId}") { route("roles") { route("{role}") { - put(putProductRoleToUser) { - requirePermission(ProductPermission.MANAGE_GROUPS) - + put(putProductRoleToUser, requirePermission(ProductPermission.MANAGE_GROUPS)) { val user = call.receive() val productId = call.requireIdParameter("productId") val role = call.requireEnumParameter("role").mapToModel() @@ -192,15 +179,18 @@ fun Route.products() = route("products/{productId}") { return@put } - authorizationService.addUserRole(user.username, ProductId(productId), role) + if (!userService.userExists(user.username)) { + call.respondError(HttpStatusCode.NotFound, "Could not find user '${user.username}'.") + return@put + } + + authorizationService.assignRole(user.username, role, call.ortServerPrincipal.effectiveRole.elementId) call.respond(HttpStatusCode.NoContent) } - delete(deleteProductRoleFromUser) { - requirePermission(ProductPermission.MANAGE_GROUPS) - + delete(deleteProductRoleFromUser, requirePermission(ProductPermission.MANAGE_GROUPS)) { val productId = call.requireIdParameter("productId") - val role = call.requireEnumParameter("role").mapToModel() + call.requireEnumParameter("role") val username = call.requireParameter("username") if (productService.getProduct(productId) == null) { @@ -208,16 +198,19 @@ fun Route.products() = route("products/{productId}") { return@delete } - authorizationService.removeUserRole(username, ProductId(productId), role) + if (!userService.userExists(username)) { + call.respondError(HttpStatusCode.NotFound, "Could not find user '$username'.") + return@delete + } + + authorizationService.removeAssignment(username, call.ortServerPrincipal.effectiveRole.elementId) call.respond(HttpStatusCode.NoContent) } } } route("vulnerabilities") { - get(getProductVulnerabilities) { - requirePermission(ProductPermission.READ) - + get(getProductVulnerabilities, requirePermission(ProductPermission.READ)) { val productId = call.requireIdParameter("productId") val pagingOptions = call.pagingOptions(SortProperty("rating", SortDirection.DESCENDING)) val filters = call.vulnerabilityForRunsFilters() @@ -241,9 +234,7 @@ fun Route.products() = route("products/{productId}") { route("statistics") { route("runs") { - get(getProductRunStatistics) { - requirePermission(ProductPermission.READ) - + get(getProductRunStatistics, requirePermission(ProductPermission.READ)) { val productId = call.requireIdParameter("productId") val repositoryIds = productService.getRepositoryIdsForProduct(productId) @@ -340,9 +331,7 @@ fun Route.products() = route("products/{productId}") { } route("runs") { - post(postProductRuns) { - requirePermission(ProductPermission.TRIGGER_ORT_RUN) - + post(postProductRuns, requirePermission(ProductPermission.TRIGGER_ORT_RUN)) { val productId = call.requireIdParameter("productId") val product = productService.getProduct(productId) @@ -353,8 +342,8 @@ fun Route.products() = route("products/{productId}") { } val createOrtRun = call.receive() - val userDisplayName = call.principal()?.let { principal -> - UserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName()) + val userDisplayName = call.ortServerPrincipal.let { principal -> + UserDisplayName(principal.userId, principal.username, principal.fullName) } // Validate the plugin configuration. @@ -375,7 +364,7 @@ fun Route.products() = route("products/{productId}") { } // Restrict the `keepAliveWorker` flags to superusers only. - if (createOrtRun.hasKeepAliveWorkerFlag() && !hasRole(Superuser.ROLE_NAME)) { + if (createOrtRun.hasKeepAliveWorkerFlag() && !call.ortServerPrincipal.effectiveRole.isSuperuser) { call.respondError( HttpStatusCode.Forbidden, "The 'keepAliveWorker' flag is only allowed for superusers." @@ -432,13 +421,11 @@ fun Route.products() = route("products/{productId}") { } route("users") { - get(getProductUsers) { - requirePermission(ProductPermission.READ) - - val productId = call.requireIdParameter("productId") + get(getProductUsers, requirePermission(ProductPermission.READ)) { val pagingOptions = call.pagingOptions(SortProperty("username", SortDirection.ASCENDING)) - val users = userService.getUsersHavingRightForProduct(productId).mapToApi() + val users = authorizationService.listUsers(call.ortServerPrincipal.effectiveRole.elementId) + .mapToApi(userService) call.respond( PagedResponse(users.sortAndPage(pagingOptions), pagingOptions.toPagingData(users.size.toLong())) diff --git a/core/src/main/kotlin/api/RepositoriesRoute.kt b/core/src/main/kotlin/api/RepositoriesRoute.kt index a88348be44..45ccf5ad70 100644 --- a/core/src/main/kotlin/api/RepositoriesRoute.kt +++ b/core/src/main/kotlin/api/RepositoriesRoute.kt @@ -19,14 +19,7 @@ package org.eclipse.apoapsis.ortserver.core.api -import io.github.smiley4.ktoropenapi.delete -import io.github.smiley4.ktoropenapi.get -import io.github.smiley4.ktoropenapi.patch -import io.github.smiley4.ktoropenapi.post -import io.github.smiley4.ktoropenapi.put - import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.principal import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route @@ -38,18 +31,18 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.Jobs import org.eclipse.apoapsis.ortserver.api.v1.model.PatchRepository import org.eclipse.apoapsis.ortserver.api.v1.model.PostRepositoryRun import org.eclipse.apoapsis.ortserver.api.v1.model.Username -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.api.RepositoryRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getFullName -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUsername -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.hasRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.mapToModel -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.UserService +import org.eclipse.apoapsis.ortserver.components.authorization.api.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel +import org.eclipse.apoapsis.ortserver.components.authorization.routes.ortServerPrincipal +import org.eclipse.apoapsis.ortserver.components.authorization.routes.patch +import org.eclipse.apoapsis.ortserver.components.authorization.routes.post +import org.eclipse.apoapsis.ortserver.components.authorization.routes.put +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requirePermission +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.UserService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginTemplateService import org.eclipse.apoapsis.ortserver.core.api.UserWithGroupsHelper.mapToApi import org.eclipse.apoapsis.ortserver.core.api.UserWithGroupsHelper.sortAndPage @@ -66,7 +59,6 @@ import org.eclipse.apoapsis.ortserver.core.apiDocs.putRepositoryRoleToUser import org.eclipse.apoapsis.ortserver.core.services.OrchestratorService import org.eclipse.apoapsis.ortserver.core.utils.getPluginConfigs import org.eclipse.apoapsis.ortserver.core.utils.hasKeepAliveWorkerFlag -import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.model.UserDisplayName import org.eclipse.apoapsis.ortserver.services.RepositoryService import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService @@ -92,18 +84,14 @@ fun Route.repositories() = route("repositories/{repositoryId}") { val repositoryService by inject() val userService by inject() - get(getRepository) { - requirePermission(RepositoryPermission.READ) - + get(getRepository, requirePermission(RepositoryPermission.READ)) { val id = call.requireIdParameter("repositoryId") repositoryService.getRepository(id)?.let { call.respond(HttpStatusCode.OK, it.mapToApi()) } ?: call.respond(HttpStatusCode.NotFound) } - patch(patchRepository) { - requirePermission(RepositoryPermission.WRITE) - + patch(patchRepository, requirePermission(RepositoryPermission.WRITE)) { val id = call.requireIdParameter("repositoryId") val updateRepository = call.receive() @@ -117,9 +105,7 @@ fun Route.repositories() = route("repositories/{repositoryId}") { call.respond(HttpStatusCode.OK, updatedRepository.mapToApi()) } - delete(deleteRepository) { - requirePermission(RepositoryPermission.DELETE) - + delete(deleteRepository, requirePermission(RepositoryPermission.DELETE)) { val id = call.requireIdParameter("repositoryId") repositoryService.deleteRepository(id) @@ -128,9 +114,7 @@ fun Route.repositories() = route("repositories/{repositoryId}") { } route("runs") { - get(getRepositoryRuns) { - requirePermission(RepositoryPermission.READ_ORT_RUNS) - + get(getRepositoryRuns, requirePermission(RepositoryPermission.READ_ORT_RUNS)) { val repositoryId = call.requireIdParameter("repositoryId") val pagingOptions = call.pagingOptions(SortProperty("index", SortDirection.ASCENDING)) @@ -139,17 +123,15 @@ fun Route.repositories() = route("repositories/{repositoryId}") { call.respond(HttpStatusCode.OK, pagedResponse) } - post(postRepositoryRun) { - requirePermission(RepositoryPermission.TRIGGER_ORT_RUN) - + post(postRepositoryRun, requirePermission(RepositoryPermission.TRIGGER_ORT_RUN)) { val repositoryId = call.requireIdParameter("repositoryId") repositoryService.getRepository(repositoryId)?.let { val createOrtRun = call.receive() // Extract the user information from the principal. - val userDisplayName = call.principal()?.let { principal -> - UserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName()) + val userDisplayName = call.ortServerPrincipal.let { principal -> + UserDisplayName(principal.userId, principal.username, principal.fullName) } // Validate the plugin configuration. @@ -170,7 +152,7 @@ fun Route.repositories() = route("repositories/{repositoryId}") { } // Restrict the `keepAliveWorker` flags to superusers only. - if (createOrtRun.hasKeepAliveWorkerFlag() && !hasRole(Superuser.ROLE_NAME)) { + if (createOrtRun.hasKeepAliveWorkerFlag() && !call.ortServerPrincipal.effectiveRole.isSuperuser) { call.respondError( HttpStatusCode.Forbidden, "The 'keepAliveWorker' flag is only allowed for superusers." @@ -195,9 +177,7 @@ fun Route.repositories() = route("repositories/{repositoryId}") { } route("{ortRunIndex}") { - get(getRepositoryRun) { - requirePermission(RepositoryPermission.READ_ORT_RUNS) - + get(getRepositoryRun, requirePermission(RepositoryPermission.READ_ORT_RUNS)) { val repositoryId = call.requireIdParameter("repositoryId") val ortRunIndex = call.requireIdParameter("ortRunIndex") @@ -208,12 +188,10 @@ fun Route.repositories() = route("repositories/{repositoryId}") { } ?: call.respond(HttpStatusCode.NotFound) } - delete(deleteRepositoryRun) { + delete(deleteRepositoryRun, requirePermission(RepositoryPermission.DELETE)) { val repositoryId = call.requireIdParameter("repositoryId") val ortRunIndex = call.requireIdParameter("ortRunIndex") - requirePermission(RepositoryPermission.DELETE) - repositoryService.getOrtRunId(repositoryId, ortRunIndex)?.let { ortRunId -> ortRunService.deleteOrtRun(ortRunId) call.respond(HttpStatusCode.NoContent) @@ -224,9 +202,7 @@ fun Route.repositories() = route("repositories/{repositoryId}") { route("roles") { route("{role}") { - put(putRepositoryRoleToUser) { - requirePermission(RepositoryPermission.MANAGE_GROUPS) - + put(putRepositoryRoleToUser, requirePermission(RepositoryPermission.MANAGE_GROUPS)) { val user = call.receive() val repositoryId = call.requireIdParameter("repositoryId") val role = call.requireEnumParameter("role").mapToModel() @@ -236,15 +212,18 @@ fun Route.repositories() = route("repositories/{repositoryId}") { return@put } - authorizationService.addUserRole(user.username, RepositoryId(repositoryId), role) + if (!userService.userExists(user.username)) { + call.respondError(HttpStatusCode.NotFound, "Could not find user '${user.username}'.") + return@put + } + + authorizationService.assignRole(user.username, role, call.ortServerPrincipal.effectiveRole.elementId) call.respond(HttpStatusCode.NoContent) } - delete(deleteRepositoryRoleFromUser) { - requirePermission(RepositoryPermission.MANAGE_GROUPS) - + delete(deleteRepositoryRoleFromUser, requirePermission(RepositoryPermission.MANAGE_GROUPS)) { val repositoryId = call.requireIdParameter("repositoryId") - val role = call.requireEnumParameter("role").mapToModel() + call.requireEnumParameter("role") val username = call.requireParameter("username") if (repositoryService.getRepository(repositoryId) == null) { @@ -252,20 +231,23 @@ fun Route.repositories() = route("repositories/{repositoryId}") { return@delete } - authorizationService.removeUserRole(username, RepositoryId(repositoryId), role) + if (!userService.userExists(username)) { + call.respondError(HttpStatusCode.NotFound, "Could not find user '$username'.") + return@delete + } + + authorizationService.removeAssignment(username, call.ortServerPrincipal.effectiveRole.elementId) call.respond(HttpStatusCode.NoContent) } } } route("users") { - get(getRepositoryUsers) { - requirePermission(RepositoryPermission.READ) - - val repositoryId = call.requireIdParameter("repositoryId") + get(getRepositoryUsers, requirePermission(RepositoryPermission.READ)) { val pagingOptions = call.pagingOptions(SortProperty("username", SortDirection.ASCENDING)) - val users = userService.getUsersHavingRightsForRepository(repositoryId).mapToApi() + val users = authorizationService.listUsers(call.ortServerPrincipal.effectiveRole.elementId) + .mapToApi(userService) call.respond( PagedResponse(users.sortAndPage(pagingOptions), pagingOptions.toPagingData(users.size.toLong())) diff --git a/core/src/main/kotlin/api/RunsRoute.kt b/core/src/main/kotlin/api/RunsRoute.kt index 57014cf8e0..0b371ffb2b 100644 --- a/core/src/main/kotlin/api/RunsRoute.kt +++ b/core/src/main/kotlin/api/RunsRoute.kt @@ -21,9 +21,6 @@ package org.eclipse.apoapsis.ortserver.core.api -import io.github.smiley4.ktoropenapi.delete -import io.github.smiley4.ktoropenapi.get - import io.ktor.http.ContentDisposition import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode @@ -34,6 +31,7 @@ import io.ktor.server.response.respondFile import io.ktor.server.response.respondOutputStream import io.ktor.server.routing.Route import io.ktor.server.routing.route +import io.ktor.util.AttributeKey import kotlinx.datetime.Clock @@ -50,9 +48,15 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.api.v1.model.PackageFilters import org.eclipse.apoapsis.ortserver.api.v1.model.RuleViolationFilters import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityFilters -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.HierarchyPermissions +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.AuthorizationChecker +import org.eclipse.apoapsis.ortserver.components.authorization.routes.delete +import org.eclipse.apoapsis.ortserver.components.authorization.routes.get +import org.eclipse.apoapsis.ortserver.components.authorization.routes.requireSuperuser +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.InvalidHierarchyIdException import org.eclipse.apoapsis.ortserver.core.apiDocs.deleteRun import org.eclipse.apoapsis.ortserver.core.apiDocs.getRun import org.eclipse.apoapsis.ortserver.core.apiDocs.getRunIssues @@ -71,6 +75,7 @@ import org.eclipse.apoapsis.ortserver.model.JobStatus import org.eclipse.apoapsis.ortserver.model.LogLevel import org.eclipse.apoapsis.ortserver.model.LogSource import org.eclipse.apoapsis.ortserver.model.OrtRun +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.model.VulnerabilityWithDetails import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository import org.eclipse.apoapsis.ortserver.model.runs.Issue @@ -110,9 +115,36 @@ fun Route.runs() = route("runs") { val projectService by inject() val ortRunService by inject() - get(getRuns) { - requireSuperuser() + /** + * Return a special [AuthorizationChecker] that checks for the given [permission] on the repository to which a + * run identified by the `runId` parameter belongs. + */ + fun requireRunPermission( + permission: RepositoryPermission = RepositoryPermission.READ_ORT_RUNS + ): AuthorizationChecker = + object : AuthorizationChecker { + override suspend fun loadEffectiveRole( + service: AuthorizationService, + userId: String, + call: ApplicationCall + ): EffectiveRole? { + val runId = call.requireIdParameter("runId") + val ortRun = ortRunRepository.get(runId) ?: throw InvalidHierarchyIdException(RepositoryId(runId)) + + return service.checkPermissions( + userId, + RepositoryId(ortRun.repositoryId), + HierarchyPermissions.permissions(permission) + ).also { + // Store the current run, so that it is directly available to route handlers. + call.attributes.put(keyOrtRun, ortRun) + } + } + + override fun toString(): String = "RequireRunPermission($permission)" + } + get(getRuns, requireSuperuser()) { val pagingOptions = call.pagingOptions(SortProperty("createdAt", SortDirection.DESCENDING)) val filters = call.filters() @@ -134,153 +166,127 @@ fun Route.runs() = route("runs") { } route("{runId}") { - get(getRun) { - val ortRunId = call.requireIdParameter("runId") + get(getRun, requireRunPermission()) { + val ortRun = call.ortRun - ortRunRepository.get(ortRunId)?.let { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - repositoryService.getJobs(ortRun.repositoryId, ortRun.index)?.let { jobs -> - call.respond(HttpStatusCode.OK, ortRun.mapToApi(jobs.mapToApi())) - } + repositoryService.getJobs(ortRun.repositoryId, ortRun.index)?.let { jobs -> + call.respond(HttpStatusCode.OK, ortRun.mapToApi(jobs.mapToApi())) } ?: call.respond(HttpStatusCode.NotFound) } - delete(deleteRun) { + delete(deleteRun, requireRunPermission(RepositoryPermission.DELETE)) { val ortRunId = call.requireIdParameter("runId") - ortRunRepository.get(ortRunId)?.let { ortRun -> - requirePermission(RepositoryPermission.DELETE.roleName(ortRun.repositoryId)) - - ortRunService.deleteOrtRun(ortRunId) - call.respond(HttpStatusCode.NoContent) - } ?: call.respond(HttpStatusCode.NotFound) + ortRunService.deleteOrtRun(ortRunId) + call.respond(HttpStatusCode.NoContent) } route("logs") { val logFileService by inject() - get(getRunLogs) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - val sources = call.extractSteps() - val level = call.extractLevel() - val startTime = ortRun.createdAt - val endTime = ortRun.finishedAt ?: Clock.System.now() - val logArchive = logFileService.createLogFilesArchive(ortRun.id, sources, level, startTime, endTime) - - try { - call.response.header( - HttpHeaders.ContentDisposition, - ContentDisposition.Attachment.withParameter( - ContentDisposition.Parameters.FileName, - "run-${ortRun.id}-$level-logs.zip" - ).toString() - ) - - call.respondFile(logArchive) - } finally { - logArchive.delete() - } + get(getRunLogs, requireRunPermission()) { + val sources = call.extractSteps() + val level = call.extractLevel() + val startTime = call.ortRun.createdAt + val endTime = call.ortRun.finishedAt ?: Clock.System.now() + val logArchive = logFileService.createLogFilesArchive( + call.ortRun.id, + sources, + level, + startTime, + endTime + ) + + try { + call.response.header( + HttpHeaders.ContentDisposition, + ContentDisposition.Attachment.withParameter( + ContentDisposition.Parameters.FileName, + "run-${call.ortRun.id}-$level-logs.zip" + ).toString() + ) + + call.respondFile(logArchive) + } finally { + logArchive.delete() } } } route("issues") { - get(getRunIssues) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - val pagingOptions = call.pagingOptions(SortProperty("timestamp", SortDirection.DESCENDING)) - val filters = call.issueFilters() + get(getRunIssues, requireRunPermission()) { + val pagingOptions = call.pagingOptions(SortProperty("timestamp", SortDirection.DESCENDING)) + val filters = call.issueFilters() - val issueForOrtRun = issueService.listForOrtRunId(ortRun.id, pagingOptions.mapToModel(), filters) + val issueForOrtRun = issueService.listForOrtRunId(call.ortRun.id, pagingOptions.mapToModel(), filters) - val pagedResponse = issueForOrtRun.mapToApi(Issue::mapToApi) + val pagedResponse = issueForOrtRun.mapToApi(Issue::mapToApi) - call.respond(HttpStatusCode.OK, pagedResponse) - } + call.respond(HttpStatusCode.OK, pagedResponse) } } route("vulnerabilities") { - get(getRunVulnerabilities) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - val pagingOptions = call.pagingOptions(SortProperty("externalId", SortDirection.ASCENDING)) - val filters = call.vulnerabilityFilters() - - val vulnerabilitiesForOrtRun = - vulnerabilityService.listForOrtRunId( - ortRun.id, - pagingOptions.mapToModel(), - filters.mapToModel() - ) + get(getRunVulnerabilities, requireRunPermission()) { + val pagingOptions = call.pagingOptions(SortProperty("externalId", SortDirection.ASCENDING)) + val filters = call.vulnerabilityFilters() + + val vulnerabilitiesForOrtRun = + vulnerabilityService.listForOrtRunId( + call.ortRun.id, + pagingOptions.mapToModel(), + filters.mapToModel() + ) - val pagedResponse = vulnerabilitiesForOrtRun.mapToApi(VulnerabilityWithDetails::mapToApi) + val pagedResponse = vulnerabilitiesForOrtRun.mapToApi(VulnerabilityWithDetails::mapToApi) - call.respond(HttpStatusCode.OK, pagedResponse) - } + call.respond(HttpStatusCode.OK, pagedResponse) } } route("rule-violations") { - get(getRunRuleViolations) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - val pagingOptions = call.pagingOptions(SortProperty("rule", SortDirection.ASCENDING)) - val filters = call.ruleViolationFilters() - - val ruleViolationsForOrtRun = ruleViolationService - .listForOrtRunId( - ortRun.id, - pagingOptions.mapToModel(), - filters.mapToModel() - ) - - val pagedResponse = ruleViolationsForOrtRun.mapToApi( - RuleViolation::mapToApi + get(getRunRuleViolations, requireRunPermission()) { + val pagingOptions = call.pagingOptions(SortProperty("rule", SortDirection.ASCENDING)) + val filters = call.ruleViolationFilters() + + val ruleViolationsForOrtRun = ruleViolationService + .listForOrtRunId( + call.ortRun.id, + pagingOptions.mapToModel(), + filters.mapToModel() ) - call.respond(HttpStatusCode.OK, pagedResponse) - } + val pagedResponse = ruleViolationsForOrtRun.mapToApi( + RuleViolation::mapToApi + ) + + call.respond(HttpStatusCode.OK, pagedResponse) } } route("packages") { - get(getRunPackages) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) + get(getRunPackages, requireRunPermission()) { + val pagingOptions = call.pagingOptions(SortProperty("purl", SortDirection.ASCENDING)) - val pagingOptions = call.pagingOptions(SortProperty("purl", SortDirection.ASCENDING)) + val filters = call.packageFilters() - val filters = call.packageFilters() + val packagesForOrtRun = packageService + .listForOrtRunId(call.ortRun.id, pagingOptions.mapToModel(), filters.mapToModel()) - val packagesForOrtRun = packageService - .listForOrtRunId(ortRun.id, pagingOptions.mapToModel(), filters.mapToModel()) + val pagedResponse = packagesForOrtRun + .mapToApi(PackageRunData::mapToApi) + .toSearchResponse(filters) - val pagedResponse = packagesForOrtRun - .mapToApi(PackageRunData::mapToApi) - .toSearchResponse(filters) - - call.respond(HttpStatusCode.OK, pagedResponse) - } + call.respond(HttpStatusCode.OK, pagedResponse) } route("licenses") { - get(getRunPackageLicenses) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - val licenses = Licenses( - packageService.getProcessedDeclaredLicenses(ortRun.id) - ) + get(getRunPackageLicenses, requireRunPermission()) { + val licenses = Licenses( + packageService.getProcessedDeclaredLicenses(call.ortRun.id) + ) - call.respond(HttpStatusCode.OK, licenses) - } + call.respond(HttpStatusCode.OK, licenses) } } } @@ -288,110 +294,99 @@ fun Route.runs() = route("runs") { route("reporter/{fileName}") { val reportStorageService by inject() - get(getRunReport) { - call.forRun(ortRunRepository) { ortRun -> - val fileName = call.requireParameter("fileName") + get(getRunReport, requireRunPermission()) { + val fileName = call.requireParameter("fileName") - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) + val downloadData = reportStorageService.fetchReport(call.ortRun.id, fileName) - val downloadData = reportStorageService.fetchReport(ortRun.id, fileName) - - call.response.header( - HttpHeaders.ContentDisposition, - ContentDisposition.Attachment.withParameter( - ContentDisposition.Parameters.FileName, - fileName - ).toString() - ) + call.response.header( + HttpHeaders.ContentDisposition, + ContentDisposition.Attachment.withParameter( + ContentDisposition.Parameters.FileName, + fileName + ).toString() + ) - call.respondOutputStream( - downloadData.contentType, - producer = downloadData.loader, - contentLength = downloadData.contentLength - ) - } + call.respondOutputStream( + downloadData.contentType, + producer = downloadData.loader, + contentLength = downloadData.contentLength + ) } } route("statistics") { - get(getRunStatistics) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) + get(getRunStatistics, requireRunPermission()) { + val ortRun = call.ortRun + val jobs = repositoryService.getJobs(ortRun.repositoryId, ortRun.index) - val jobs = repositoryService.getJobs(ortRun.repositoryId, ortRun.index) + val analyzerJobInFinalState = jobs?.analyzer?.status in JobStatus.FINAL_STATUSES + val analyzerJobInFinishedState = jobs?.analyzer?.status in JobStatus.SUCCESSFUL_STATUSES + val advisorJobInFinishedState = jobs?.advisor?.status in JobStatus.SUCCESSFUL_STATUSES + val evaluatorJobInFinishedState = jobs?.evaluator?.status in JobStatus.SUCCESSFUL_STATUSES - val analyzerJobInFinalState = jobs?.analyzer?.status in JobStatus.FINAL_STATUSES - val analyzerJobInFinishedState = jobs?.analyzer?.status in JobStatus.SUCCESSFUL_STATUSES - val advisorJobInFinishedState = jobs?.advisor?.status in JobStatus.SUCCESSFUL_STATUSES - val evaluatorJobInFinishedState = jobs?.evaluator?.status in JobStatus.SUCCESSFUL_STATUSES + val issuesCount = if (analyzerJobInFinalState) issueService.countForOrtRunIds(ortRun.id) else null - val issuesCount = if (analyzerJobInFinalState) issueService.countForOrtRunIds(ortRun.id) else null - - val issuesBySeverity = if (analyzerJobInFinalState) { - issueService.countBySeverityForOrtRunIds(ortRun.id).map.mapKeys { it.key.mapToApi() } - } else { - null - } + val issuesBySeverity = if (analyzerJobInFinalState) { + issueService.countBySeverityForOrtRunIds(ortRun.id).map.mapKeys { it.key.mapToApi() } + } else { + null + } - val packagesCount = - if (analyzerJobInFinishedState) packageService.countForOrtRunIds(ortRun.id) else null + val packagesCount = + if (analyzerJobInFinishedState) packageService.countForOrtRunIds(ortRun.id) else null - val ecosystems = if (analyzerJobInFinishedState) { - packageService.countEcosystemsForOrtRunIds(ortRun.id).map { ecosystemStats -> - ecosystemStats.mapToApi() - } - } else { - null + val ecosystems = if (analyzerJobInFinishedState) { + packageService.countEcosystemsForOrtRunIds(ortRun.id).map { ecosystemStats -> + ecosystemStats.mapToApi() } + } else { + null + } - val vulnerabilitiesCount = - if (advisorJobInFinishedState) vulnerabilityService.countForOrtRunIds(ortRun.id) else null + val vulnerabilitiesCount = + if (advisorJobInFinishedState) vulnerabilityService.countForOrtRunIds(ortRun.id) else null - val vulnerabilitiesByRating = if (advisorJobInFinishedState) { - vulnerabilityService.countByRatingForOrtRunIds(ortRun.id).map.mapKeys { it.key.mapToApi() } - } else { - null - } + val vulnerabilitiesByRating = if (advisorJobInFinishedState) { + vulnerabilityService.countByRatingForOrtRunIds(ortRun.id).map.mapKeys { it.key.mapToApi() } + } else { + null + } - val ruleViolationsCount = - if (evaluatorJobInFinishedState) ruleViolationService.countForOrtRunIds(ortRun.id) else null + val ruleViolationsCount = + if (evaluatorJobInFinishedState) ruleViolationService.countForOrtRunIds(ortRun.id) else null - val ruleViolationsBySeverity = if (evaluatorJobInFinishedState) { - ruleViolationService.countBySeverityForOrtRunIds(ortRun.id).map.mapKeys { it.key.mapToApi() } - } else { - null - } + val ruleViolationsBySeverity = if (evaluatorJobInFinishedState) { + ruleViolationService.countBySeverityForOrtRunIds(ortRun.id).map.mapKeys { it.key.mapToApi() } + } else { + null + } - call.respond( - HttpStatusCode.OK, - OrtRunStatistics( - issuesCount = issuesCount, - issuesCountBySeverity = issuesBySeverity, - packagesCount = packagesCount, - ecosystems = ecosystems, - vulnerabilitiesCount = vulnerabilitiesCount, - vulnerabilitiesCountByRating = vulnerabilitiesByRating, - ruleViolationsCount = ruleViolationsCount, - ruleViolationsCountBySeverity = ruleViolationsBySeverity - ) + call.respond( + HttpStatusCode.OK, + OrtRunStatistics( + issuesCount = issuesCount, + issuesCountBySeverity = issuesBySeverity, + packagesCount = packagesCount, + ecosystems = ecosystems, + vulnerabilitiesCount = vulnerabilitiesCount, + vulnerabilitiesCountByRating = vulnerabilitiesByRating, + ruleViolationsCount = ruleViolationsCount, + ruleViolationsCountBySeverity = ruleViolationsBySeverity ) - } + ) } } route("projects") { - get(getRunProjects) { - call.forRun(ortRunRepository) { ortRun -> - requirePermission(RepositoryPermission.READ_ORT_RUNS.roleName(ortRun.repositoryId)) - - val pagingOptions = call.pagingOptions(SortProperty("id", SortDirection.ASCENDING)) + get(getRunProjects, requireRunPermission()) { + val pagingOptions = call.pagingOptions(SortProperty("id", SortDirection.ASCENDING)) - val projectsForOrtRun = projectService.listForOrtRunId(ortRun.id, pagingOptions.mapToModel()) + val projectsForOrtRun = projectService.listForOrtRunId(call.ortRun.id, pagingOptions.mapToModel()) - val pagedResponse = projectsForOrtRun.mapToApi(Project::mapToApi) + val pagedResponse = projectsForOrtRun.mapToApi(Project::mapToApi) - call.respond(HttpStatusCode.OK, pagedResponse) - } + call.respond(HttpStatusCode.OK, pagedResponse) } } } @@ -511,3 +506,14 @@ private fun ApplicationCall.vulnerabilityFilters() = VulnerabilityFilters( resolved = parameters["resolved"]?.lowercase()?.toBooleanStrictOrNull() ) + +/** + * A key under which the current [OrtRun] is stored in the current call. + */ +private val keyOrtRun = AttributeKey("RunsRoute.OrtRun") + +/** + * The current [OrtRun] stored in this [ApplicationCall]. + */ +private val ApplicationCall.ortRun: OrtRun + get() = attributes[keyOrtRun] diff --git a/core/src/main/kotlin/api/UserWithGroupsHelper.kt b/core/src/main/kotlin/api/UserWithGroupsHelper.kt index b02426fc50..1838526032 100644 --- a/core/src/main/kotlin/api/UserWithGroupsHelper.kt +++ b/core/src/main/kotlin/api/UserWithGroupsHelper.kt @@ -21,6 +21,9 @@ package org.eclipse.apoapsis.ortserver.core.api import org.eclipse.apoapsis.ortserver.api.v1.mapping.mapToApi import org.eclipse.apoapsis.ortserver.api.v1.model.UserWithGroups +import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToGroup +import org.eclipse.apoapsis.ortserver.components.authorization.service.UserService import org.eclipse.apoapsis.ortserver.dao.QueryParametersException import org.eclipse.apoapsis.ortserver.model.User import org.eclipse.apoapsis.ortserver.model.UserGroup @@ -66,4 +69,12 @@ internal object UserWithGroupsHelper { user.value.map { it.mapToApi() }.toList().sortedBy { it.getRank() }.reversed() ) } + + internal suspend fun Map.mapToApi(userService: UserService): List { + val userMapping = userService.getUsersById(keys).associateBy(User::username) + + return filter { it.key in userMapping } + .mapKeys { userMapping.getValue(it.key) } + .mapValues { (_, role) -> setOf(role.mapToGroup()) }.mapToApi() + } } diff --git a/core/src/main/kotlin/di/Module.kt b/core/src/main/kotlin/di/Module.kt index 6cf8df9d98..ca3749ca9d 100644 --- a/core/src/main/kotlin/di/Module.kt +++ b/core/src/main/kotlin/di/Module.kt @@ -28,9 +28,11 @@ import kotlinx.serialization.json.Json import org.eclipse.apoapsis.ortserver.clients.keycloak.DefaultKeycloakClient import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClient -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.UserService +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.migration.RolesToDbMigration +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.KeycloakUserService +import org.eclipse.apoapsis.ortserver.components.authorization.service.UserService import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InfrastructureServiceService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginEventStore import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginService @@ -194,15 +196,11 @@ fun ortServerModule(config: ApplicationConfig, db: Database?, authorizationServi if (authorizationService != null) { single { authorizationService } } else { - single { - val keycloakGroupPrefix = get().tryGetString("keycloak.groupPrefix").orEmpty() - KeycloakAuthorizationService(get(), get(), get(), get(), get(), keycloakGroupPrefix) - } + single { DbAuthorizationService(get()) } } single { - val keycloakGroupPrefix = get().tryGetString("keycloak.groupPrefix").orEmpty() - UserService(get(), keycloakGroupPrefix) + KeycloakUserService(get()) } single { @@ -214,4 +212,17 @@ fun ortServerModule(config: ApplicationConfig, db: Database?, authorizationServi singleOf(::PluginService) singleOf(::PluginTemplateEventStore) singleOf(::PluginTemplateService) + + single { RolesToDbMigration(get(), get(), getKeycloakGroupPrefix(config), get()) } } + +/** + * Retrieve the prefix for Keycloak groups representing roles for hierarchy elements from the given [config]. This is + * needed for the migration of roles managed by Keycloak to roles stored in the database. The prefix is obtained from + * the configuration of the authorization component based on Keycloak. It is, however, possible to override it via a + * special property for the migration. This is useful for instance, to test the migration on different ORT Server + * deployments, e.g. a test environment. + */ +private fun getKeycloakGroupPrefix(config: ApplicationConfig): String = + config.tryGetString("keycloak.migrationGroupPrefix") + ?: config.tryGetString("keycloak.groupPrefix").orEmpty() diff --git a/core/src/main/kotlin/plugins/Koin.kt b/core/src/main/kotlin/plugins/Koin.kt index 81510d735e..db7d48c05e 100644 --- a/core/src/main/kotlin/plugins/Koin.kt +++ b/core/src/main/kotlin/plugins/Koin.kt @@ -22,7 +22,7 @@ package org.eclipse.apoapsis.ortserver.core.plugins import io.ktor.server.application.Application import io.ktor.server.application.install -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService import org.eclipse.apoapsis.ortserver.core.di.ortServerModule import org.jetbrains.exposed.sql.Database diff --git a/core/src/main/kotlin/plugins/Lifecycle.kt b/core/src/main/kotlin/plugins/Lifecycle.kt index 9a620d9b28..a5176877d9 100644 --- a/core/src/main/kotlin/plugins/Lifecycle.kt +++ b/core/src/main/kotlin/plugins/Lifecycle.kt @@ -27,7 +27,7 @@ import kotlin.concurrent.thread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.migration.RolesToDbMigration import org.eclipse.apoapsis.ortserver.utils.logging.runBlocking import org.eclipse.apoapsis.ortserver.utils.logging.withMdcContext @@ -41,26 +41,28 @@ import org.slf4j.MDC */ fun Application.configureLifecycle() { monitor.subscribe(ApplicationStarted) { - val authorizationService by inject() + val rolesMigration by inject() val mdcContext = MDC.getCopyOfContextMap() thread { MDC.setContextMap(mdcContext) runBlocking(Dispatchers.IO) { - syncRoles(authorizationService) + migrateRoles(rolesMigration) } } } } /** - * Trigger the synchronization of permissions and roles in Keycloak. The synchronization then runs in the background. + * Perform a migration to new database-based structures for access rights if necessary. This makes sure that the + * new structures are populated once when switching from access rights stored in Keycloak to the new storage in the + * database. The migration then runs in the background. */ -private suspend fun syncRoles(authorizationService: AuthorizationService) { +private suspend fun migrateRoles(migration: RolesToDbMigration) { withMdcContext("component" to "core") { launch { - authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions() + migration.migrateRolesToDb() } } } diff --git a/core/src/main/kotlin/plugins/StatusPages.kt b/core/src/main/kotlin/plugins/StatusPages.kt index f2d6b4065e..a5f7773081 100644 --- a/core/src/main/kotlin/plugins/StatusPages.kt +++ b/core/src/main/kotlin/plugins/StatusPages.kt @@ -29,7 +29,8 @@ import io.ktor.server.plugins.requestvalidation.RequestValidationException import io.ktor.server.plugins.statuspages.StatusPages import io.ktor.server.response.respond -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.AuthorizationException +import org.eclipse.apoapsis.ortserver.components.authorization.routes.AuthorizationException +import org.eclipse.apoapsis.ortserver.components.authorization.service.InvalidHierarchyIdException import org.eclipse.apoapsis.ortserver.components.infrastructureservices.InvalidSecretReferenceException import org.eclipse.apoapsis.ortserver.core.api.AuthenticationException import org.eclipse.apoapsis.ortserver.dao.QueryParametersException @@ -61,6 +62,9 @@ fun Application.configureStatusPages() { exception { call, _ -> call.respond(HttpStatusCode.NotFound) } + exception { call, e -> + call.respondError(HttpStatusCode.NotFound, e.message.orEmpty()) + } exception { call, e -> call.respondError(HttpStatusCode.BadRequest, "Secret reference could not be resolved.", e.message) } diff --git a/core/src/main/resources/application.conf b/core/src/main/resources/application.conf index 7ebc45d6f1..187ad85308 100644 --- a/core/src/main/resources/application.conf +++ b/core/src/main/resources/application.conf @@ -74,6 +74,7 @@ keycloak { subjectClientId = ${?KEYCLOAK_SUBJECT_CLIENT_ID} groupPrefix = "" groupPrefix = ${?KEYCLOAK_GROUP_PREFIX} + migrationGroupPrefix = ${?KEYCLOAK_MIGRATION_GROUP_PREFIX} timeoutSeconds = 60 timeoutSeconds = ${?KEYCLOAK_TIMEOUT_SECONDS} } diff --git a/core/src/test/kotlin/ApplicationTest.kt b/core/src/test/kotlin/ApplicationTest.kt index 1e48b55975..ebe6b00864 100644 --- a/core/src/test/kotlin/ApplicationTest.kt +++ b/core/src/test/kotlin/ApplicationTest.kt @@ -23,7 +23,7 @@ import io.ktor.server.application.Application import io.mockk.mockk -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.configureAuthentication +import org.eclipse.apoapsis.ortserver.components.authorization.routes.configureAuthentication import org.eclipse.apoapsis.ortserver.core.plugins.* import org.eclipse.apoapsis.ortserver.core.testutils.configureTestAuthentication import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension diff --git a/core/src/test/kotlin/api/AbstractIntegrationTest.kt b/core/src/test/kotlin/api/AbstractIntegrationTest.kt index 992f0e7829..75400c0b1c 100644 --- a/core/src/test/kotlin/api/AbstractIntegrationTest.kt +++ b/core/src/test/kotlin/api/AbstractIntegrationTest.kt @@ -33,15 +33,15 @@ import kotlinx.serialization.json.Json import org.eclipse.apoapsis.ortserver.clients.keycloak.DefaultKeycloakClient.Companion.configureAuthentication import org.eclipse.apoapsis.ortserver.clients.keycloak.test.KeycloakTestExtension import org.eclipse.apoapsis.ortserver.clients.keycloak.test.TEST_SUBJECT_CLIENT -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.addUserRole import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createJwtConfigMapForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakClientConfigurationForTestRealm -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakClientForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakConfigMapForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpClientScope import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpUser -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpUserRoles -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService import org.eclipse.apoapsis.ortserver.core.SUPERUSER import org.eclipse.apoapsis.ortserver.core.SUPERUSER_PASSWORD import org.eclipse.apoapsis.ortserver.core.TEST_USER @@ -50,28 +50,36 @@ import org.eclipse.apoapsis.ortserver.core.createJsonClient import org.eclipse.apoapsis.ortserver.core.testutils.TestConfig import org.eclipse.apoapsis.ortserver.core.testutils.ortServerTestApplication import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting +import org.eclipse.apoapsis.ortserver.utils.logging.runBlocking @Suppress("UnnecessaryAbstractClass") -abstract class AbstractIntegrationTest(body: AbstractIntegrationTest.() -> Unit) : WordSpec() { +abstract class AbstractIntegrationTest( + body: AbstractIntegrationTest.() -> Unit, + + /** + * A flag indicating whether a new Keycloak realm should be created for each test. This should be set to *true* for + * test classes that manipulate the Keycloak realm during text execution. + */ + createKeycloakRealmPerTest: Boolean = false +) : WordSpec() { val dbExtension = extension(DatabaseTestExtension()) - val keycloak = install(KeycloakTestExtension(createRealmPerTest = true)) { + val keycloak = install(KeycloakTestExtension(createRealmPerTest = createKeycloakRealmPerTest)) { setUpUser(SUPERUSER, SUPERUSER_PASSWORD) - setUpUserRoles(SUPERUSER.username.value, listOf(Superuser.ROLE_NAME)) setUpUser(TEST_USER, TEST_USER_PASSWORD) setUpClientScope(TEST_SUBJECT_CLIENT) } - val keycloakClient = keycloak.createKeycloakClientForTestRealm() - private val keycloakConfig = keycloak.createKeycloakConfigMapForTestRealm() private val jwtConfig = keycloak.createJwtConfigMapForTestRealm() - val secretValue = "secret-value" val secretErrorPath = "error-path" + private lateinit var authorizationService: AuthorizationService + private val secretsConfig = mapOf( "${SecretStorage.CONFIG_PREFIX}.${SecretStorage.NAME_PROPERTY}" to SecretsProviderFactoryForTesting.NAME, "${SecretStorage.CONFIG_PREFIX}.${SecretsProviderFactoryForTesting.ERROR_PATH_PROPERTY}" to secretErrorPath @@ -114,11 +122,28 @@ abstract class AbstractIntegrationTest(body: AbstractIntegrationTest.() -> Unit) body() } - fun integrationTestApplication(block: suspend ApplicationTestBuilder.() -> Unit) = + fun integrationTestApplication(block: suspend ApplicationTestBuilder.() -> Unit) { + authorizationService = setUpAuthorizationService() + ortServerTestApplication(dbExtension.db, TestConfig.TestAuth, additionalConfig, block) + } fun requestShouldRequireRole( - role: String, + role: Role, + hierarchyId: CompoundHierarchyId, + successStatus: HttpStatusCode = HttpStatusCode.OK, + request: suspend HttpClient.() -> HttpResponse + ) { + integrationTestApplication { + val client = testUserClient + + client.request() shouldHaveStatus HttpStatusCode.Forbidden + authorizationService.assignRole(TEST_USER.username.value, role, hierarchyId) + client.request() shouldHaveStatus successStatus + } + } + + fun requestShouldRequireSuperuser( successStatus: HttpStatusCode = HttpStatusCode.OK, request: suspend HttpClient.() -> HttpResponse ) { @@ -126,7 +151,11 @@ abstract class AbstractIntegrationTest(body: AbstractIntegrationTest.() -> Unit) val client = testUserClient client.request() shouldHaveStatus HttpStatusCode.Forbidden - keycloak.keycloakAdminClient.addUserRole(TEST_USER.username.value, role) + authorizationService.assignRole( + TEST_USER.username.value, + OrganizationRole.ADMIN, + CompoundHierarchyId.WILDCARD + ) client.request() shouldHaveStatus successStatus } } @@ -140,4 +169,19 @@ abstract class AbstractIntegrationTest(body: AbstractIntegrationTest.() -> Unit) testUserClient.request() shouldHaveStatus successStatus } } + + /** + * Create a new instance of the [AuthorizationService] used for testing and make sure that the permissions + * required by tests are added. + */ + private fun setUpAuthorizationService(): AuthorizationService = + DbAuthorizationService(dbExtension.db).apply { + runBlocking { + assignRole( + SUPERUSER.username.value, + OrganizationRole.ADMIN, + CompoundHierarchyId.WILDCARD + ) + } + } } diff --git a/core/src/test/kotlin/api/AdminRouteIntegrationTest.kt b/core/src/test/kotlin/api/AdminRouteIntegrationTest.kt index 2f5e6d9874..643c411850 100644 --- a/core/src/test/kotlin/api/AdminRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/AdminRouteIntegrationTest.kt @@ -41,7 +41,6 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.ContentManagementSection import org.eclipse.apoapsis.ortserver.api.v1.model.PatchSection import org.eclipse.apoapsis.ortserver.api.v1.model.PostUser import org.eclipse.apoapsis.ortserver.api.v1.model.User -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser import org.eclipse.apoapsis.ortserver.core.SUPERUSER import org.eclipse.apoapsis.ortserver.core.TEST_USER import org.eclipse.apoapsis.ortserver.utils.test.Integration @@ -56,21 +55,6 @@ class AdminRouteIntegrationTest : AbstractIntegrationTest({ val testPassword = "password123" val testTemporary = true - "GET /admin/sync-roles" should { - "start sync process for permissions and roles" { - integrationTestApplication { - val response = superuserClient.get("/api/v1/admin/sync-roles") - response shouldHaveStatus HttpStatusCode.Accepted - } - } - - "require superuser role" { - requestShouldRequireRole(Superuser.ROLE_NAME, HttpStatusCode.Accepted) { - get("/api/v1/admin/sync-roles") - } - } - } - "GET /admin/users" should { "return a list of users" { integrationTestApplication { @@ -89,7 +73,7 @@ class AdminRouteIntegrationTest : AbstractIntegrationTest({ } "require superuser role" { - requestShouldRequireRole(Superuser.ROLE_NAME, HttpStatusCode.OK) { + requestShouldRequireSuperuser(HttpStatusCode.OK) { get("/api/v1/admin/users") } } @@ -138,7 +122,7 @@ class AdminRouteIntegrationTest : AbstractIntegrationTest({ } "require superuser role" { - requestShouldRequireRole(Superuser.ROLE_NAME, HttpStatusCode.Created) { + requestShouldRequireSuperuser(HttpStatusCode.Created) { post("/api/v1/admin/users") { setBody( PostUser( @@ -180,7 +164,7 @@ class AdminRouteIntegrationTest : AbstractIntegrationTest({ } "require superuser role" { - requestShouldRequireRole(Superuser.ROLE_NAME, HttpStatusCode.NoContent) { + requestShouldRequireSuperuser(HttpStatusCode.NoContent) { delete("/api/v1/admin/users") { parameter("username", TEST_USER.username.value) } @@ -274,4 +258,4 @@ class AdminRouteIntegrationTest : AbstractIntegrationTest({ } } } -}) +}, createKeycloakRealmPerTest = true) diff --git a/core/src/test/kotlin/api/DownloadsRouteIntegrationTest.kt b/core/src/test/kotlin/api/DownloadsRouteIntegrationTest.kt index 9f8099fe58..a8303a7a07 100644 --- a/core/src/test/kotlin/api/DownloadsRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/DownloadsRouteIntegrationTest.kt @@ -30,11 +30,12 @@ import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.http.HttpStatusCode +import io.mockk.mockk + import kotlin.time.Duration.Companion.minutes import kotlinx.datetime.Clock -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.RepositoryType @@ -51,20 +52,11 @@ class DownloadsRouteIntegrationTest : AbstractIntegrationTest({ var repositoryId = -1L beforeEach { - val authorizationService = KeycloakAuthorizationService( - keycloakClient, - dbExtension.db, - dbExtension.fixtures.organizationRepository, - dbExtension.fixtures.productRepository, - dbExtension.fixtures.repositoryRepository, - keycloakGroupPrefix = "" - ) - val organizationService = OrganizationService( dbExtension.db, dbExtension.fixtures.organizationRepository, dbExtension.fixtures.productRepository, - authorizationService + mockk() ) val productService = ProductService( @@ -72,7 +64,7 @@ class DownloadsRouteIntegrationTest : AbstractIntegrationTest({ dbExtension.fixtures.productRepository, dbExtension.fixtures.repositoryRepository, dbExtension.fixtures.ortRunRepository, - authorizationService + mockk() ) val orgId = organizationService.createOrganization(name = "name", description = "description").id diff --git a/core/src/test/kotlin/api/OrganizationsRouteIntegrationTest.kt b/core/src/test/kotlin/api/OrganizationsRouteIntegrationTest.kt index f3fec7f3e6..ab40ec4de9 100644 --- a/core/src/test/kotlin/api/OrganizationsRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/OrganizationsRouteIntegrationTest.kt @@ -23,18 +23,15 @@ import io.kotest.assertions.ktor.client.shouldHaveStatus import io.kotest.data.forAll import io.kotest.data.row import io.kotest.inspectors.forAll -import io.kotest.matchers.collections.containAll -import io.kotest.matchers.collections.containAnyOf import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldContainExactly import io.kotest.matchers.nulls.beNull import io.kotest.matchers.nulls.shouldBeNull -import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNot import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldStartWith @@ -68,21 +65,20 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.UserWithGroups as ApiUserWith import org.eclipse.apoapsis.ortserver.api.v1.model.Username import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityForRunsFilters import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityRating -import org.eclipse.apoapsis.ortserver.clients.keycloak.GroupName -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.addUserRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.api.OrganizationRole as ApiOrganizationRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.mapToModel -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.OrganizationPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.OrganizationRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.ProductRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.api.OrganizationRole as ApiOrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService import org.eclipse.apoapsis.ortserver.core.SUPERUSER import org.eclipse.apoapsis.ortserver.core.TEST_USER +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.model.JobStatus import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.model.RepositoryType import org.eclipse.apoapsis.ortserver.model.Severity import org.eclipse.apoapsis.ortserver.model.runs.Identifier @@ -120,14 +116,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ lateinit var productService: ProductService beforeEach { - authorizationService = KeycloakAuthorizationService( - keycloakClient, - dbExtension.db, - dbExtension.fixtures.organizationRepository, - dbExtension.fixtures.productRepository, - dbExtension.fixtures.repositoryRepository, - keycloakGroupPrefix = "" - ) + authorizationService = DbAuthorizationService(dbExtension.db) organizationService = OrganizationService( dbExtension.db, @@ -180,13 +169,15 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ val org4 = createOrganization(name = "org4") createOrganization(name = "org5") - keycloak.keycloakAdminClient.addUserRole( + authorizationService.assignRole( TEST_USER.username.value, - OrganizationPermission.READ.roleName(org2.id) + OrganizationRole.READER, + CompoundHierarchyId.forOrganization(OrganizationId(org2.id)) ) - keycloak.keycloakAdminClient.addUserRole( + authorizationService.assignRole( TEST_USER.username.value, - OrganizationPermission.READ.roleName(org4.id) + OrganizationRole.READER, + CompoundHierarchyId.forOrganization(OrganizationId(org4.id)) ) val response = testUserClient.get("/api/v1/organizations") @@ -336,7 +327,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "require OrganizationPermission.READ" { val createdOrg = createOrganization() - requestShouldRequireRole(OrganizationPermission.READ.roleName(createdOrg.id)) { + requestShouldRequireRole(OrganizationRole.READER, createdOrg.hierarchyId) { get("/api/v1/organizations/${createdOrg.id}") } } @@ -403,25 +394,6 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } } - "create Keycloak roles and groups" { - integrationTestApplication { - val org = PostOrganization(name = "name", description = "description") - - val createdOrg = superuserClient.post("/api/v1/organizations") { - setBody(org) - }.body() - - keycloakClient.getRoles().map { it.name.value } should containAll( - OrganizationPermission.getRolesForOrganization(createdOrg.id) + - OrganizationRole.getRolesForOrganization(createdOrg.id) - ) - - keycloakClient.getGroups().map { it.name.value } should containAll( - OrganizationRole.getGroupsForOrganization(createdOrg.id) - ) - } - } - "respond with 'Conflict' if the organization already exists" { integrationTestApplication { createOrganization() @@ -435,7 +407,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } "require the superuser role" { - requestShouldRequireRole(Superuser.ROLE_NAME, HttpStatusCode.Created) { + requestShouldRequireSuperuser(HttpStatusCode.Created) { val org = PostOrganization(name = "name", description = "description") post("/api/v1/organizations") { setBody(org) } } @@ -526,7 +498,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "require OrganizationPermission.WRITE" { val createdOrg = createOrganization() - requestShouldRequireRole(OrganizationPermission.WRITE.roleName(createdOrg.id)) { + requestShouldRequireRole(OrganizationRole.WRITER, createdOrg.hierarchyId) { val updateOrg = PatchOrganization("updated".asPresent(), "updated".asPresent()) patch("/api/v1/organizations/${createdOrg.id}") { setBody(updateOrg) } } @@ -545,26 +517,13 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } } - "delete Keycloak roles and groups" { - integrationTestApplication { - val createdOrg = createOrganization() - - superuserClient.delete("/api/v1/organizations/${createdOrg.id}") - - keycloakClient.getRoles().map { it.name.value } shouldNot containAnyOf( - OrganizationPermission.getRolesForOrganization(createdOrg.id) + - OrganizationRole.getRolesForOrganization(createdOrg.id) - ) - - keycloakClient.getGroups().map { it.name.value } shouldNot containAnyOf( - OrganizationRole.getGroupsForOrganization(createdOrg.id) - ) - } - } - "require OrganizationPermission.DELETE" { val createdOrg = createOrganization() - requestShouldRequireRole(OrganizationPermission.DELETE.roleName(createdOrg.id), HttpStatusCode.NoContent) { + requestShouldRequireRole( + OrganizationRole.ADMIN, + createdOrg.hierarchyId, + HttpStatusCode.NoContent + ) { delete("/api/v1/organizations/${createdOrg.id}") } } @@ -606,30 +565,11 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } } - "create Keycloak roles and groups" { - integrationTestApplication { - val orgId = createOrganization().id - - val product = PostProduct(name = "product", description = "description") - val createdProduct = superuserClient.post("/api/v1/organizations/$orgId/products") { - setBody(product) - }.body() - - keycloakClient.getRoles().map { it.name.value } should containAll( - ProductPermission.getRolesForProduct(createdProduct.id) + - ProductRole.getRolesForProduct(createdProduct.id) - ) - - keycloakClient.getGroups().map { it.name.value } should containAll( - ProductRole.getGroupsForProduct(createdProduct.id) - ) - } - } - "require OrganizationPermission.CREATE_PRODUCT" { val createdOrg = createOrganization() requestShouldRequireRole( - OrganizationPermission.CREATE_PRODUCT.roleName(createdOrg.id), + OrganizationRole.WRITER, + createdOrg.hierarchyId, HttpStatusCode.Created ) { val createProduct = PostProduct(name = "product", description = "description") @@ -670,6 +610,77 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } } + "return only products for which the user has ProductPermission.READ" { + integrationTestApplication { + val createdOrganization = createOrganization() + val orgId = createdOrganization.id + val otherOrg = createOrganization("otherOrg") + + val name1 = "name1" + val name2 = "name2" + val description = "description" + + val createdProduct1 = + organizationService.createProduct(name = name1, description = description, organizationId = orgId) + val createdProduct2 = + organizationService.createProduct(name = name2, description = description, organizationId = orgId) + organizationService.createProduct(name = "name3", description = description, organizationId = orgId) + val createdRepo = productService.createRepository( + RepositoryType.GIT, + "https://example.com/repo.git", + createdProduct2.id, + null + ) + val productInOtherOrg = organizationService.createProduct( + name = "otherOrgProduct", + description = description, + organizationId = otherOrg.id + ) + + authorizationService.assignRole( + TEST_USER.username.value, + ProductRole.READER, + CompoundHierarchyId.forProduct( + OrganizationId(createdOrganization.id), + ProductId(createdProduct1.id) + ) + ) + authorizationService.assignRole( + TEST_USER.username.value, + ProductRole.READER, + CompoundHierarchyId.forProduct( + OrganizationId(otherOrg.id), + ProductId(productInOtherOrg.id) + ) + ) + authorizationService.assignRole( + TEST_USER.username.value, + RepositoryRole.WRITER, + CompoundHierarchyId.forRepository( + OrganizationId(createdOrganization.id), + ProductId(createdProduct2.id), + RepositoryId(createdRepo.id) + ) + ) + + val response = testUserClient.get("/api/v1/organizations/$orgId/products") + + response shouldHaveStatus HttpStatusCode.OK + response shouldHaveBody PagedResponse( + listOf( + Product(createdProduct1.id, orgId, name1, description), + Product(createdProduct2.id, orgId, name2, description) + ), + PagingData( + limit = DEFAULT_LIMIT, + offset = 0, + totalCount = 2, + sortProperties = listOf(SortProperty("name", SortDirection.ASCENDING)) + ) + ) + } + } + "support query parameters" { integrationTestApplication { val orgId = createOrganization().id @@ -735,7 +746,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "require OrganizationPermission.READ_PRODUCTS" { val createdOrg = createOrganization() - requestShouldRequireRole(OrganizationPermission.READ_PRODUCTS.roleName(createdOrg.id)) { + requestShouldRequireRole(OrganizationRole.WRITER, createdOrg.hierarchyId) { get("/api/v1/organizations/${createdOrg.id}/products") } } @@ -751,7 +762,8 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ val user = Username(TEST_USER.username.value) requestShouldRequireRole( - OrganizationPermission.MANAGE_GROUPS.roleName(createdOrg.id), + OrganizationRole.ADMIN, + createdOrg.hierarchyId, HttpStatusCode.NoContent ) { when (method) { @@ -792,10 +804,10 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ else -> error("Unsupported method: $method") } - response shouldHaveStatus HttpStatusCode.InternalServerError + response shouldHaveStatus HttpStatusCode.NotFound val body = response.body() - body.cause shouldContain "Could not find user" + body.message shouldContain "Could not find user" } } } @@ -887,6 +899,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "assign the '$role' role to the user" { integrationTestApplication { val createdOrg = createOrganization() + val orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(createdOrg.id)) val user = Username(TEST_USER.username.value) val response = superuserClient.put( @@ -897,14 +910,9 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NoContent - val groupName = role.mapToModel().groupName(createdOrg.id) - val group = keycloakClient.getGroup(GroupName(groupName)) - group.shouldNotBeNull() - - val members = keycloakClient.getGroupMembers(group.name) - members.shouldBeSingleton { - it.username shouldBe TEST_USER.username - } + val members = authorizationService.listUsersWithRole(role.mapToModel(), orgHierarchyId) + members shouldHaveSize 1 + members shouldContain TEST_USER.username.value } } } @@ -915,17 +923,10 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "remove the '$role' role from the user" { integrationTestApplication { val createdOrg = createOrganization() + val orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(createdOrg.id)) val user = Username(TEST_USER.username.value) - authorizationService.addUserRole(user.username, OrganizationId(createdOrg.id), role.mapToModel()) - - // Check pre-condition - val groupName = role.mapToModel().groupName(createdOrg.id) - val groupBefore = keycloakClient.getGroup(GroupName(groupName)) - val membersBefore = keycloakClient.getGroupMembers(groupBefore.name) - membersBefore.shouldBeSingleton { - it.username shouldBe TEST_USER.username - } + authorizationService.assignRole(user.username, role.mapToModel(), orgHierarchyId) val response = superuserClient.delete( "/api/v1/organizations/${createdOrg.id}/roles/${role.name}?username=${user.username}" @@ -933,10 +934,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NoContent - val groupAfter = keycloakClient.getGroup(GroupName(groupName)) - groupAfter.shouldNotBeNull() - - val membersAfter = keycloakClient.getGroupMembers(groupAfter.name) + val membersAfter = authorizationService.listUsersWithRole(role.mapToModel(), orgHierarchyId) membersAfter.shouldBeEmpty() } } @@ -1518,7 +1516,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "require OrganizationPermission.READ" { val createdOrganization = createOrganization() - requestShouldRequireRole(OrganizationPermission.READ.roleName(createdOrganization.id)) { + requestShouldRequireRole(OrganizationRole.READER, createdOrganization.hierarchyId) { get("/api/v1/organizations/${createdOrganization.id}/vulnerabilities") } } @@ -1733,7 +1731,7 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "require OrganizationPermission.READ" { val createdOrganization = createOrganization() - requestShouldRequireRole(OrganizationPermission.READ.roleName(createdOrganization.id)) { + requestShouldRequireRole(OrganizationRole.READER, createdOrganization.hierarchyId) { get("/api/v1/organizations/${createdOrganization.id}/statistics/runs") } } @@ -1743,21 +1741,17 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ "return list of users that have rights for organization" { integrationTestApplication { val orgId = createOrganization().id + val orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(orgId)) - authorizationService.addUserRole( + authorizationService.assignRole( TEST_USER.username.value, - OrganizationId(orgId), - OrganizationRole.READER + OrganizationRole.READER, + orgHierarchyId ) - authorizationService.addUserRole( + authorizationService.assignRole( SUPERUSER.username.value, - OrganizationId(orgId), - OrganizationRole.WRITER - ) - authorizationService.addUserRole( - SUPERUSER.username.value, - OrganizationId(orgId), - OrganizationRole.ADMIN + OrganizationRole.ADMIN, + orgHierarchyId ) val response = superuserClient.get("/api/v1/organizations/$orgId/users") @@ -1784,6 +1778,42 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } } + "handle unknown users gracefully" { + integrationTestApplication { + val orgId = createOrganization().id + val orgHierarchyId = CompoundHierarchyId.forOrganization(OrganizationId(orgId)) + + authorizationService.assignRole( + TEST_USER.username.value, + OrganizationRole.READER, + orgHierarchyId + ) + authorizationService.assignRole( + "non-existing-user", + OrganizationRole.ADMIN, + orgHierarchyId + ) + + val response = superuserClient.get("/api/v1/organizations/$orgId/users") + + response shouldHaveStatus HttpStatusCode.OK + response shouldHaveBody PagedResponse( + listOf( + ApiUserWithGroups( + ApiUser(TEST_USER.username.value, TEST_USER.firstName, TEST_USER.lastName, TEST_USER.email), + listOf(ApiUserGroup.READERS) + ) + ), + PagingData( + limit = DEFAULT_LIMIT, + offset = 0, + totalCount = 1, + sortProperties = listOf(SortProperty("username", SortDirection.ASCENDING)) + ) + ) + } + } + "return empty list if no user has rights for organizations" { integrationTestApplication { val orgId = createOrganization().id @@ -1832,10 +1862,10 @@ class OrganizationsRouteIntegrationTest : AbstractIntegrationTest({ } "require OrganizationPermission.READ" { - val orgId = createOrganization().id + val createdOrg = createOrganization() - requestShouldRequireRole(OrganizationPermission.READ.roleName(orgId)) { - get("/api/v1/organizations/$orgId/users") + requestShouldRequireRole(OrganizationRole.READER, createdOrg.hierarchyId) { + get("/api/v1/organizations/${createdOrg.id}/users") } } } @@ -1850,3 +1880,9 @@ private fun generateAdvisorResult(vulnerabilities: List) = Adviso defects = emptyList(), vulnerabilities = vulnerabilities ) + +/** + * Return a [CompoundHierarchyId] for this [Organization]. + */ +private val org.eclipse.apoapsis.ortserver.model.Organization.hierarchyId: CompoundHierarchyId + get() = CompoundHierarchyId.forOrganization(OrganizationId(id)) diff --git a/core/src/test/kotlin/api/ProductsRouteIntegrationTest.kt b/core/src/test/kotlin/api/ProductsRouteIntegrationTest.kt index fcbe95315e..a73b4c5e71 100644 --- a/core/src/test/kotlin/api/ProductsRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/ProductsRouteIntegrationTest.kt @@ -23,18 +23,14 @@ import io.kotest.assertions.ktor.client.shouldHaveStatus import io.kotest.data.forAll import io.kotest.data.row import io.kotest.inspectors.forAll -import io.kotest.matchers.collections.containAll -import io.kotest.matchers.collections.containAnyOf import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldContainExactly import io.kotest.matchers.nulls.beNull -import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNot import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContainIgnoringCase @@ -49,6 +45,8 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode +import io.mockk.mockk + import kotlinx.datetime.Clock import org.eclipse.apoapsis.ortserver.api.v1.mapping.mapToApi @@ -81,24 +79,23 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.UserWithGroups as ApiUserWith import org.eclipse.apoapsis.ortserver.api.v1.model.Username import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityForRunsFilters import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityRating -import org.eclipse.apoapsis.ortserver.clients.keycloak.GroupName -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.addUserRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.api.ProductRole as ApiProductRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.mapToModel -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.ProductPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.ProductRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.RepositoryRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.api.ProductRole as ApiProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionTemplate import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.core.SUPERUSER import org.eclipse.apoapsis.ortserver.core.TEST_USER +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.model.JobStatus +import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.model.RepositoryType import org.eclipse.apoapsis.ortserver.model.Severity import org.eclipse.apoapsis.ortserver.model.runs.Identifier @@ -138,20 +135,13 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ var orgId = -1L beforeEach { - authorizationService = KeycloakAuthorizationService( - keycloakClient, - dbExtension.db, - dbExtension.fixtures.organizationRepository, - dbExtension.fixtures.productRepository, - dbExtension.fixtures.repositoryRepository, - keycloakGroupPrefix = "" - ) + authorizationService = DbAuthorizationService(dbExtension.db) organizationService = OrganizationService( dbExtension.db, dbExtension.fixtures.organizationRepository, dbExtension.fixtures.productRepository, - authorizationService + mockk() ) pluginService = PluginService(dbExtension.db) @@ -176,6 +166,9 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ organizationId: Long = orgId ) = organizationService.createProduct(name, description, organizationId) + fun org.eclipse.apoapsis.ortserver.model.Product.hierarchyId(organizationId: Long = orgId): CompoundHierarchyId = + CompoundHierarchyId.forProduct(OrganizationId(organizationId), ProductId(id)) + "GET /products/{productId}" should { "return a single product" { integrationTestApplication { @@ -190,7 +183,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "require ProductPermission.READ" { val createdProduct = createProduct() - requestShouldRequireRole(ProductPermission.READ.roleName(createdProduct.id)) { + requestShouldRequireRole(ProductRole.READER, createdProduct.hierarchyId()) { get("/api/v1/products/${createdProduct.id}") } } @@ -221,7 +214,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "require ProductPermission.WRITE" { val createdProduct = createProduct() - requestShouldRequireRole(ProductPermission.WRITE.roleName(createdProduct.id)) { + requestShouldRequireRole(ProductRole.WRITER, createdProduct.hierarchyId()) { val updatedProduct = PatchProduct("updatedName".asPresent(), "updatedDescription".asPresent()) patch("/api/v1/products/${createdProduct.id}") { setBody(updatedProduct) } } @@ -260,26 +253,9 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ } } - "delete Keycloak roles and groups" { - integrationTestApplication { - val createdProduct = createProduct() - - superuserClient.delete("/api/v1/products/${createdProduct.id}") - - keycloakClient.getRoles().map { it.name.value } shouldNot containAnyOf( - ProductPermission.getRolesForProduct(createdProduct.id) + - ProductRole.getRolesForProduct(createdProduct.id) - ) - - keycloakClient.getGroups().map { it.name.value } shouldNot containAnyOf( - ProductRole.getGroupsForProduct(createdProduct.id) - ) - } - } - "require ProductPermission.DELETE" { val createdProduct = createProduct() - requestShouldRequireRole(ProductPermission.DELETE.roleName(createdProduct.id), HttpStatusCode.NoContent) { + requestShouldRequireRole(ProductRole.ADMIN, createdProduct.hierarchyId(), HttpStatusCode.NoContent) { delete("/api/v1/products/${createdProduct.id}") } } @@ -326,6 +302,71 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ } } + "return the repositories a user has access to" { + integrationTestApplication { + val createdProduct = createProduct() + + val type = RepositoryType.GIT + val url1 = "https://example.com/repo1.git" + val url2 = "https://example.com/repo2.git" + val description = "description" + + val createdRepository1 = productService.createRepository( + type = type, + url = url1, + productId = createdProduct.id, + description = description + ) + val createdRepository2 = productService.createRepository( + type = type, + url = url2, + productId = createdProduct.id, + description = description + ) + productService.createRepository( + type = type, + url = "https://example.com/hidden-repo.git", + productId = createdProduct.id, + description = "You cannot see me" + ) + + authorizationService.assignRole( + TEST_USER.username.value, + RepositoryRole.READER, + CompoundHierarchyId.forRepository( + OrganizationId(createdProduct.organizationId), + ProductId(createdProduct.id), + RepositoryId(createdRepository1.id) + ) + ) + authorizationService.assignRole( + TEST_USER.username.value, + RepositoryRole.READER, + CompoundHierarchyId.forRepository( + OrganizationId(createdProduct.organizationId), + ProductId(createdProduct.id), + RepositoryId(createdRepository2.id) + ) + ) + + val response = testUserClient.get("/api/v1/products/${createdProduct.id}/repositories") + + response shouldHaveStatus HttpStatusCode.OK + response shouldHaveBody PagedResponse( + listOf( + Repository(createdRepository1.id, orgId, createdProduct.id, type.mapToApi(), url1, description), + Repository(createdRepository2.id, orgId, createdProduct.id, type.mapToApi(), url2, description) + ), + PagingData( + limit = DEFAULT_LIMIT, + offset = 0, + totalCount = 2, + sortProperties = listOf(SortProperty("url", SortDirection.ASCENDING)) + ) + ) + } + } + "support query parameters" { integrationTestApplication { val createdProduct = createProduct() @@ -375,7 +416,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "require ProductPermission.READ_REPOSITORIES" { val createdProduct = createProduct() - requestShouldRequireRole(ProductPermission.READ_REPOSITORIES.roleName(createdProduct.id)) { + requestShouldRequireRole(ProductRole.WRITER, createdProduct.hierarchyId()) { get("/api/v1/products/${createdProduct.id}/repositories") } } @@ -431,30 +472,11 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ } } - "create Keycloak roles and groups" { - integrationTestApplication { - val createdProduct = createProduct() - - val repository = PostRepository(ApiRepositoryType.GIT, "https://example.com/repo.git") - val createdRepository = superuserClient.post("/api/v1/products/${createdProduct.id}/repositories") { - setBody(repository) - }.body() - - keycloakClient.getRoles().map { it.name.value } should containAll( - RepositoryPermission.getRolesForRepository(createdRepository.id) + - RepositoryRole.getRolesForRepository(createdRepository.id) - ) - - keycloakClient.getGroups().map { it.name.value } should containAll( - RepositoryRole.getGroupsForRepository(createdRepository.id) - ) - } - } - "require ProductPermission.CREATE_REPOSITORY" { val createdProduct = createProduct() requestShouldRequireRole( - ProductPermission.CREATE_REPOSITORY.roleName(createdProduct.id), + ProductRole.WRITER, + createdProduct.hierarchyId(), HttpStatusCode.Created ) { val repository = PostRepository(ApiRepositoryType.GIT, "https://example.com/repo.git") @@ -472,7 +494,8 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ val createdProd = createProduct() val user = Username(TEST_USER.username.value) requestShouldRequireRole( - ProductPermission.MANAGE_GROUPS.roleName(createdProd.id), + ProductRole.ADMIN, + createdProd.hierarchyId(), HttpStatusCode.NoContent ) { when (method) { @@ -508,10 +531,10 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ else -> error("Unsupported method: $method") } - response shouldHaveStatus HttpStatusCode.InternalServerError + response shouldHaveStatus HttpStatusCode.NotFound val body = response.body() - body.cause shouldContain "Could not find user" + body.message shouldContain "Could not find user" } } } @@ -539,7 +562,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NotFound val body = response.body() - body.message shouldContain "not found" + body.message shouldContain "Could not resolve hierarchy ID" } } } @@ -608,14 +631,15 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NoContent - val groupName = role.mapToModel().groupName(createdProd.id) - val group = keycloakClient.getGroup(GroupName(groupName)) - group.shouldNotBeNull() - - val members = keycloakClient.getGroupMembers(group.name) - members.shouldBeSingleton { - it.username shouldBe TEST_USER.username - } + val members = authorizationService.listUsersWithRole( + role.mapToModel(), + CompoundHierarchyId.forProduct( + OrganizationId(createdProd.organizationId), + ProductId(createdProd.id) + ) + ) + members shouldHaveSize 1 + members shouldContain TEST_USER.username.value } } } @@ -626,17 +650,13 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "remove the '$role' role from the user" { integrationTestApplication { val createdProd = createProduct() + val productHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(createdProd.organizationId), + ProductId(createdProd.id) + ) val user = Username(TEST_USER.username.value) - authorizationService.addUserRole(user.username, ProductId(createdProd.id), role.mapToModel()) - - // Check pre-condition - val groupName = role.mapToModel().groupName(createdProd.id) - val groupBefore = keycloakClient.getGroup(GroupName(groupName)) - val membersBefore = keycloakClient.getGroupMembers(groupBefore.name) - membersBefore.shouldBeSingleton { - it.username shouldBe TEST_USER.username - } + authorizationService.assignRole(user.username, role.mapToModel(), productHierarchyId) val response = superuserClient.delete( "/api/v1/products/${createdProd.id}/roles/${role.name}?username=${user.username}" @@ -644,10 +664,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NoContent - val groupAfter = keycloakClient.getGroup(GroupName(groupName)) - groupAfter.shouldNotBeNull() - - val membersAfter = keycloakClient.getGroupMembers(groupAfter.name) + val membersAfter = authorizationService.listUsersWithRole(role.mapToModel(), productHierarchyId) membersAfter.shouldBeEmpty() } } @@ -1124,7 +1141,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "require ProductPermission.READ" { val createdProduct = createProduct() - requestShouldRequireRole(ProductPermission.READ.roleName(createdProduct.id)) { + requestShouldRequireRole(ProductRole.READER, createdProduct.hierarchyId()) { get("/api/v1/products/${createdProduct.id}/vulnerabilities") } } @@ -1421,7 +1438,7 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "require ProductPermission.READ" { val createdProduct = createProduct() - requestShouldRequireRole(ProductPermission.READ.roleName(createdProduct.id)) { + requestShouldRequireRole(ProductRole.READER, createdProduct.hierarchyId()) { get("/api/v1/products/${createdProduct.id}/statistics/runs") } } @@ -1430,11 +1447,15 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "GET /products/{productId}/users" should { "return list of users that have rights for product" { integrationTestApplication { - val productId = createProduct().id + val product = createProduct() + val productId = product.id + val productHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(product.organizationId), + ProductId(productId) + ) - authorizationService.addUserRole(TEST_USER.username.value, ProductId(productId), ProductRole.READER) - authorizationService.addUserRole(SUPERUSER.username.value, ProductId(productId), ProductRole.WRITER) - authorizationService.addUserRole(SUPERUSER.username.value, ProductId(productId), ProductRole.ADMIN) + authorizationService.assignRole(TEST_USER.username.value, ProductRole.READER, productHierarchyId) + authorizationService.assignRole(SUPERUSER.username.value, ProductRole.ADMIN, productHierarchyId) val response = superuserClient.get("/api/v1/products/$productId/users") @@ -1507,10 +1528,10 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ } "require ProductPermission.READ" { - val productId = createProduct().id + val createProduct = createProduct() - requestShouldRequireRole(ProductPermission.READ.roleName(productId)) { - get("/api/v1/products/$productId/users") + requestShouldRequireRole(ProductRole.READER, createProduct.hierarchyId()) { + get("/api/v1/products/${createProduct.id}/users") } } } @@ -1574,9 +1595,12 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ responseSpecific shouldHaveStatus HttpStatusCode.Created val createdRunsSpecific = responseSpecific.body>() - createdRunsSpecific.shouldBeSingleton { - dbExtension.fixtures.ortRunRepository.get(it.id)?.repositoryId shouldBe repository1Id + createdRunsSpecific shouldHaveSize 1 + + val repositoryIdsSpecific = createdRunsSpecific.map { run -> + dbExtension.fixtures.ortRunRepository.get(run.id)?.repositoryId } + repositoryIdsSpecific shouldContainExactlyInAnyOrder listOf(repository1Id) } } @@ -1761,13 +1785,15 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "respond with 'Forbidden' if 'keepAliveWorker' is set by a non super-user" { integrationTestApplication { - val productId = createProduct().id - - keycloak.keycloakAdminClient.addUserRole( - TEST_USER.username.value, - ProductPermission.TRIGGER_ORT_RUN.roleName(productId) + val product = createProduct() + val productId = product.id + val productHierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(product.organizationId), + ProductId(productId) ) + authorizationService.assignRole(TEST_USER.username.value, ProductRole.WRITER, productHierarchyId) + keepAliveJobConfigs.forAll { val response = testUserClient.post("/api/v1/products/$productId/runs") { setBody(PostRepositoryRun(revision = "main", jobConfigs = it)) @@ -1931,7 +1957,8 @@ class ProductsRouteIntegrationTest : AbstractIntegrationTest({ "require ProductPermission.TRIGGER_ORT_RUN" { val createdProduct = createProduct() requestShouldRequireRole( - ProductPermission.TRIGGER_ORT_RUN.roleName(createdProduct.id), + ProductRole.WRITER, + createdProduct.hierarchyId(), HttpStatusCode.Created ) { val createOrtRun = PostRepositoryRun( diff --git a/core/src/test/kotlin/api/RepositoriesRouteIntegrationTest.kt b/core/src/test/kotlin/api/RepositoriesRouteIntegrationTest.kt index 596c1eb3fb..d1cc4214f9 100644 --- a/core/src/test/kotlin/api/RepositoriesRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/RepositoriesRouteIntegrationTest.kt @@ -23,14 +23,13 @@ import io.kotest.assertions.ktor.client.shouldHaveStatus import io.kotest.data.forAll import io.kotest.data.row import io.kotest.inspectors.forAll -import io.kotest.matchers.collections.containAnyOf import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.beNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNot import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContainIgnoringCase @@ -45,6 +44,8 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode +import io.mockk.mockk + import java.util.EnumSet import kotlinx.coroutines.Dispatchers @@ -77,23 +78,23 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.User as ApiUser import org.eclipse.apoapsis.ortserver.api.v1.model.UserGroup as ApiUserGroup import org.eclipse.apoapsis.ortserver.api.v1.model.UserWithGroups as ApiUserWithGroups import org.eclipse.apoapsis.ortserver.api.v1.model.Username -import org.eclipse.apoapsis.ortserver.clients.keycloak.GroupName -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.addUserRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.api.RepositoryRole as ApiRepositoryRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.mapToModel -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.RepositoryRole -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.api.RepositoryRole as ApiRepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.mapToModel +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginOptionTemplate import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginService import org.eclipse.apoapsis.ortserver.components.pluginmanager.PluginType import org.eclipse.apoapsis.ortserver.core.SUPERUSER import org.eclipse.apoapsis.ortserver.core.TEST_USER +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.model.CredentialsType import org.eclipse.apoapsis.ortserver.model.EnvironmentVariableDeclaration import org.eclipse.apoapsis.ortserver.model.InfrastructureServiceDeclaration import org.eclipse.apoapsis.ortserver.model.JobConfigurations +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.model.RepositoryType import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository @@ -127,20 +128,13 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ var productId = -1L beforeEach { - authorizationService = KeycloakAuthorizationService( - keycloakClient, - dbExtension.db, - dbExtension.fixtures.organizationRepository, - dbExtension.fixtures.productRepository, - dbExtension.fixtures.repositoryRepository, - keycloakGroupPrefix = "" - ) + authorizationService = DbAuthorizationService(dbExtension.db) val organizationService = OrganizationService( dbExtension.db, dbExtension.fixtures.organizationRepository, dbExtension.fixtures.productRepository, - authorizationService + mockk() ) pluginService = PluginService(dbExtension.db) @@ -150,7 +144,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ dbExtension.fixtures.productRepository, dbExtension.fixtures.repositoryRepository, dbExtension.fixtures.ortRunRepository, - authorizationService + mockk() ) ortRunRepository = dbExtension.fixtures.ortRunRepository @@ -175,6 +169,13 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ fun createJobSummaries(ortRunId: Long) = dbExtension.fixtures.createJobs(ortRunId).mapToApiSummary() + fun org.eclipse.apoapsis.ortserver.model.Repository.hierarchyId(prodId: Long = productId): CompoundHierarchyId = + CompoundHierarchyId.forRepository( + OrganizationId(orgId), + ProductId(prodId), + RepositoryId(id) + ) + "GET /repositories/{repositoryId}" should { "return a single repository" { integrationTestApplication { @@ -197,7 +198,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ "require RepositoryPermission.READ" { val createdRepository = createRepository() - requestShouldRequireRole(RepositoryPermission.READ.roleName(createdRepository.id)) { + requestShouldRequireRole(RepositoryRole.READER, createdRepository.hierarchyId()) { get("/api/v1/repositories/${createdRepository.id}") } } @@ -274,7 +275,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ "require RepositoryPermission.WRITE" { val createdRepository = createRepository() - requestShouldRequireRole(RepositoryPermission.WRITE.roleName(createdRepository.id)) { + requestShouldRequireRole(RepositoryRole.WRITER, createdRepository.hierarchyId()) { val updateRepository = PatchRepository( ApiRepositoryType.SUBVERSION.asPresent(), "https://svn.example.com/repos/org/repo/trunk".asPresent() @@ -296,27 +297,11 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ } } - "delete Keycloak roles and groups" { - integrationTestApplication { - val createdRepository = createRepository() - - superuserClient.delete("/api/v1/repositories/${createdRepository.id}") - - keycloakClient.getRoles().map { it.name.value } shouldNot containAnyOf( - RepositoryPermission.getRolesForRepository(createdRepository.id) + - RepositoryRole.getRolesForRepository(createdRepository.id) - ) - - keycloakClient.getGroups().map { it.name.value } shouldNot containAnyOf( - RepositoryRole.getGroupsForRepository(createdRepository.id) - ) - } - } - "require RepositoryPermission.DELETE" { val createdRepository = createRepository() requestShouldRequireRole( - RepositoryPermission.DELETE.roleName(createdRepository.id), + RepositoryRole.ADMIN, + createdRepository.hierarchyId(), HttpStatusCode.NoContent ) { delete("/api/v1/repositories/${createdRepository.id}") @@ -455,7 +440,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ "require RepositoryPermission.READ_ORT_RUNS" { val createdRepository = createRepository() - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(createdRepository.id)) { + requestShouldRequireRole(RepositoryRole.READER, createdRepository.hierarchyId()) { get("/api/v1/repositories/${createdRepository.id}/runs") } } @@ -522,7 +507,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ null ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(createdRepository.id)) { + requestShouldRequireRole(RepositoryRole.READER, createdRepository.hierarchyId()) { get("/api/v1/repositories/${createdRepository.id}/runs/${run.index}") } } @@ -544,7 +529,8 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ ) requestShouldRequireRole( - RepositoryPermission.DELETE.roleName(createdRepository.id), + RepositoryRole.ADMIN, + createdRepository.hierarchyId(), HttpStatusCode.NoContent ) { delete("/api/v1/repositories/${createdRepository.id}/runs/${run.index}") @@ -681,7 +667,8 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ "require RepositoryPermission.TRIGGER_ORT_RUN" { val createdRepository = createRepository() requestShouldRequireRole( - RepositoryPermission.TRIGGER_ORT_RUN.roleName(createdRepository.id), + RepositoryRole.WRITER, + createdRepository.hierarchyId(), HttpStatusCode.Created ) { val createRun = PostRepositoryRun("main", null, ApiJobConfigurations(), labelsMap) @@ -917,15 +904,16 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ "respond with 'Forbidden' if 'keepAliveWorker' is set by a non super-user" { integrationTestApplication { - val repositoryId = createRepository().id + val repository = createRepository() - keycloak.keycloakAdminClient.addUserRole( + authorizationService.assignRole( TEST_USER.username.value, - RepositoryPermission.TRIGGER_ORT_RUN.roleName(repositoryId) + RepositoryRole.WRITER, + repository.hierarchyId() ) keepAliveJobConfigs.forAll { - val response = testUserClient.post("/api/v1/repositories/$repositoryId/runs") { + val response = testUserClient.post("/api/v1/repositories/${repository.id}/runs") { setBody(PostRepositoryRun(revision = "main", jobConfigs = it)) } @@ -959,7 +947,8 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ val createdRepo = createRepository() val user = Username(TEST_USER.username.value) requestShouldRequireRole( - RepositoryPermission.MANAGE_GROUPS.roleName(createdRepo.id), + RepositoryRole.ADMIN, + createdRepo.hierarchyId(), HttpStatusCode.NoContent ) { when (method) { @@ -995,10 +984,10 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ else -> error("Unsupported method: $method") } - response shouldHaveStatus HttpStatusCode.InternalServerError + response shouldHaveStatus HttpStatusCode.NotFound val body = response.body() - body.cause shouldContain "Could not find user" + body.message shouldContain "Could not find user" } } } @@ -1026,7 +1015,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NotFound val body = response.body() - body.message shouldContain "not found" + body.message shouldContain "Could not resolve hierarchy ID" } } } @@ -1097,14 +1086,9 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NoContent - val groupName = role.mapToModel().groupName(createdRepo.id) - val group = keycloakClient.getGroup(GroupName(groupName)) - group.shouldNotBeNull() - - val members = keycloakClient.getGroupMembers(group.name) - members.shouldBeSingleton { - it.username shouldBe TEST_USER.username - } + val members = authorizationService.listUsersWithRole(role.mapToModel(), createdRepo.hierarchyId()) + members shouldHaveSize 1 + members shouldContain TEST_USER.username.value } } } @@ -1117,15 +1101,7 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ val createdRepo = createRepository() val user = Username(TEST_USER.username.value) - authorizationService.addUserRole(user.username, RepositoryId(createdRepo.id), role.mapToModel()) - - // Check pre-condition - val groupName = role.mapToModel().groupName(createdRepo.id) - val groupBefore = keycloakClient.getGroup(GroupName(groupName)) - val membersBefore = keycloakClient.getGroupMembers(groupBefore.name) - membersBefore.shouldBeSingleton { - it.username shouldBe TEST_USER.username - } + authorizationService.assignRole(user.username, role.mapToModel(), createdRepo.hierarchyId()) val response = superuserClient.delete( "/api/v1/repositories/${createdRepo.id}/roles/${role.name}?username=${user.username}" @@ -1133,10 +1109,10 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ response shouldHaveStatus HttpStatusCode.NoContent - val groupAfter = keycloakClient.getGroup(GroupName(groupName)) - groupAfter.shouldNotBeNull() - - val membersAfter = keycloakClient.getGroupMembers(groupAfter.name) + val membersAfter = authorizationService.listUsersWithRole( + role.mapToModel(), + createdRepo.hierarchyId() + ) membersAfter.shouldBeEmpty() } } @@ -1146,25 +1122,20 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ "GET /repositories/{repositoryId}/users" should { "return list of users that have rights for repository" { integrationTestApplication { - val repositoryId = createRepository().id + val repository = createRepository() - authorizationService.addUserRole( + authorizationService.assignRole( TEST_USER.username.value, - RepositoryId(repositoryId), - RepositoryRole.READER + RepositoryRole.READER, + repository.hierarchyId() ) - authorizationService.addUserRole( + authorizationService.assignRole( SUPERUSER.username.value, - RepositoryId(repositoryId), - RepositoryRole.WRITER - ) - authorizationService.addUserRole( - SUPERUSER.username.value, - RepositoryId(repositoryId), - RepositoryRole.ADMIN + RepositoryRole.ADMIN, + repository.hierarchyId() ) - val response = superuserClient.get("/api/v1/repositories/$repositoryId/users") + val response = superuserClient.get("/api/v1/repositories/${repository.id}/users") response shouldHaveStatus HttpStatusCode.OK response shouldHaveBody PagedResponse( @@ -1236,10 +1207,10 @@ class RepositoriesRouteIntegrationTest : AbstractIntegrationTest({ } "require RepositoryPermission.READ" { - val repositoryId = createRepository().id + val createRepository = createRepository() - requestShouldRequireRole(RepositoryPermission.READ.roleName(repositoryId)) { - get("/api/v1/repositories/$repositoryId/users") + requestShouldRequireRole(RepositoryRole.READER, createRepository.hierarchyId()) { + get("/api/v1/repositories/${createRepository.id}/users") } } } diff --git a/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt b/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt index 125ff30beb..fb37224ecd 100644 --- a/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt @@ -50,6 +50,8 @@ import io.ktor.http.isSuccess import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.jvm.javaio.copyTo +import io.mockk.mockk + import java.io.File import java.io.IOException import java.util.EnumSet @@ -84,23 +86,25 @@ import org.eclipse.apoapsis.ortserver.api.v1.model.RuleViolation as ApiRuleViola import org.eclipse.apoapsis.ortserver.api.v1.model.Severity as ApiSeverity import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityRating import org.eclipse.apoapsis.ortserver.api.v1.model.VulnerabilityWithDetails -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.dao.utils.toDatabasePrecision import org.eclipse.apoapsis.ortserver.logaccess.LogFileCriteria import org.eclipse.apoapsis.ortserver.logaccess.LogFileProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.model.AdvisorJobConfiguration import org.eclipse.apoapsis.ortserver.model.AnalyzerJobConfiguration +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.model.EvaluatorJobConfiguration import org.eclipse.apoapsis.ortserver.model.JobConfigurations import org.eclipse.apoapsis.ortserver.model.JobStatus import org.eclipse.apoapsis.ortserver.model.LogLevel import org.eclipse.apoapsis.ortserver.model.LogSource +import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.model.RepositoryType import org.eclipse.apoapsis.ortserver.model.Severity import org.eclipse.apoapsis.ortserver.model.UserDisplayName @@ -148,22 +152,14 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ lateinit var productService: ProductService var repositoryId = -1L + lateinit var hierarchyId: CompoundHierarchyId beforeEach { - val authorizationService = KeycloakAuthorizationService( - keycloakClient, - dbExtension.db, - dbExtension.fixtures.organizationRepository, - dbExtension.fixtures.productRepository, - dbExtension.fixtures.repositoryRepository, - keycloakGroupPrefix = "" - ) - organizationService = OrganizationService( dbExtension.db, dbExtension.fixtures.organizationRepository, dbExtension.fixtures.productRepository, - authorizationService + mockk() ) productService = ProductService( @@ -171,7 +167,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ dbExtension.fixtures.productRepository, dbExtension.fixtures.repositoryRepository, dbExtension.fixtures.ortRunRepository, - authorizationService + mockk() ) ortRunRepository = dbExtension.fixtures.ortRunRepository @@ -185,6 +181,11 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ productId = productId, description = "description" ).id + hierarchyId = CompoundHierarchyId.forRepository( + OrganizationId(orgId), + ProductId(productId), + RepositoryId(repositoryId) + ) LogFileProviderFactoryForTesting.reset() } @@ -340,7 +341,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ null ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${run.id}") } } @@ -434,7 +435,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ null ) - requestShouldRequireRole(RepositoryPermission.DELETE.roleName(repositoryId), HttpStatusCode.NoContent) { + requestShouldRequireRole(RepositoryRole.ADMIN, hierarchyId, HttpStatusCode.NoContent) { delete("/api/v1/runs/${run.id}") } } @@ -574,7 +575,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ "require RepositoryPermission.READ_ORT_RUNS" { val run = prepareLogTest(EnumSet.of(LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO)) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${run.id}/logs") } } @@ -614,7 +615,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ "require RepositoryPermission.READ_ORT_RUNS" { val run = createReport() - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${run.id}/reporter/$reportFile") } } @@ -823,7 +824,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ null ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${run.id}/vulnerabilities") } } @@ -837,7 +838,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ jobConfigurations = JobConfigurations() ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${ortRun.id}/issues") } } @@ -1379,7 +1380,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ null ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${run.id}/packages") } } @@ -1483,7 +1484,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ null ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${run.id}/projects") } } @@ -1673,7 +1674,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ } "require superuser role" { - requestShouldRequireRole(Superuser.ROLE_NAME) { + requestShouldRequireSuperuser { get("/api/v1/runs") } } @@ -1687,7 +1688,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ jobConfigurations = JobConfigurations() ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${ortRun.id}/rule-violations") } } @@ -2198,7 +2199,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ jobConfigurations = JobConfigurations() ) - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/${ortRun.id}/statistics") } } @@ -2266,7 +2267,7 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ jobConfigurations = JobConfigurations() ).id - requestShouldRequireRole(RepositoryPermission.READ_ORT_RUNS.roleName(repositoryId)) { + requestShouldRequireRole(RepositoryRole.READER, hierarchyId) { get("/api/v1/runs/$ortRunId/packages/licenses") } } diff --git a/core/src/test/kotlin/auth/AuthenticationIntegrationTest.kt b/core/src/test/kotlin/auth/AuthenticationIntegrationTest.kt index ccf444c5f7..86bff6a20e 100644 --- a/core/src/test/kotlin/auth/AuthenticationIntegrationTest.kt +++ b/core/src/test/kotlin/auth/AuthenticationIntegrationTest.kt @@ -22,9 +22,8 @@ package org.eclipse.apoapsis.ortserver.components.authorization import io.kotest.assertions.ktor.client.shouldHaveStatus import io.kotest.core.extensions.install import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.containExactlyInAnyOrder import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.should +import io.kotest.matchers.shouldBe import io.ktor.client.request.get import io.ktor.http.HttpStatusCode @@ -47,13 +46,13 @@ import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakClient import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakConfigMapForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpClientScope import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpUser -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpUserRoles -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.SecurityConfigurations +import org.eclipse.apoapsis.ortserver.components.authorization.routes.AuthenticationProviders +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal import org.eclipse.apoapsis.ortserver.core.TEST_USER import org.eclipse.apoapsis.ortserver.core.TEST_USER_PASSWORD import org.eclipse.apoapsis.ortserver.core.testutils.TestConfig import org.eclipse.apoapsis.ortserver.core.testutils.ortServerTestApplication +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.utils.test.Integration class AuthenticationIntegrationTest : StringSpec({ @@ -81,7 +80,7 @@ class AuthenticationIntegrationTest : StringSpec({ ortServerTestApplication(config = TestConfig.TestAuth, additionalConfigs = keycloakConfig + jwtConfig) { routing { route("api/v1") { - authenticate(SecurityConfigurations.TOKEN) { + authenticate(AuthenticationProviders.TOKEN_PROVIDER) { route("test") { get { onCall() @@ -131,19 +130,20 @@ class AuthenticationIntegrationTest : StringSpec({ } } - "A principal with the correct client roles should be created" { + "A principal with correct properties should be created" { keycloak.keycloakAdminClient.setUpClientScope(TEST_SUBJECT_CLIENT) - keycloak.keycloakAdminClient.setUpUserRoles(TEST_USER.username.value, listOf("role-1", "role-2")) authTestApplication(onCall = { - val principal = call.principal(SecurityConfigurations.TOKEN) + val principal = call.principal(AuthenticationProviders.TOKEN_PROVIDER) principal.shouldNotBeNull() - principal.roles should containExactlyInAnyOrder("role-1", "role-2") + principal.username shouldBe TEST_USER.username.value + principal.effectiveRole.elementId shouldBe CompoundHierarchyId.WILDCARD + principal.effectiveRole.isSuperuser shouldBe false }) { val authenticatedClient = client.configureAuthentication(testUserClientConfig, json) - authenticatedClient.get("/api/v1/test") + authenticatedClient.get("/api/v1/test") shouldHaveStatus HttpStatusCode.OK } } }) diff --git a/services/hierarchy/build.gradle.kts b/services/hierarchy/build.gradle.kts index 45206919ce..be3ea2891a 100644 --- a/services/hierarchy/build.gradle.kts +++ b/services/hierarchy/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { api(libs.exposedCore) - implementation(projects.components.authorizationKeycloak.backend) + implementation(projects.components.authorization.backend) implementation(projects.dao) implementation(projects.services.reportStorageService) diff --git a/services/hierarchy/src/main/kotlin/OrganizationService.kt b/services/hierarchy/src/main/kotlin/OrganizationService.kt index d52154829a..fa66a3e333 100644 --- a/services/hierarchy/src/main/kotlin/OrganizationService.kt +++ b/services/hierarchy/src/main/kotlin/OrganizationService.kt @@ -19,12 +19,15 @@ package org.eclipse.apoapsis.ortserver.services -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService import org.eclipse.apoapsis.ortserver.dao.dbQuery -import org.eclipse.apoapsis.ortserver.dao.dbQueryCatching import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable import org.eclipse.apoapsis.ortserver.model.Organization +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.Product import org.eclipse.apoapsis.ortserver.model.repositories.OrganizationRepository import org.eclipse.apoapsis.ortserver.model.repositories.ProductRepository import org.eclipse.apoapsis.ortserver.model.util.FilterParameter @@ -34,10 +37,6 @@ import org.eclipse.apoapsis.ortserver.model.util.OptionalValue import org.jetbrains.exposed.sql.Database -import org.slf4j.LoggerFactory - -private val logger = LoggerFactory.getLogger(OrganizationService::class.java) - /** * A service providing functions for working with [organizations][Organization]. */ @@ -50,35 +49,21 @@ class OrganizationService( /** * Create an organization. */ - suspend fun createOrganization(name: String, description: String?): Organization = db.dbQueryCatching { + suspend fun createOrganization(name: String, description: String?): Organization = db.dbQuery { organizationRepository.create(name, description) - }.onSuccess { organization -> - runCatching { - authorizationService.createOrganizationPermissions(organization.id) - authorizationService.createOrganizationRoles(organization.id) - }.onFailure { e -> - logger.error("Error while creating Keycloak roles for organization '${organization.id}'.", e) - } - }.getOrThrow() + } /** * Create a product inside an [organization][organizationId]. */ - suspend fun createProduct(name: String, description: String?, organizationId: Long) = db.dbQueryCatching { + suspend fun createProduct(name: String, description: String?, organizationId: Long) = db.dbQuery { productRepository.create(name, description, organizationId) - }.onSuccess { product -> - runCatching { - authorizationService.createProductPermissions(product.id) - authorizationService.createProductRoles(product.id) - }.onFailure { e -> - logger.error("Error while creating Keycloak roles for product '${product.id}'.", e) - } - }.getOrThrow() + } /** * Delete an organization by [organizationId]. */ - suspend fun deleteOrganization(organizationId: Long): Unit = db.dbQueryCatching { + suspend fun deleteOrganization(organizationId: Long): Unit = db.dbQuery { if (productRepository.countForOrganization(organizationId) != 0L) { throw OrganizationNotEmptyException( "Cannot delete organization '$organizationId', as it still contains products." @@ -86,14 +71,7 @@ class OrganizationService( } organizationRepository.delete(organizationId) - }.onSuccess { - runCatching { - authorizationService.deleteOrganizationPermissions(organizationId) - authorizationService.deleteOrganizationRoles(organizationId) - }.onFailure { e -> - logger.error("Error while deleting Keycloak roles for organization '$organizationId'.", e) - } - }.getOrThrow() + } /** * Get an organization by [organizationId]. Returns null if the organization is not found. @@ -112,6 +90,23 @@ class OrganizationService( organizationRepository.list(parameters, filter) } + /** + * List all organizations that are visible to the given [userId] according to the given [parameters] and [filter]. + */ + suspend fun listOrganizationsForUser( + userId: String, + parameters: ListQueryParameters = ListQueryParameters.DEFAULT, + filter: FilterParameter? = null + ): ListQueryResult { + val orgFilter = authorizationService.filterHierarchyIds(userId, OrganizationRole.READER) + + return organizationRepository.list( + parameters = parameters, + nameFilter = filter, + hierarchyFilter = orgFilter + ) + } + /** * List all products for an [organization][organizationId]. */ @@ -123,6 +118,25 @@ class OrganizationService( productRepository.listForOrganization(organizationId, parameters, filter) } + /** + * List all products for an [organization][organizationId] that are visible to the user with the given [userId], + * applying the given [parameters] and optional [nameFilter]. + */ + suspend fun listProductsForOrganizationAndUser( + organizationId: Long, + userId: String, + parameters: ListQueryParameters = ListQueryParameters.DEFAULT, + nameFilter: FilterParameter? = null + ): ListQueryResult { + val productFilter = authorizationService.filterHierarchyIds( + userId, + ProductRole.READER, + OrganizationId(organizationId) + ) + + return productRepository.list(parameters, nameFilter, productFilter) + } + /** * Update an organization by [organizationId] with the [present][OptionalValue.Present] values. */ diff --git a/services/hierarchy/src/main/kotlin/ProductService.kt b/services/hierarchy/src/main/kotlin/ProductService.kt index a108a5025b..eb909a39e7 100644 --- a/services/hierarchy/src/main/kotlin/ProductService.kt +++ b/services/hierarchy/src/main/kotlin/ProductService.kt @@ -19,14 +19,15 @@ package org.eclipse.apoapsis.ortserver.services -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService import org.eclipse.apoapsis.ortserver.dao.dbQuery -import org.eclipse.apoapsis.ortserver.dao.dbQueryCatching import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.OrtRunsTable import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.model.Product +import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.model.Repository import org.eclipse.apoapsis.ortserver.model.RepositoryType import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository @@ -42,10 +43,6 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.max -import org.slf4j.LoggerFactory - -private val logger = LoggerFactory.getLogger(OrganizationService::class.java) - /** * A service providing functions for working with [products][Product]. */ @@ -64,33 +61,19 @@ class ProductService( url: String, productId: Long, description: String? - ): Repository = db.dbQueryCatching { + ): Repository = db.dbQuery { repositoryRepository.create(type, url, productId, description) - }.onSuccess { repository -> - runCatching { - authorizationService.createRepositoryPermissions(repository.id) - authorizationService.createRepositoryRoles(repository.id) - }.onFailure { e -> - logger.error("Error while creating Keycloak roles for repository '${repository.id}'.", e) - } - }.getOrThrow() + } /** * Delete a [product][productId] with its [repositories][Repository] and [OrtRun]s. */ - suspend fun deleteProduct(productId: Long): Unit = db.dbQueryCatching { + suspend fun deleteProduct(productId: Long): Unit = db.dbQuery { ortRunRepository.deleteByProduct(productId) repositoryRepository.deleteByProduct(productId) productRepository.delete(productId) - }.onSuccess { - runCatching { - authorizationService.deleteProductPermissions(productId) - authorizationService.deleteProductRoles(productId) - }.onFailure { e -> - logger.error("Error while deleting Keycloak roles for product '$productId'.", e) - } - }.getOrThrow() + } /** * Get a product by [productId]. Returns null if the product is not found. @@ -110,6 +93,22 @@ class ProductService( repositoryRepository.listForProduct(productId, parameters, filter) } + /** + * List all repositories for a [product][productId] that are visible to a specific [user][userId] according to the + * given [parameters] and [urlFilter]. + */ + suspend fun listRepositoriesForProductAndUser( + productId: Long, + userId: String, + parameters: ListQueryParameters = ListQueryParameters.DEFAULT, + urlFilter: FilterParameter? = null + ): ListQueryResult = getProduct(productId)?.let { product -> + val filter = authorizationService.filterHierarchyIds(userId, RepositoryRole.READER, ProductId(product.id)) + db.dbQuery { + repositoryRepository.list(parameters, urlFilter, filter) + } + } ?: ListQueryResult(emptyList(), parameters, 0) + /** * Update a product by [productId] with the [present][OptionalValue.Present] values. */ diff --git a/services/hierarchy/src/main/kotlin/RepositoryService.kt b/services/hierarchy/src/main/kotlin/RepositoryService.kt index 2657cda093..c34fb50137 100644 --- a/services/hierarchy/src/main/kotlin/RepositoryService.kt +++ b/services/hierarchy/src/main/kotlin/RepositoryService.kt @@ -19,9 +19,7 @@ package org.eclipse.apoapsis.ortserver.services -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService import org.eclipse.apoapsis.ortserver.dao.dbQuery -import org.eclipse.apoapsis.ortserver.dao.dbQueryCatching import org.eclipse.apoapsis.ortserver.dao.repositories.advisorjob.AdvisorJobsTable import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerjob.AnalyzerJobsTable import org.eclipse.apoapsis.ortserver.dao.repositories.evaluatorjob.EvaluatorJobsTable @@ -49,10 +47,6 @@ import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.and -import org.slf4j.LoggerFactory - -private val logger = LoggerFactory.getLogger(OrganizationService::class.java) - /** * A service providing functions for working with [repositories][Repository]. */ @@ -66,24 +60,16 @@ class RepositoryService( private val scannerJobRepository: ScannerJobRepository, private val evaluatorJobRepository: EvaluatorJobRepository, private val reporterJobRepository: ReporterJobRepository, - private val notifierJobRepository: NotifierJobRepository, - private val authorizationService: AuthorizationService + private val notifierJobRepository: NotifierJobRepository ) { /** * Delete the [Repository] by its [repositoryId] and all [OrtRun]s that are associated with it. */ - suspend fun deleteRepository(repositoryId: Long): Unit = db.dbQueryCatching { + suspend fun deleteRepository(repositoryId: Long): Unit = db.dbQuery { ortRunRepository.deleteByRepository(repositoryId) repositoryRepository.delete(repositoryId) - }.onSuccess { - runCatching { - authorizationService.deleteRepositoryPermissions(repositoryId) - authorizationService.deleteRepositoryRoles(repositoryId) - }.onFailure { e -> - logger.error("Error while deleting Keycloak roles for repository '$repositoryId'.", e) - } - }.getOrThrow() + } /** * Get all [Jobs] for the [OrtRun] with the provided [ortRunIndex] and [repositoryId]. diff --git a/services/hierarchy/src/test/kotlin/OrganizationServiceTest.kt b/services/hierarchy/src/test/kotlin/OrganizationServiceTest.kt index 1ca9fb314b..5a6a369100 100644 --- a/services/hierarchy/src/test/kotlin/OrganizationServiceTest.kt +++ b/services/hierarchy/src/test/kotlin/OrganizationServiceTest.kt @@ -21,18 +21,22 @@ package org.eclipse.apoapsis.ortserver.services import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just import io.mockk.mockk -import io.mockk.runs -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService import org.eclipse.apoapsis.ortserver.dao.repositories.organization.DaoOrganizationRepository import org.eclipse.apoapsis.ortserver.dao.repositories.product.DaoProductRepository import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension import org.eclipse.apoapsis.ortserver.dao.test.Fixtures +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter import org.jetbrains.exposed.sql.Database @@ -51,71 +55,83 @@ class OrganizationServiceTest : WordSpec({ fixtures = dbExtension.fixtures } - "createOrganization" should { - "create Keycloak roles" { - val authorizationService = mockk { - coEvery { createOrganizationPermissions(any()) } just runs - coEvery { createOrganizationRoles(any()) } just runs - } - - val service = OrganizationService(db, organizationRepository, productRepository, authorizationService) - val organization = service.createOrganization("name", "description") + "getRepositoryIdsForOrganization" should { + "return IDs for all repositories found in the products of the organization" { + val service = OrganizationService(db, organizationRepository, productRepository, mockk()) - coVerify(exactly = 1) { - authorizationService.createOrganizationPermissions(organization.id) - authorizationService.createOrganizationRoles(organization.id) - } - } - } + val orgId = fixtures.createOrganization().id - "createProduct" should { - "create Keycloak roles" { - val authorizationService = mockk { - coEvery { createProductPermissions(any()) } just runs - coEvery { createProductRoles(any()) } just runs - } + val prod1Id = fixtures.createProduct(organizationId = orgId).id + val prod2Id = fixtures.createProduct("Prod2", organizationId = orgId).id - val service = OrganizationService(db, organizationRepository, productRepository, authorizationService) - val product = service.createProduct("name", "description", fixtures.organization.id) + val repo1Id = fixtures.createRepository(productId = prod1Id).id + val repo2Id = fixtures.createRepository(url = "https://example.com/repo2.git", productId = prod2Id).id + val repo3Id = fixtures.createRepository(url = "https://example.com/repo3.git", productId = prod2Id).id - coVerify(exactly = 1) { - authorizationService.createProductPermissions(product.id) - authorizationService.createProductRoles(product.id) - } + service.getRepositoryIdsForOrganization(orgId).shouldContainExactlyInAnyOrder(repo1Id, repo2Id, repo3Id) } } - "deleteOrganization" should { - "delete Keycloak roles" { - val authorizationService = mockk { - coEvery { deleteOrganizationPermissions(any()) } just runs - coEvery { deleteOrganizationRoles(any()) } just runs + "listOrganizationsForUser" should { + "filter for organizations visible to a specific user" { + val userId = "test-user" + val org1Id = fixtures.organization.id + val org2 = fixtures.createOrganization(name = "Org2") + val org2Id = org2.id + val orgHierarchyIds = listOf(org1Id, org2Id).map { id -> + CompoundHierarchyId.forOrganization(OrganizationId(id)) + } + fixtures.createOrganization(name = "HiddenOrg").id + + val authService = mockk { + coEvery { + filterHierarchyIds(userId, OrganizationRole.READER) + } returns HierarchyFilter( + transitiveIncludes = mapOf(CompoundHierarchyId.ORGANIZATION_LEVEL to orgHierarchyIds), + nonTransitiveIncludes = emptyMap() + ) } - val service = OrganizationService(db, organizationRepository, productRepository, authorizationService) - service.deleteOrganization(fixtures.organization.id) + val service = OrganizationService(db, organizationRepository, productRepository, authService) + val organizations = service.listOrganizationsForUser(userId) - coVerify(exactly = 1) { - authorizationService.deleteOrganizationPermissions(fixtures.organization.id) - authorizationService.deleteOrganizationRoles(fixtures.organization.id) - } + organizations.totalCount shouldBe 2 + organizations.data shouldContainExactlyInAnyOrder listOf(fixtures.organization, org2) } } - "getRepositoryIdsForOrganization" should { - "return IDs for all repositories found in the products of the organization" { - val service = OrganizationService(db, organizationRepository, productRepository, mockk()) - - val orgId = fixtures.createOrganization().id - - val prod1Id = fixtures.createProduct(organizationId = orgId).id - val prod2Id = fixtures.createProduct("Prod2", organizationId = orgId).id + "listOrganizationsForUserAndOrganization" should { + "filter for products visible to a specific user in a specific organization" { + val userId = "test-user" + val org1Id = fixtures.organization.id + val prod1 = fixtures.createProduct("product", organizationId = org1Id) + val prod2 = fixtures.createProduct("product2", organizationId = org1Id) + fixtures.createProduct("hiddenProduct") + val prod1HierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(org1Id), + ProductId(prod1.id) + ) + val prod2HierarchyId = CompoundHierarchyId.forProduct( + OrganizationId(org1Id), + ProductId(prod2.id) + ) + + val authService = mockk { + coEvery { + filterHierarchyIds(userId, ProductRole.READER, OrganizationId(org1Id)) + } returns HierarchyFilter( + transitiveIncludes = mapOf( + CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod1HierarchyId, prod2HierarchyId) + ), + nonTransitiveIncludes = emptyMap() + ) + } - val repo1Id = fixtures.createRepository(productId = prod1Id).id - val repo2Id = fixtures.createRepository(url = "https://example.com/repo2.git", productId = prod2Id).id - val repo3Id = fixtures.createRepository(url = "https://example.com/repo3.git", productId = prod2Id).id + val service = OrganizationService(db, organizationRepository, productRepository, authService) + val products = service.listProductsForOrganizationAndUser(org1Id, userId) - service.getRepositoryIdsForOrganization(orgId).shouldContainExactlyInAnyOrder(repo1Id, repo2Id, repo3Id) + products.totalCount shouldBe 2 + products.data shouldContainExactlyInAnyOrder listOf(prod1, prod2) } } }) diff --git a/services/hierarchy/src/test/kotlin/ProductServiceTest.kt b/services/hierarchy/src/test/kotlin/ProductServiceTest.kt index a9c220cfa2..65516ca27b 100644 --- a/services/hierarchy/src/test/kotlin/ProductServiceTest.kt +++ b/services/hierarchy/src/test/kotlin/ProductServiceTest.kt @@ -20,22 +20,26 @@ package org.eclipse.apoapsis.ortserver.services import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.collections.beEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just import io.mockk.mockk -import io.mockk.runs -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.DaoOrtRunRepository import org.eclipse.apoapsis.ortserver.dao.repositories.product.DaoProductRepository import org.eclipse.apoapsis.ortserver.dao.repositories.repository.DaoRepositoryRepository import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension import org.eclipse.apoapsis.ortserver.dao.test.Fixtures -import org.eclipse.apoapsis.ortserver.model.RepositoryType +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId +import org.eclipse.apoapsis.ortserver.model.OrganizationId +import org.eclipse.apoapsis.ortserver.model.ProductId +import org.eclipse.apoapsis.ortserver.model.RepositoryId +import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter import org.jetbrains.exposed.sql.Database @@ -56,46 +60,7 @@ class ProductServiceTest : WordSpec({ fixtures = dbExtension.fixtures } - "createRepository" should { - "create Keycloak permissions" { - val authorizationService = mockk { - coEvery { createRepositoryPermissions(any()) } just runs - coEvery { createRepositoryRoles(any()) } just runs - } - - val service = - ProductService(db, productRepository, repositoryRepository, ortRunRepository, authorizationService) - val repository = service.createRepository( - RepositoryType.GIT, - "https://example.com/repo.git", - fixtures.product.id, - "Description" - ) - - coVerify(exactly = 1) { - authorizationService.createRepositoryPermissions(repository.id) - authorizationService.createRepositoryRoles(repository.id) - } - } - } - "deleteProduct" should { - "delete Keycloak permissions" { - val authorizationService = mockk { - coEvery { deleteProductPermissions(any()) } just runs - coEvery { deleteProductRoles(any()) } just runs - } - - val service = - ProductService(db, productRepository, repositoryRepository, ortRunRepository, authorizationService) - service.deleteProduct(fixtures.product.id) - - coVerify(exactly = 1) { - authorizationService.deleteProductPermissions(fixtures.product.id) - authorizationService.deleteProductRoles(fixtures.product.id) - } - } - "delete all repositories associated to this product" { val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, mockk()) @@ -134,4 +99,46 @@ class ProductServiceTest : WordSpec({ service.getRepositoryIdsForProduct(prodId).shouldContainExactlyInAnyOrder(repo1Id, repo2Id, repo3Id) } } + + "listRepositoriesForProductAndUser" should { + "apply a hierarchy filter obtained from the authorization service" { + val userId = "the-test-user" + val repo1 = fixtures.repository + val repo1Id = CompoundHierarchyId.forRepository( + OrganizationId(fixtures.organization.id), + ProductId(fixtures.product.id), + RepositoryId(repo1.id) + ) + val repo2 = fixtures.createRepository(url = "https://example.com/another-repo.git") + val repo2Id = CompoundHierarchyId.forRepository( + OrganizationId(fixtures.organization.id), + ProductId(fixtures.product.id), + RepositoryId(repo2.id) + ) + + val filter = HierarchyFilter( + transitiveIncludes = mapOf(CompoundHierarchyId.REPOSITORY_LEVEL to listOf(repo1Id, repo2Id)), + nonTransitiveIncludes = emptyMap() + ) + val authService = mockk { + coEvery { + filterHierarchyIds(userId, RepositoryRole.READER, ProductId(fixtures.product.id)) + } returns filter + } + + val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, authService) + val result = service.listRepositoriesForProductAndUser( + productId = fixtures.product.id, + userId = userId + ) + + result.data shouldContainExactlyInAnyOrder listOf(repo1, repo2) + } + + "return an empty list for a non-existing product ID" { + val service = ProductService(db, productRepository, repositoryRepository, ortRunRepository, mockk()) + + service.listRepositoriesForProductAndUser(-1L, "some-user").data should beEmpty() + } + } }) diff --git a/services/hierarchy/src/test/kotlin/RepositoryServiceTest.kt b/services/hierarchy/src/test/kotlin/RepositoryServiceTest.kt index d595ebc899..26ba073c99 100644 --- a/services/hierarchy/src/test/kotlin/RepositoryServiceTest.kt +++ b/services/hierarchy/src/test/kotlin/RepositoryServiceTest.kt @@ -26,13 +26,6 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should import io.kotest.matchers.shouldBe -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.mockk -import io.mockk.runs - -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension import org.eclipse.apoapsis.ortserver.dao.test.Fixtures import org.eclipse.apoapsis.ortserver.model.JobStatus @@ -52,7 +45,7 @@ class RepositoryServiceTest : WordSpec({ fixtures = dbExtension.fixtures } - fun createService(authorizationService: AuthorizationService = mockk()) = RepositoryService( + fun createService() = RepositoryService( db, dbExtension.fixtures.ortRunRepository, dbExtension.fixtures.repositoryRepository, @@ -61,26 +54,10 @@ class RepositoryServiceTest : WordSpec({ dbExtension.fixtures.scannerJobRepository, dbExtension.fixtures.evaluatorJobRepository, dbExtension.fixtures.reporterJobRepository, - dbExtension.fixtures.notifierJobRepository, - authorizationService + dbExtension.fixtures.notifierJobRepository ) "deleteRepository" should { - "delete Keycloak permissions" { - val authorizationService = mockk { - coEvery { deleteRepositoryPermissions(any()) } just runs - coEvery { deleteRepositoryRoles(any()) } just runs - } - val service = createService(authorizationService) - - service.deleteRepository(fixtures.repository.id) - - coVerify(exactly = 1) { - authorizationService.deleteRepositoryPermissions(fixtures.repository.id) - authorizationService.deleteRepositoryRoles(fixtures.repository.id) - } - } - "delete all ORT runs of the repository" { val service = createService() diff --git a/shared/ktor-utils/build.gradle.kts b/shared/ktor-utils/build.gradle.kts index e8042f2c5a..52cc041998 100644 --- a/shared/ktor-utils/build.gradle.kts +++ b/shared/ktor-utils/build.gradle.kts @@ -42,7 +42,7 @@ dependencies { testImplementation(libs.kotestAssertionsCore) testImplementation(libs.kotestAssertionsKtor) - testFixturesApi(projects.components.authorizationKeycloak.backend) + testFixturesApi(projects.components.authorization.backend) testFixturesApi(projects.utils.test) testFixturesApi(testFixtures(projects.dao)) testFixturesApi(libs.kotestAssertionsCore) diff --git a/shared/ktor-utils/src/testFixtures/kotlin/AbstractAuthorizationTest.kt b/shared/ktor-utils/src/testFixtures/kotlin/AbstractAuthorizationTest.kt index a05beac71c..0df7f95bb9 100644 --- a/shared/ktor-utils/src/testFixtures/kotlin/AbstractAuthorizationTest.kt +++ b/shared/ktor-utils/src/testFixtures/kotlin/AbstractAuthorizationTest.kt @@ -48,20 +48,20 @@ import org.eclipse.apoapsis.ortserver.clients.keycloak.UserId import org.eclipse.apoapsis.ortserver.clients.keycloak.UserName import org.eclipse.apoapsis.ortserver.clients.keycloak.test.KeycloakTestExtension import org.eclipse.apoapsis.ortserver.clients.keycloak.test.TEST_SUBJECT_CLIENT -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.addUserRole import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createJwtConfigMapForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakClientConfigurationForTestRealm -import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakClientForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.createKeycloakConfigMapForTestRealm import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpClientScope import org.eclipse.apoapsis.ortserver.clients.keycloak.test.setUpUser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.AuthorizationException -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.SecurityConfigurations -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.configureAuthentication -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.roles.Superuser -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.AuthorizationService -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.service.KeycloakAuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role +import org.eclipse.apoapsis.ortserver.components.authorization.routes.AuthenticationProviders +import org.eclipse.apoapsis.ortserver.components.authorization.routes.AuthorizationException +import org.eclipse.apoapsis.ortserver.components.authorization.routes.configureAuthentication +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.DbAuthorizationService import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.utils.test.Authorization import org.jetbrains.exposed.dao.exceptions.EntityNotFoundException @@ -83,7 +83,7 @@ private const val TEST_USER_PASSWORD = "password" @Suppress("UnnecessaryAbstractClass") abstract class AbstractAuthorizationTest(body: AbstractAuthorizationTest.() -> Unit) : WordSpec() { val dbExtension = DatabaseTestExtension() - val keycloakExtension = KeycloakTestExtension(createRealmPerTest = true) + val keycloakExtension = KeycloakTestExtension() // The "extension()" and "install()" functions cannot be used above because of // https://github.com/kotest/kotest/issues/3555. @@ -97,7 +97,6 @@ abstract class AbstractAuthorizationTest(body: AbstractAuthorizationTest.() -> U val json = Json { ignoreUnknownKeys = true } - val keycloakClient = keycloak.createKeycloakClientForTestRealm() val keycloakConfig = keycloak.createKeycloakConfigMapForTestRealm() val jwtConfig = keycloak.createJwtConfigMapForTestRealm() @@ -109,19 +108,10 @@ abstract class AbstractAuthorizationTest(body: AbstractAuthorizationTest.() -> U lateinit var authorizationService: AuthorizationService override suspend fun beforeEach(testCase: TestCase) { - authorizationService = KeycloakAuthorizationService( - keycloakClient, - dbExtension.db, - dbExtension.fixtures.organizationRepository, - dbExtension.fixtures.productRepository, - dbExtension.fixtures.repositoryRepository, - keycloakGroupPrefix = "" - ) - - authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions() + authorizationService = DbAuthorizationService(dbExtension.db) } - private fun authorizationTestApplication( + fun authorizationTestApplication( routes: Route.() -> Unit, block: suspend ApplicationTestBuilder.(unauthenticatedClient: HttpClient, testUserClient: HttpClient) -> Unit ) { @@ -152,7 +142,7 @@ abstract class AbstractAuthorizationTest(body: AbstractAuthorizationTest.() -> U configureAuthentication(config, authorizationService) routing { - authenticate(SecurityConfigurations.TOKEN) { + authenticate(AuthenticationProviders.TOKEN_PROVIDER) { routes() } } @@ -181,13 +171,14 @@ abstract class AbstractAuthorizationTest(body: AbstractAuthorizationTest.() -> U fun requestShouldRequireRole( routes: Route.() -> Unit, - role: String, + role: Role, + hierarchyId: CompoundHierarchyId, successStatus: HttpStatusCode = HttpStatusCode.OK, request: suspend HttpClient.() -> HttpResponse ) { authorizationTestApplication(routes) { _, testUserClient -> testUserClient.request() shouldHaveStatus HttpStatusCode.Forbidden - keycloak.keycloakAdminClient.addUserRole(TEST_USER.username.value, role) + authorizationService.assignRole(TEST_USER.username.value, role, hierarchyId) testUserClient.request() shouldHaveStatus successStatus } } @@ -197,6 +188,6 @@ abstract class AbstractAuthorizationTest(body: AbstractAuthorizationTest.() -> U successStatus: HttpStatusCode = HttpStatusCode.OK, request: suspend HttpClient.() -> HttpResponse ) { - requestShouldRequireRole(routes, Superuser.ROLE_NAME, successStatus, request) + requestShouldRequireRole(routes, OrganizationRole.ADMIN, CompoundHierarchyId.WILDCARD, successStatus, request) } } diff --git a/shared/ktor-utils/src/testFixtures/kotlin/AbstractIntegrationTest.kt b/shared/ktor-utils/src/testFixtures/kotlin/AbstractIntegrationTest.kt index 6dbcf71f4f..5a0e9e5d8a 100644 --- a/shared/ktor-utils/src/testFixtures/kotlin/AbstractIntegrationTest.kt +++ b/shared/ktor-utils/src/testFixtures/kotlin/AbstractIntegrationTest.kt @@ -19,7 +19,6 @@ package org.eclipse.apoapsis.ortserver.shared.ktorutils -import io.kotest.core.spec.Spec import io.kotest.core.spec.style.WordSpec import io.ktor.client.HttpClient @@ -38,7 +37,6 @@ import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.requestvalidation.RequestValidation import io.ktor.server.plugins.requestvalidation.RequestValidationConfig import io.ktor.server.routing.Route -import io.ktor.server.routing.RoutingContext import io.ktor.server.routing.routing import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.testApplication @@ -46,14 +44,10 @@ import io.ktor.util.appendIfNameAbsent import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkAll import kotlinx.serialization.json.Json -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId -import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.hasRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension import org.eclipse.apoapsis.ortserver.utils.test.Integration @@ -64,9 +58,9 @@ import org.eclipse.apoapsis.ortserver.utils.test.Integration abstract class AbstractIntegrationTest(body: AbstractIntegrationTest.() -> Unit) : WordSpec() { val dbExtension = extension(DatabaseTestExtension()) - val principal = mockk { - every { getUserId() } returns "userId" - every { hasRole(any()) } returns true + val principal = mockk { + every { userId } returns "userId" + every { isAuthorized } returns true } init { @@ -74,14 +68,6 @@ abstract class AbstractIntegrationTest(body: AbstractIntegrationTest.() -> Unit) body() } - override suspend fun beforeSpec(spec: Spec) { - mockkStatic(RoutingContext::hasRole) - } - - override suspend fun afterSpec(spec: Spec) { - unmockkAll() - } - fun integrationTestApplication( routes: Route.() -> Unit = {}, validations: RequestValidationConfig.() -> Unit = {}, @@ -123,7 +109,7 @@ fun ApplicationTestBuilder.createJsonClient() = createClient { } } -class DummyConfig(val principal: OrtPrincipal) : AuthenticationProvider.Config("test") +class DummyConfig(val principal: OrtServerPrincipal) : AuthenticationProvider.Config("test") class FakeAuthenticationProvider(val config: DummyConfig) : AuthenticationProvider(config) { override suspend fun onAuthenticate(context: AuthenticationContext) {