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/backend/src/main/kotlin/rights/OrganizationPermission.kt b/components/authorization/backend/src/main/kotlin/rights/OrganizationPermission.kt index 91cf53918b..5e8cebe525 100644 --- a/components/authorization/backend/src/main/kotlin/rights/OrganizationPermission.kt +++ b/components/authorization/backend/src/main/kotlin/rights/OrganizationPermission.kt @@ -49,3 +49,12 @@ enum class OrganizationPermission { /** Permission to delete the [Organization]. */ DELETE } + +/** + * The set of permissions required by the role to read an organization. (This is defined here to avoid circular + * dependencies, as it is referenced by multiple role classes.) + */ +internal val organizationReadPermissions = setOf( + OrganizationPermission.READ, + OrganizationPermission.READ_PRODUCTS +) diff --git a/components/authorization/backend/src/main/kotlin/rights/OrganizationRole.kt b/components/authorization/backend/src/main/kotlin/rights/OrganizationRole.kt index 088093567b..dba43a6bb0 100644 --- a/components/authorization/backend/src/main/kotlin/rights/OrganizationRole.kt +++ b/components/authorization/backend/src/main/kotlin/rights/OrganizationRole.kt @@ -19,6 +19,8 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId + /** * This enum contains the available roles for [organizations][org.eclipse.apoapsis.ortserver.model.Organization]. It * maps the permissions available for an organization to the default roles [READER], [WRITER], and [ADMIN]. @@ -35,10 +37,7 @@ enum class OrganizationRole( ) : Role { /** A role that grants read permissions for an [org.eclipse.apoapsis.ortserver.model.Organization]. */ READER( - organizationPermissions = setOf( - OrganizationPermission.READ, - OrganizationPermission.READ_PRODUCTS - ), + organizationPermissions = organizationReadPermissions, productPermissions = ProductRole.READER.productPermissions, repositoryPermissions = RepositoryRole.READER.repositoryPermissions ), @@ -63,5 +62,7 @@ enum class OrganizationRole( organizationPermissions = OrganizationPermission.entries.toSet(), productPermissions = ProductPermission.entries.toSet(), repositoryPermissions = RepositoryPermission.entries.toSet() - ) + ); + + override val level = CompoundHierarchyId.ORGANIZATION_LEVEL } diff --git a/components/authorization/backend/src/main/kotlin/rights/ProductPermission.kt b/components/authorization/backend/src/main/kotlin/rights/ProductPermission.kt index 57f263d415..ab94b974e5 100644 --- a/components/authorization/backend/src/main/kotlin/rights/ProductPermission.kt +++ b/components/authorization/backend/src/main/kotlin/rights/ProductPermission.kt @@ -51,3 +51,12 @@ enum class ProductPermission { /** Permission to delete the [Product]. */ DELETE } + +/** + * The set of permissions required by the role to read a product. (This is defined here to avoid circular dependencies, + * as it is referenced by multiple role classes.) + */ +internal val productReadPermissions = setOf( + ProductPermission.READ, + ProductPermission.READ_REPOSITORIES +) diff --git a/components/authorization/backend/src/main/kotlin/rights/ProductRole.kt b/components/authorization/backend/src/main/kotlin/rights/ProductRole.kt index 3467e7387b..cb9da1fd28 100644 --- a/components/authorization/backend/src/main/kotlin/rights/ProductRole.kt +++ b/components/authorization/backend/src/main/kotlin/rights/ProductRole.kt @@ -19,6 +19,8 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId + /** * This enum contains the available roles for [products][org.eclipse.apoapsis.ortserver.model.Product]. It * maps the permissions available for a product to the default roles [READER], [WRITER], and [ADMIN]. @@ -29,16 +31,13 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights * - The constants are expected to be listed in increasing order of permissions. */ enum class ProductRole( - override val organizationPermissions: Set = setOf(OrganizationPermission.READ), + override val organizationPermissions: Set = organizationReadPermissions, override val productPermissions: Set, override val repositoryPermissions: Set ) : Role { /** A role that grants read permissions for a [org.eclipse.apoapsis.ortserver.model.Product]. */ READER( - productPermissions = setOf( - ProductPermission.READ, - ProductPermission.READ_REPOSITORIES - ), + productPermissions = productReadPermissions, repositoryPermissions = RepositoryRole.READER.repositoryPermissions ), @@ -61,5 +60,7 @@ enum class ProductRole( ADMIN( productPermissions = ProductPermission.entries.toSet(), repositoryPermissions = RepositoryPermission.entries.toSet() - ) + ); + + override val level = CompoundHierarchyId.PRODUCT_LEVEL } diff --git a/components/authorization/backend/src/main/kotlin/rights/RepositoryRole.kt b/components/authorization/backend/src/main/kotlin/rights/RepositoryRole.kt index f347a1f71d..310fd26707 100644 --- a/components/authorization/backend/src/main/kotlin/rights/RepositoryRole.kt +++ b/components/authorization/backend/src/main/kotlin/rights/RepositoryRole.kt @@ -19,13 +19,15 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId + /** * This enum contains the available roles for [repositories][org.eclipse.apoapsis.ortserver.model.Repository]. It * maps the permissions available for a product to the default roles [READER], [WRITER], and [ADMIN]. */ enum class RepositoryRole( - override val organizationPermissions: Set = setOf(OrganizationPermission.READ), - override val productPermissions: Set = setOf(ProductPermission.READ), + override val organizationPermissions: Set = organizationReadPermissions, + override val productPermissions: Set = productReadPermissions, override val repositoryPermissions: Set ) : Role { /** A role that grants read permissions for a [org.eclipse.apoapsis.ortserver.model.Repository]. */ @@ -49,5 +51,7 @@ enum class RepositoryRole( /** A role that grants all permissions for a [org.eclipse.apoapsis.ortserver.model.Repository]. */ ADMIN( repositoryPermissions = RepositoryPermission.entries.toSet() - ) + ); + + override val level: Int = CompoundHierarchyId.REPOSITORY_LEVEL } diff --git a/components/authorization/backend/src/main/kotlin/rights/Role.kt b/components/authorization/backend/src/main/kotlin/rights/Role.kt index df7172419f..d579931b83 100644 --- a/components/authorization/backend/src/main/kotlin/rights/Role.kt +++ b/components/authorization/backend/src/main/kotlin/rights/Role.kt @@ -19,6 +19,8 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId + /** * An interface to define common properties of all roles in the authorization system. * @@ -27,6 +29,36 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights * specific repository should also be entitled to view the product and the organization the repository belongs to. */ sealed interface Role { + companion object { + /** + * Return a [List] of all roles defined for the given hierarchy [level]. For an invalid level, return an empty + * list. + */ + fun rolesForLevel(level: Int): List = + when (level) { + CompoundHierarchyId.ORGANIZATION_LEVEL -> OrganizationRole.entries + CompoundHierarchyId.PRODUCT_LEVEL -> ProductRole.entries + CompoundHierarchyId.REPOSITORY_LEVEL -> RepositoryRole.entries + else -> emptyList() + } + + /** + * Return the role with the given [name] defined for the given hierarchy [level], or null if no such role + * exists. + */ + fun getRoleByNameAndLevel(level: Int, name: String): Role? = + rolesForLevel(level).find { it.name == name } + } + + /** The name of this role. */ + val name: String + + /** + * The level in the hierarchy this role is defined for. The value corresponds to the constants defined by the + * [org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId] companion object. + */ + val level: Int + /** A set with permissions that are granted by this role on the organization level. */ val organizationPermissions: Set diff --git a/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt b/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt index b953850ade..8a1868d722 100644 --- a/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt +++ b/components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt @@ -17,8 +17,11 @@ * License-Filename: LICENSE */ +@file:Suppress("TooManyFunctions") + 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 @@ -28,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 @@ -38,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. @@ -52,17 +95,19 @@ suspend fun ApplicationCall.createAuthorizedPrincipal( (this as? RoutingPipelineCall)?.let { routingCall -> val checker = routingCall.route.findAuthorizationChecker() - val effectiveRole = if (checker != null) { - checker.loadEffectiveRole( - service = authorizationService, - userId = payload.getClaim("preferred_username").asString(), - call = this - ) - } else { - EffectiveRole.EMPTY - } + runCatching { + val effectiveRole = if (checker != null) { + checker.loadEffectiveRole( + service = authorizationService, + userId = payload.getClaim("preferred_username").asString(), + call = this + ) + } else { + EffectiveRole.EMPTY + } - OrtServerPrincipal.create(payload, effectiveRole) + OrtServerPrincipal.create(payload, effectiveRole) + }.getOrElse(OrtServerPrincipal::fromException) } /** @@ -75,7 +120,19 @@ fun Route.get( ): Route = documentedAuthorized(checker, body) { get(builder, it) } /** - * Create a new [Route] for HTTP POST requests that performs an automatic authorization check using the given [checker]. + * Create a new [Route] for HTTP GET requests with the given [path] that performs an automatic authorization check + * using the given [checker]. + */ +fun Route.get( + path: String, + builder: RouteConfig.() -> Unit, + checker: AuthorizationChecker, + body: suspend RoutingContext.() -> Unit +): Route = documentedAuthorized(checker, body) { get(path, builder, it) } + +/** + * Create a new [Route] for HTTP POST requests that performs an automatic authorization check using the given + * [checker]. */ fun Route.post( builder: RouteConfig.() -> Unit, @@ -83,6 +140,17 @@ fun Route.post( body: suspend RoutingContext.() -> Unit ): Route = documentedAuthorized(checker, body) { post(builder, it) } +/** + * Create a new [Route] for HTTP POST requests with the given [path] that performs an automatic authorization check + * using the given [checker]. + */ +fun Route.post( + path: String, + builder: RouteConfig.() -> Unit, + checker: AuthorizationChecker, + body: suspend RoutingContext.() -> Unit +): Route = documentedAuthorized(checker, body) { post(path, builder, it) } + /** * Create a new [Route] for HTTP PATCH requests that performs an automatic authorization check using the given * [checker]. @@ -93,6 +161,17 @@ fun Route.patch( body: suspend RoutingContext.() -> Unit ): Route = documentedAuthorized(checker, body) { patch(builder, it) } +/** + * Create a new [Route] for HTTP PATCH requests with the given [path] that performs an automatic authorization check + * using the given [checker]. + */ +fun Route.patch( + path: String, + builder: RouteConfig.() -> Unit, + checker: AuthorizationChecker, + body: suspend RoutingContext.() -> Unit +): Route = documentedAuthorized(checker, body) { patch(path, builder, it) } + /** * Create a new [Route] for HTTP PUT requests that performs an automatic authorization check using the given * [checker]. @@ -103,6 +182,17 @@ fun Route.put( body: suspend RoutingContext.() -> Unit ): Route = documentedAuthorized(checker, body) { put(builder, it) } +/** + * Create a new [Route] for HTTP PUT requests with the given [path] that performs an automatic authorization check + * using the given [checker]. + */ +fun Route.put( + path: String, + builder: RouteConfig.() -> Unit, + checker: AuthorizationChecker, + body: suspend RoutingContext.() -> Unit +): Route = documentedAuthorized(checker, body) { put(path, builder, it) } + /** * Create a new [Route] for HTTP DELETE requests that performs an automatic authorization check using the given * [checker]. @@ -113,6 +203,17 @@ fun Route.delete( body: suspend RoutingContext.() -> Unit ): Route = documentedAuthorized(checker, body) { delete(builder, it) } +/** + * Create a new [Route] for HTTP DELETE requests with the given [path] that performs an automatic authorization check + * using the given [checker]. + */ +fun Route.delete( + path: String, + builder: RouteConfig.() -> Unit, + checker: AuthorizationChecker, + body: suspend RoutingContext.() -> Unit +): Route = documentedAuthorized(checker, body) { delete(path, builder, it) } + /** * Generic function to create a new [Route] that performs an automatic authorization check using the given [checker]. * The content of the route is defined by the given original [body] and the [build] function. diff --git a/components/authorization/backend/src/main/kotlin/routes/Mappings.kt b/components/authorization/backend/src/main/kotlin/routes/Mappings.kt new file mode 100644 index 0000000000..356928d670 --- /dev/null +++ b/components/authorization/backend/src/main/kotlin/routes/Mappings.kt @@ -0,0 +1,58 @@ +/* + * 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 org.eclipse.apoapsis.ortserver.components.authorization.api.OrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.api.ProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.api.RepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole as ModelOrganizationRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole as ModelProductRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryRole as ModelRepositoryRole +import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role +import org.eclipse.apoapsis.ortserver.model.UserGroup + +fun OrganizationRole.mapToModel() = when (this) { + OrganizationRole.READER -> ModelOrganizationRole.READER + OrganizationRole.WRITER -> ModelOrganizationRole.WRITER + OrganizationRole.ADMIN -> ModelOrganizationRole.ADMIN +} + +fun ProductRole.mapToModel() = when (this) { + ProductRole.READER -> ModelProductRole.READER + ProductRole.WRITER -> ModelProductRole.WRITER + ProductRole.ADMIN -> ModelProductRole.ADMIN +} + +fun RepositoryRole.mapToModel() = when (this) { + RepositoryRole.READER -> ModelRepositoryRole.READER + RepositoryRole.WRITER -> ModelRepositoryRole.WRITER + RepositoryRole.ADMIN -> ModelRepositoryRole.ADMIN +} + +/** + * Map this [Role] to a [UserGroup] which is required by the endpoint for querying user / role information. + */ +fun Role.mapToGroup(): UserGroup = + when (name) { + "READER" -> UserGroup.READERS + "WRITER" -> UserGroup.WRITERS + "ADMIN" -> UserGroup.ADMINS + else -> throw IllegalArgumentException("Cannot map role '$name' to a user group.") + } diff --git a/components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt b/components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt index 3585f4b138..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,10 @@ 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 + import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole /** @@ -36,6 +40,15 @@ class OrtServerPrincipal( /** The full name of the principal. */ val fullName: String, + /** + * An exception that occurred when setting up the principal. If this is not *null*, this exception is re-thrown + * when querying the authorization status. The background of this property is that during authentication, all + * exceptions are caught by Ktor and lead to HTTP 401 responses. However, for some exceptions, different status + * codes are more appropriate, for instance, status 404 if a non-existing hierarchy ID was requested. This can + * only be achieved by storing the exception first and re-throwing it later when a route handler is active. + */ + val validationException: Throwable?, + /** * The effective role computed for the principal. This can be *null* if either no authorization is required or the * authorization check failed. In the latter case, an exception is thrown when the role is accessed. @@ -57,16 +70,39 @@ class OrtServerPrincipal( userId = payload.subject, username = payload.getClaim(CLAIM_USERNAME).asString(), fullName = payload.getClaim(CLAIM_FULL_NAME).asString(), - role = effectiveRole + role = effectiveRole, + validationException = null ) + + /** + * Create an [OrtServerPrincipal] for the case that during authentication the given [exception] occurred. This + * exception is recorded, so that it can be handled later. + */ + fun fromException(exception: Throwable): OrtServerPrincipal = + OrtServerPrincipal( + userId = "", + username = "", + fullName = "", + role = null, + validationException = exception + ) + + /** + * Make sure that the current [RoutingContext] contains an authorized [OrtServerPrincipal] and return it. + * Throw an [AuthorizationException] otherwise. + */ + fun RoutingContext.requirePrincipal(): OrtServerPrincipal = + call.principal() ?: throw AuthorizationException() } /** * A flag indicating whether the principal is authorized. If this is *true*, the effective role of the principal - * can be accessed via [effectiveRole]. + * can be accessed via [effectiveRole]. If a [validationException] is recorded in this instance, it is thrown + * when accessing this property. Since this property is typically accessed in the beginning of a route handler, + * this leads to proper exception handling and mapping to HTTP response status codes. */ val isAuthorized: Boolean - get() = role != null + get() = validationException?.let { throw it } ?: (role != null) /** * The effective role of the principal if authorization was successful. Otherwise, accessing this property throws @@ -75,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/AuthorizationService.kt b/components/authorization/backend/src/main/kotlin/service/AuthorizationService.kt index a9f161f516..a7ba9b5b89 100644 --- a/components/authorization/backend/src/main/kotlin/service/AuthorizationService.kt +++ b/components/authorization/backend/src/main/kotlin/service/AuthorizationService.kt @@ -93,11 +93,11 @@ interface AuthorizationService { suspend fun listUsersWithRole(role: Role, compoundHierarchyId: CompoundHierarchyId): Set /** - * Return a [Map] with the IDs of all users and their assigned roles on the hierarchy element identified by the + * Return a [Map] with the IDs of all users and their assigned role on the hierarchy element identified by the * given [compoundHierarchyId]. The result includes users who inherit access rights on this hierarchy element from - * higher levels, such as organization admins. + * higher levels, such as organization admins, but no superusers. */ - suspend fun listUsers(compoundHierarchyId: CompoundHierarchyId): Map> + suspend fun listUsers(compoundHierarchyId: CompoundHierarchyId): Map /** * Return a [HierarchyFilter] with information about all hierarchy elements for which the specified [userId] has at diff --git a/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt b/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt index 5d380afe84..e08c9331ae 100644 --- a/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt +++ b/components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt @@ -21,6 +21,10 @@ package org.eclipse.apoapsis.ortserver.components.authorization.service +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext + import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole import org.eclipse.apoapsis.ortserver.components.authorization.rights.HierarchyPermissions @@ -92,35 +96,25 @@ class DbAuthorizationService( userId: String, hierarchyId: HierarchyId, checker: PermissionChecker - ): EffectiveRole? { - val compoundHierarchyId = resolveCompoundId(hierarchyId) - - return compoundHierarchyId.takeUnless { it.isInvalid() }?.let { - checkPermissions(userId, it, checker) - } - } + ): EffectiveRole? = + checkPermissions(userId, resolveCompoundId(hierarchyId), checker) override suspend fun getEffectiveRole( userId: String, compoundHierarchyId: CompoundHierarchyId ): EffectiveRole { val roleAssignments = loadAssignments(userId, compoundHierarchyId) - val roles = rolesForLevel(compoundHierarchyId).reversed().asSequence() - - // Check for all roles on the current level, starting with the ADMIN role, whether all its permissions are - // granted. This is then the effective role. This assumes that constants in the enums for roles are ordered - // by the number of permissions they grant in ascending order. If this changes, there should be failing tests. - return roles.mapNotNull { role -> - val permissionChecker = HierarchyPermissions.permissions(role) - HierarchyPermissions.create(roleAssignments, permissionChecker) - .takeIf { it.hasPermission(compoundHierarchyId) }?.let { - EffectiveRoleImpl( - elementId = compoundHierarchyId, - isSuperuser = it.isSuperuser(), - permissions = permissionChecker - ) - } - }.firstOrNull() ?: EffectiveRoleImpl( + + return findHighestRole( + roleAssignments, + compoundHierarchyId + )?.let { (_, permissionChecker, hierarchyPermissions) -> + EffectiveRoleImpl( + elementId = compoundHierarchyId, + isSuperuser = hierarchyPermissions.isSuperuser(), + permissions = permissionChecker + ) + } ?: EffectiveRoleImpl( elementId = compoundHierarchyId, isSuperuser = false, permissions = PermissionChecker() @@ -130,21 +124,8 @@ class DbAuthorizationService( override suspend fun getEffectiveRole( userId: String, hierarchyId: HierarchyId - ): EffectiveRole { - val compoundHierarchyId = resolveCompoundId(hierarchyId) - - return if (compoundHierarchyId.isInvalid()) { - logger.warn("Failed to resolve hierarchy ID $hierarchyId.") - - EffectiveRoleImpl( - elementId = compoundHierarchyId, - isSuperuser = false, - permissions = PermissionChecker() - ) - } else { - getEffectiveRole(userId, compoundHierarchyId) - } - } + ): EffectiveRole = + getEffectiveRole(userId, resolveCompoundId(hierarchyId)) override suspend fun assignRole( userId: String, @@ -193,15 +174,26 @@ class DbAuthorizationService( }.mapTo(mutableSetOf()) { it[RoleAssignmentsTable.userId] } } - override suspend fun listUsers(compoundHierarchyId: CompoundHierarchyId): Map> = db.dbQuery { - RoleAssignmentsTable.selectAll() - .where { - repositoryCondition(compoundHierarchyId) or productWildcardCondition(compoundHierarchyId) - }.map { row -> row[RoleAssignmentsTable.userId] to row.extractRole() } - .filterNot { it.second == null } - .groupBy(keySelector = { it.first }, valueTransform = { it.second }) - .mapValues { it.value.filterNotNullTo(mutableSetOf()) } - } + override suspend fun listUsers(compoundHierarchyId: CompoundHierarchyId): Map = + withContext(Dispatchers.Default) { + db.dbQuery { + logger.info("Loading role assignments on element {}...", compoundHierarchyId) + + RoleAssignmentsTable.selectAll() + .where { + RoleAssignmentsTable.organizationId eq compoundHierarchyId.organizationId?.value + }.mapNotNull { row -> + row.extractRole()?.let { role -> + Triple(row[RoleAssignmentsTable.userId], role, row.extractHierarchyId()) + } + } + }.groupBy(keySelector = { it.first }, valueTransform = { it.third to it.second }) + .mapValues { (user, assignments) -> + async { computeRoleForUser(user, compoundHierarchyId, assignments) } + }.mapNotNull { (user, deferredRole) -> + deferredRole.await()?.let { user to it } + }.toMap() + } override suspend fun filterHierarchyIds( userId: String, @@ -225,7 +217,7 @@ class DbAuthorizationService( return HierarchyFilter( transitiveIncludes = includes, nonTransitiveIncludes = permissions.implicitIncludes().filterContainedIn(containedInId), - isWildcard = permissions.isSuperuser() + isWildcard = permissions.isSuperuser() && containedInId == null ) } @@ -263,6 +255,11 @@ class DbAuthorizationService( val (orgId, prodId) = resolveOrganizationAndProduct(hierarchyId) CompoundHierarchyId.forRepository(orgId, prodId, hierarchyId) } + }.also { + if (it.isInvalid()) { + logger.warn("Failed to resolve hierarchy ID $hierarchyId.") + throw InvalidHierarchyIdException(hierarchyId) + } } /** @@ -305,6 +302,8 @@ class DbAuthorizationService( userId: String, compoundHierarchyId: CompoundHierarchyId? ): List> = db.dbQuery { + logger.info("Loading role assignments for user '{}' on element {}...", userId, compoundHierarchyId) + RoleAssignmentsTable.selectAll() .where { val hierarchyCondition = compoundHierarchyId?.let { id -> @@ -411,24 +410,18 @@ private fun ResultRow.extractHierarchyId(): CompoundHierarchyId { } /** - * Generate the SQL condition to match the repository part of this [hierarchyId]. The condition also has to select - * assignments on higher levels in the same hierarchy. - */ -private fun SqlExpressionBuilder.repositoryCondition(hierarchyId: CompoundHierarchyId): Op = - ( - (RoleAssignmentsTable.repositoryId eq hierarchyId.repositoryId?.value) or - (RoleAssignmentsTable.repositoryId eq null) - ) and - (RoleAssignmentsTable.productId eq hierarchyId.productId?.value) and - (RoleAssignmentsTable.organizationId eq hierarchyId.organizationId?.value) - -/** - * Generate the SQL condition to match role assignments for the given [hierarchyId] for which no product ID is - * defined. + * Compute the effective [Role] for a user on the element with the given [hierarchyId] based on the given + * [assignments]. */ -private fun SqlExpressionBuilder.productWildcardCondition(hierarchyId: CompoundHierarchyId): Op = - (RoleAssignmentsTable.productId eq null) and - (RoleAssignmentsTable.organizationId eq hierarchyId.organizationId?.value) +private fun computeRoleForUser( + user: String, + hierarchyId: CompoundHierarchyId, + assignments: List> +): Role? { + logger.info("Computing effective role for user '{}' on element {}...", user, hierarchyId) + + return findHighestRole(assignments, hierarchyId)?.first +} /** * Generate the SQL condition to match role assignments for the given [role]. @@ -456,11 +449,26 @@ private fun IdsByLevel.filterContainedIn( } /** - * Return a collection with the roles that are relevant on the hierarchy level defined by [hierarchyId]. + * Find the highest role for the given [hierarchyId] based on the provided [roleAssignments]. Check for all roles on + * the level of the hierarchy ID, starting with the ADMIN role, whether all its permissions are granted. This is then + * the effective role. This assumes that constants in the enums for roles are ordered by the number of permissions they + * grant in ascending order. (If this changes, there should be failing tests.) Return all relevant information about + * the detected role and the relevant permissions, or *null* if no role was found. */ -private fun rolesForLevel(hierarchyId: CompoundHierarchyId): Collection = - when (hierarchyId.level) { - CompoundHierarchyId.REPOSITORY_LEVEL -> RepositoryRole.entries - CompoundHierarchyId.PRODUCT_LEVEL -> ProductRole.entries - else -> OrganizationRole.entries - } +private fun findHighestRole( + roleAssignments: List>, + hierarchyId: CompoundHierarchyId +): Triple? { + val roles = ( + Role.rolesForLevel(hierarchyId.level) + .takeUnless { it.isEmpty() } ?: OrganizationRole.entries + ).reversed().asSequence() + + return roles.mapNotNull { role -> + val permissionChecker = HierarchyPermissions.permissions(role) + HierarchyPermissions.create(roleAssignments, permissionChecker) + .takeIf { it.hasPermission(hierarchyId) }?.let { + Triple(role, permissionChecker, it) + } + }.firstOrNull() +} diff --git a/components/authorization/backend/src/main/kotlin/service/InvalidHierarchyIdException.kt b/components/authorization/backend/src/main/kotlin/service/InvalidHierarchyIdException.kt new file mode 100644 index 0000000000..dda372b1f5 --- /dev/null +++ b/components/authorization/backend/src/main/kotlin/service/InvalidHierarchyIdException.kt @@ -0,0 +1,31 @@ +/* + * 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.service + +import org.eclipse.apoapsis.ortserver.model.HierarchyId + +/** + * An exception class that is thrown by the [AuthorizationService] when it cannot resolve a [HierarchyId]. This + * typically means that users have called the API with the ID of a non-existing hierarchy element. Thus, such exceptions + * should lead to HTTP 404 responses. + */ +class InvalidHierarchyIdException( + val hierarchyId: HierarchyId +) : RuntimeException("Could not resolve hierarchy ID: $hierarchyId.") diff --git a/components/authorization/backend/src/main/kotlin/service/KeycloakUserService.kt b/components/authorization/backend/src/main/kotlin/service/KeycloakUserService.kt index d269afa447..6ecb82d203 100644 --- a/components/authorization/backend/src/main/kotlin/service/KeycloakUserService.kt +++ b/components/authorization/backend/src/main/kotlin/service/KeycloakUserService.kt @@ -66,8 +66,12 @@ class KeycloakUserService( keycloakClient.getUser(UserName(id)).toOrtUser() override suspend fun getUsersById(ids: Set): Set = withContext(Dispatchers.IO) { - ids.map { async { getUserById(it) } } - .mapTo(mutableSetOf()) { it.await() } + ids.map { async { runCatching { getUserById(it) } } } + .mapNotNullTo(mutableSetOf()) { it.await().getOrNull() } + } + + override suspend fun existsUser(id: String): Boolean { + return runCatching { getUserById(id) }.isSuccess } } diff --git a/components/authorization/backend/src/main/kotlin/service/UserService.kt b/components/authorization/backend/src/main/kotlin/service/UserService.kt index dd4b7d8b4d..e8ca44952d 100644 --- a/components/authorization/backend/src/main/kotlin/service/UserService.kt +++ b/components/authorization/backend/src/main/kotlin/service/UserService.kt @@ -57,4 +57,9 @@ interface UserService { * Resolve a number of users by their internal [ids]. Ignore unknown ids. */ suspend fun getUsersById(ids: Set): Set + + /** + * Return a flag whether a user with the given [id] exists. + */ + suspend fun existsUser(id: String): Boolean } diff --git a/components/authorization/backend/src/test/kotlin/rights/RolesTest.kt b/components/authorization/backend/src/test/kotlin/rights/RolesTest.kt new file mode 100644 index 0000000000..57ef891f69 --- /dev/null +++ b/components/authorization/backend/src/test/kotlin/rights/RolesTest.kt @@ -0,0 +1,69 @@ +/* + * 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.rights + +import io.kotest.core.spec.style.WordSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe + +import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId + +class RolesTest : WordSpec({ + "getRoleByNameAndLevel" should { + "return the correct role" { + val allRoles = OrganizationRole.entries.toList() + + ProductRole.entries.toList() + + RepositoryRole.entries.toList() + + allRoles.forAll { role -> + Role.getRoleByNameAndLevel(role.level, role.name) shouldBe role + } + } + + "return null for an invalid role name" { + Role.getRoleByNameAndLevel(CompoundHierarchyId.ORGANIZATION_LEVEL, "GARDENER") shouldBe null + } + + "return null for an invalid level" { + Role.getRoleByNameAndLevel(CompoundHierarchyId.WILDCARD_LEVEL, "READER") shouldBe null + Role.getRoleByNameAndLevel(1000, "READER") shouldBe null + } + } + + "level" should { + "be correct for OrganizationRole" { + OrganizationRole.entries.forAll { role -> + role.level shouldBe CompoundHierarchyId.ORGANIZATION_LEVEL + } + } + + "be correct for ProductRole" { + ProductRole.entries.forAll { role -> + role.level shouldBe CompoundHierarchyId.PRODUCT_LEVEL + } + } + + "be correct for RepositoryRole" { + RepositoryRole.entries.forAll { role -> + role.level shouldBe CompoundHierarchyId.REPOSITORY_LEVEL + } + } + } +}) diff --git a/components/authorization/backend/src/test/kotlin/routes/AuthorizedRoutesTest.kt b/components/authorization/backend/src/test/kotlin/routes/AuthorizedRoutesTest.kt index cd71d7e602..39d9307d39 100644 --- a/components/authorization/backend/src/test/kotlin/routes/AuthorizedRoutesTest.kt +++ b/components/authorization/backend/src/test/kotlin/routes/AuthorizedRoutesTest.kt @@ -66,7 +66,9 @@ import org.eclipse.apoapsis.ortserver.components.authorization.rights.Organizati import org.eclipse.apoapsis.ortserver.components.authorization.rights.PermissionChecker import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService +import org.eclipse.apoapsis.ortserver.components.authorization.service.InvalidHierarchyIdException import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId import org.eclipse.apoapsis.ortserver.model.HierarchyId import org.eclipse.apoapsis.ortserver.model.OrganizationId @@ -106,6 +108,9 @@ class AuthorizedRoutesTest : WordSpec() { exception { call, _ -> call.respond(HttpStatusCode.Forbidden) } + exception { call, _ -> + call.respond(HttpStatusCode.NotFound) + } } routing { @@ -190,6 +195,26 @@ class AuthorizedRoutesTest : WordSpec() { } } + "support checks for an authenticated principal" { + runAuthorizationTest( + mockk(), + routeBuilder = { + route("test") { + get(testDocs) { + val principal = requirePrincipal() + principal.username shouldBe USERNAME + principal.effectiveRole.elementId shouldBe CompoundHierarchyId.WILDCARD + + call.respond(HttpStatusCode.OK) + } + } + } + ) { client -> + val response = client.get("test") + response.status shouldBe HttpStatusCode.OK + } + } + "support GET with an organization permission" { runAuthorizationTest( OrganizationPermission.WRITE_SECRETS, @@ -360,6 +385,116 @@ class AuthorizedRoutesTest : WordSpec() { } } } + + "support GET with a path" { + runAuthorizationTest( + OrganizationPermission.WRITE_SECRETS, + routeBuilder = { + get( + "test/{organizationId}", + testDocs, + requirePermission(OrganizationPermission.WRITE_SECRETS) + ) { + call.principal().shouldNotBeNull { + username shouldBe USERNAME + } + + call.respond(HttpStatusCode.OK) + } + } + ) { client -> + val response = client.get("test/$ID_PARAMETER") + response.status shouldBe HttpStatusCode.OK + } + } + + "support POST with a path" { + runAuthorizationTest( + OrganizationPermission.MANAGE_GROUPS, + routeBuilder = { + post( + "test/{organizationId}", + testDocs, + requirePermission(OrganizationPermission.MANAGE_GROUPS) + ) { + call.principal().shouldNotBeNull { + username shouldBe USERNAME + } + + call.respond(HttpStatusCode.OK) + } + } + ) { client -> + val response = client.post("test/$ID_PARAMETER") + response.status shouldBe HttpStatusCode.OK + } + } + + "support PATCH with a path" { + runAuthorizationTest( + OrganizationPermission.CREATE_PRODUCT, + routeBuilder = { + patch( + "test/{organizationId}", + testDocs, + requirePermission(OrganizationPermission.CREATE_PRODUCT) + ) { + call.principal().shouldNotBeNull { + username shouldBe USERNAME + } + + call.respond(HttpStatusCode.OK) + } + } + ) { client -> + val response = client.patch("test/$ID_PARAMETER") + response.status shouldBe HttpStatusCode.OK + } + } + + "support PUT with a path" { + runAuthorizationTest( + OrganizationPermission.READ_PRODUCTS, + routeBuilder = { + put( + "test/{organizationId}", + testDocs, + requirePermission(OrganizationPermission.READ_PRODUCTS) + ) { + call.principal().shouldNotBeNull { + username shouldBe USERNAME + } + + call.respond(HttpStatusCode.OK) + } + } + ) { client -> + val response = client.put("test/$ID_PARAMETER") + response.status shouldBe HttpStatusCode.OK + } + } + + "support DELETE with a path" { + runAuthorizationTest( + OrganizationPermission.WRITE, + routeBuilder = { + delete( + "test/{organizationId}", + testDocs, + requirePermission(OrganizationPermission.WRITE) + ) { + call.principal().shouldNotBeNull { + username shouldBe USERNAME + } + + call.respond(HttpStatusCode.OK) + } + } + ) { client -> + val response = client.delete("test/$ID_PARAMETER") + response.status shouldBe HttpStatusCode.OK + } + } } "failed authorization checks" should { @@ -453,6 +588,29 @@ class AuthorizedRoutesTest : WordSpec() { } } } + + "exceptions" should { + "be mapped to correct status codes" { + val service = mockk { + coEvery { checkPermissions(any(), any(), any()) } throws + InvalidHierarchyIdException(OrganizationId(42)) + } + + runAuthorizationTest( + service, + routeBuilder = { + route("test/{organizationId}") { + get(testDocs, requirePermission(OrganizationPermission.READ)) { + call.respond(HttpStatusCode.OK) + } + } + } + ) { client -> + val response = client.get("test/$ID_PARAMETER") + response.status shouldBe HttpStatusCode.NotFound + } + } + } } } diff --git a/components/authorization/backend/src/test/kotlin/routes/OrtServerPrincipalTest.kt b/components/authorization/backend/src/test/kotlin/routes/OrtServerPrincipalTest.kt index aea8269b90..28dd4105db 100644 --- a/components/authorization/backend/src/test/kotlin/routes/OrtServerPrincipalTest.kt +++ b/components/authorization/backend/src/test/kotlin/routes/OrtServerPrincipalTest.kt @@ -25,10 +25,14 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.shouldBe +import io.ktor.server.auth.principal +import io.ktor.server.routing.RoutingContext + import io.mockk.every import io.mockk.mockk import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole +import org.eclipse.apoapsis.ortserver.components.authorization.routes.OrtServerPrincipal.Companion.requirePrincipal class OrtServerPrincipalTest : WordSpec({ "create()" should { @@ -58,7 +62,8 @@ class OrtServerPrincipalTest : WordSpec({ userId = "user-id", username = "username", fullName = "Full Name", - role = mockk() + role = mockk(), + validationException = null ) principal.isAuthorized shouldBe true @@ -69,11 +74,25 @@ class OrtServerPrincipalTest : WordSpec({ userId = "user-id", username = "username", fullName = "Full Name", - role = null + role = null, + validationException = null ) principal.isAuthorized shouldBe false } + + "re-throw a validation exception if present" { + val exception = IllegalStateException("Validation failed") + val principal = OrtServerPrincipal.fromException(exception) + + principal.userId shouldBe "" + principal.username shouldBe "" + principal.fullName shouldBe "" + + shouldThrow { + principal.isAuthorized + } shouldBe exception + } } "effectiveRole" should { @@ -83,7 +102,8 @@ class OrtServerPrincipalTest : WordSpec({ userId = "user-id", username = "username", fullName = "Full Name", - role = effectiveRole + role = effectiveRole, + validationException = null ) principal.effectiveRole shouldBe effectiveRole @@ -94,7 +114,8 @@ class OrtServerPrincipalTest : WordSpec({ userId = "user-id", username = "username", fullName = "Full Name", - role = null + role = null, + validationException = null ) shouldThrow { @@ -102,4 +123,16 @@ class OrtServerPrincipalTest : WordSpec({ } } } + + "requirePrincipal()" should { + "throw an exception if no principal is present in the routing context" { + val context = mockk { + every { call.principal() } returns null + } + + shouldThrow { + context.requirePrincipal() + } + } + } }) diff --git a/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt b/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt index 2b0d9caeef..8e3d06ab56 100644 --- a/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt +++ b/components/authorization/backend/src/test/kotlin/service/DbAuthorizationServiceTest.kt @@ -19,6 +19,7 @@ package org.eclipse.apoapsis.ortserver.components.authorization.service +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.WordSpec import io.kotest.inspectors.forAll import io.kotest.matchers.collections.shouldBeSingleton @@ -29,6 +30,7 @@ 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.string.shouldContain import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole @@ -105,36 +107,33 @@ class DbAuthorizationServiceTest : WordSpec() { } } - "return an object with no permissions if resolving the product ID fails" { + "throw an InvalidHierarchyIdException if resolving the product ID fails" { val service = createService() - val missingProductId = ProductId(-1L) - val effectiveRole = service.getEffectiveRole( - USER_ID, - missingProductId - ) + val exception = shouldThrow { + service.getEffectiveRole( + USER_ID, + missingProductId + ) + } - effectiveRole.elementId shouldBe CompoundHierarchyId.forProduct(OrganizationId(-1L), missingProductId) - checkPermissions(effectiveRole) + exception.hierarchyId shouldBe missingProductId } - "return an object with no permissions if resolving the repository ID fails" { + "throw an IllegalHierarchyIdException if resolving the repository ID fails" { val service = createService() val missingRepositoryId = RepositoryId(-1L) - val effectiveRole = service.getEffectiveRole( - USER_ID, - missingRepositoryId - ) + val exception = shouldThrow { + service.getEffectiveRole( + USER_ID, + missingRepositoryId + ) + } - effectiveRole.elementId shouldBe CompoundHierarchyId.forRepository( - OrganizationId(-1L), - ProductId(-1L), - missingRepositoryId - ) - checkPermissions(effectiveRole) + exception.hierarchyId shouldBe missingRepositoryId } "return an object with no permissions for a user without role assignments" { @@ -434,16 +433,20 @@ class DbAuthorizationServiceTest : WordSpec() { } } - "handle an invalid compound hierarchy ID gracefully" { + "throw an InvalidHierarchyIdException for an invalid hierarchy ID" { val service = createService() + val invalidId = ProductId(-1L) - val effectiveRole = service.checkPermissions( - USER_ID, - ProductId(-1L), - HierarchyPermissions.permissions(ProductPermission.READ) - ) + val exception = shouldThrow { + service.checkPermissions( + USER_ID, + invalidId, + HierarchyPermissions.permissions(ProductPermission.READ) + ) + } - effectiveRole should beNull() + exception.hierarchyId shouldBe invalidId + exception.message shouldContain "Could not resolve hierarchy ID" } } @@ -462,7 +465,7 @@ class DbAuthorizationServiceTest : WordSpec() { checkPermissions(effectiveRole, RepositoryRole.READER) val effectiveRoleProduct = service.getEffectiveRole(USER_ID, repositoryCompoundId.parent!!) - checkPermissions(effectiveRoleProduct) + checkPermissions(effectiveRoleProduct, ProductRole.READER) } "create a new role assignment on product level" { @@ -482,7 +485,7 @@ class DbAuthorizationServiceTest : WordSpec() { checkPermissions(effectiveRole, ProductRole.WRITER) val effectiveRoleOrg = service.getEffectiveRole(USER_ID, productCompoundId.parent!!) - checkPermissions(effectiveRoleOrg) + checkPermissions(effectiveRoleOrg, OrganizationRole.READER) } "create a new role assignment on organization level" { @@ -671,10 +674,10 @@ class DbAuthorizationServiceTest : WordSpec() { organizationAdminUser ) - users[USER_ID] shouldContainExactlyInAnyOrder listOf(RepositoryRole.READER) - users[writerUser] shouldContainExactlyInAnyOrder listOf(RepositoryRole.WRITER) - users[productAdminUser] shouldContainExactlyInAnyOrder listOf(ProductRole.ADMIN) - users[organizationAdminUser] shouldContainExactlyInAnyOrder listOf(OrganizationRole.ADMIN) + users[USER_ID] shouldBe RepositoryRole.READER + users[writerUser] shouldBe RepositoryRole.WRITER + users[productAdminUser] shouldBe RepositoryRole.ADMIN + users[organizationAdminUser] shouldBe RepositoryRole.ADMIN } "list users with assignments on organization level" { @@ -684,59 +687,55 @@ class DbAuthorizationServiceTest : WordSpec() { ) val writerUser = "writer-user" val adminUser = "admin-user" + val repoReaderUser = "repo-reader-user" val service = createService() service.assignRole(USER_ID, OrganizationRole.READER, organizationCompoundId) service.assignRole(writerUser, OrganizationRole.WRITER, organizationCompoundId) service.assignRole(adminUser, OrganizationRole.ADMIN, organizationCompoundId) - service.assignRole("repo-reader-user", RepositoryRole.READER, repositoryCompoundId) + service.assignRole(repoReaderUser, RepositoryRole.READER, repositoryCompoundId) val users = service.listUsers(organizationCompoundId) users.keys shouldContainExactlyInAnyOrder listOf( USER_ID, writerUser, - adminUser + adminUser, + repoReaderUser ) - users[USER_ID] shouldContainExactlyInAnyOrder listOf(OrganizationRole.READER) - users[writerUser] shouldContainExactlyInAnyOrder listOf(OrganizationRole.WRITER) - users[adminUser] shouldContainExactlyInAnyOrder listOf(OrganizationRole.ADMIN) + users[USER_ID] shouldBe OrganizationRole.READER + users[writerUser] shouldBe OrganizationRole.WRITER + users[adminUser] shouldBe OrganizationRole.ADMIN + users[repoReaderUser] shouldBe OrganizationRole.READER } - "list all roles assigned to a user" { + "list users with implicit rights from lower levels" { val repositoryCompoundId = repositoryCompoundId() - val product2 = dbExtension.fixtures.createProduct("otherProduct") val service = createService() service.assignRole(USER_ID, RepositoryRole.READER, repositoryCompoundId) + + val users = service.listUsers(repositoryCompoundId.parent!!) + users shouldHaveSize 1 + + users[USER_ID] shouldBe ProductRole.READER + } + + "inherit roles from higher levels" { + val repositoryCompoundId = repositoryCompoundId() + val service = createService() + service.assignRole( USER_ID, - ProductRole.ADMIN, - CompoundHierarchyId.forProduct( - OrganizationId(dbExtension.fixtures.organization.id), - ProductId(product2.id) - ) - ) - service.assignRole( - USER_ID, - ProductRole.WRITER, - CompoundHierarchyId.forProduct( - OrganizationId(dbExtension.fixtures.organization.id), - ProductId(dbExtension.fixtures.product.id) - ) - ) - service.assignRole( - USER_ID, - OrganizationRole.ADMIN, + OrganizationRole.WRITER, CompoundHierarchyId.forOrganization(OrganizationId(dbExtension.fixtures.organization.id)) ) + service.assignRole(USER_ID, RepositoryRole.READER, repositoryCompoundId) val users = service.listUsers(repositoryCompoundId) - users[USER_ID] shouldContainExactlyInAnyOrder listOf( - RepositoryRole.READER, - ProductRole.WRITER, - OrganizationRole.ADMIN - ) + users shouldHaveSize 1 + + users[USER_ID] shouldBe RepositoryRole.WRITER } "not include super users" { @@ -771,7 +770,7 @@ class DbAuthorizationServiceTest : WordSpec() { users.entries.shouldBeSingleton { (key, value) -> key shouldBe USER_ID - value shouldBe setOf(RepositoryRole.READER) + value shouldBe RepositoryRole.READER } } } @@ -1121,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/authorization/backend/src/test/kotlin/service/KeycloakUserServiceTest.kt b/components/authorization/backend/src/test/kotlin/service/KeycloakUserServiceTest.kt index edd8dc7c94..07887f99b1 100644 --- a/components/authorization/backend/src/test/kotlin/service/KeycloakUserServiceTest.kt +++ b/components/authorization/backend/src/test/kotlin/service/KeycloakUserServiceTest.kt @@ -31,6 +31,7 @@ import io.mockk.mockk import io.mockk.runs import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClient +import org.eclipse.apoapsis.ortserver.clients.keycloak.KeycloakClientException import org.eclipse.apoapsis.ortserver.clients.keycloak.User as KeycloakUser import org.eclipse.apoapsis.ortserver.clients.keycloak.UserId import org.eclipse.apoapsis.ortserver.clients.keycloak.UserName @@ -142,6 +143,44 @@ class KeycloakUserServiceTest : WordSpec({ users shouldContainExactly expectedUsers } + + "ignore unknown user IDs" { + val existingKeycloakUser = createKeycloakUser(1) + val expectedUser = createUser(1) + val userIds = setOf(existingKeycloakUser.username.value, "non-existing-user") + + val client = mockk { + coEvery { getUser(existingKeycloakUser.username) } returns existingKeycloakUser + coEvery { getUser(UserName("non-existing-user")) } throws KeycloakClientException("User not found") + } + + val service = KeycloakUserService(client) + val users = service.getUsersById(userIds) + + users shouldContainExactly setOf(expectedUser) + } + } + + "existsUsername" should { + "return true if the username can be resolved" { + val username = "existing-user" + val client = mockk { + coEvery { getUser(UserName(username)) } returns createKeycloakUser(1) + } + + val service = KeycloakUserService(client) + service.existsUser(username) shouldBe true + } + + "return false if the username does not exist" { + val username = "non-existing-user" + val client = mockk { + coEvery { getUser(UserName(username)) } throws KeycloakClientException("User not found") + } + + val service = KeycloakUserService(client) + service.existsUser(username) shouldBe false + } } }) 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/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..69eb49482e 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.existsUser(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.existsUser(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..cfc6f5193e 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.existsUser(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.existsUser(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..79074f7f13 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.existsUser(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.existsUser(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..4258c85880 100644 --- a/core/src/main/kotlin/di/Module.kt +++ b/core/src/main/kotlin/di/Module.kt @@ -22,15 +22,15 @@ package org.eclipse.apoapsis.ortserver.core.di import com.typesafe.config.ConfigFactory import io.ktor.server.config.ApplicationConfig -import io.ktor.server.config.tryGetString 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.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 +194,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 { 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/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/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..76ebda9897 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,16 +108,7 @@ 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( @@ -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) {