|
| 1 | +/* |
| 2 | + * Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>) |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + * |
| 16 | + * SPDX-License-Identifier: Apache-2.0 |
| 17 | + * License-Filename: LICENSE |
| 18 | + */ |
| 19 | + |
| 20 | +package org.eclipse.apoapsis.ortserver.components.authorization.routes |
| 21 | + |
| 22 | +import com.auth0.jwt.interfaces.Payload |
| 23 | + |
| 24 | +import io.github.smiley4.ktoropenapi.config.RouteConfig |
| 25 | +import io.github.smiley4.ktoropenapi.delete |
| 26 | +import io.github.smiley4.ktoropenapi.get |
| 27 | +import io.github.smiley4.ktoropenapi.patch |
| 28 | +import io.github.smiley4.ktoropenapi.post |
| 29 | +import io.github.smiley4.ktoropenapi.put |
| 30 | + |
| 31 | +import io.ktor.server.application.ApplicationCall |
| 32 | +import io.ktor.server.routing.Route |
| 33 | +import io.ktor.server.routing.RouteSelector |
| 34 | +import io.ktor.server.routing.RouteSelectorEvaluation |
| 35 | +import io.ktor.server.routing.RoutingContext |
| 36 | +import io.ktor.server.routing.RoutingPipelineCall |
| 37 | +import io.ktor.server.routing.RoutingResolveContext |
| 38 | +import io.ktor.util.AttributeKey |
| 39 | + |
| 40 | +import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole |
| 41 | +import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService |
| 42 | + |
| 43 | +/** |
| 44 | + * Create an [OrtServerPrincipal] for this [ApplicationCall]. If an [AuthorizationChecker] is present in the current |
| 45 | + * context, use it to an [EffectiveRole] and perform an authorization check. Result is *null* if this check fails. |
| 46 | + */ |
| 47 | +suspend fun ApplicationCall.createAuthorizedPrincipal( |
| 48 | + authorizationService: AuthorizationService, |
| 49 | + payload: Payload |
| 50 | +): OrtServerPrincipal? = |
| 51 | + (this as? RoutingPipelineCall)?.let { routingCall -> |
| 52 | + val checker = routingCall.route.findAuthorizationChecker() |
| 53 | + |
| 54 | + val effectiveRole = if (checker != null) { |
| 55 | + checker.loadEffectiveRole( |
| 56 | + service = authorizationService, |
| 57 | + userId = payload.getClaim("preferred_username").asString(), |
| 58 | + call = this |
| 59 | + ).takeIf { checker.checkAuthorization(it) } |
| 60 | + } else { |
| 61 | + EffectiveRole.EMPTY |
| 62 | + } |
| 63 | + |
| 64 | + effectiveRole?.let { OrtServerPrincipal.create(payload, it) } |
| 65 | + } |
| 66 | + |
| 67 | +/** |
| 68 | + * Create a new [Route] for HTTP GET requests that performs an automatic authorization check using the given [checker]. |
| 69 | + */ |
| 70 | +fun Route.get( |
| 71 | + builder: RouteConfig.() -> Unit, |
| 72 | + checker: AuthorizationChecker, |
| 73 | + body: suspend RoutingContext.() -> Unit |
| 74 | +): Route = documentedAuthorized(checker) { get(builder, body) } |
| 75 | + |
| 76 | +/** |
| 77 | + * Create a new [Route] for HTTP POST requests that performs an automatic authorization check using the given [checker]. |
| 78 | + */ |
| 79 | +fun Route.post( |
| 80 | + builder: RouteConfig.() -> Unit, |
| 81 | + checker: AuthorizationChecker, |
| 82 | + body: suspend RoutingContext.() -> Unit |
| 83 | +): Route = documentedAuthorized(checker) { post(builder, body) } |
| 84 | + |
| 85 | +/** |
| 86 | + * Create a new [Route] for HTTP PATCH requests that performs an automatic authorization check using the given |
| 87 | + * [checker]. |
| 88 | + */ |
| 89 | +fun Route.patch( |
| 90 | + builder: RouteConfig.() -> Unit, |
| 91 | + checker: AuthorizationChecker, |
| 92 | + body: suspend RoutingContext.() -> Unit |
| 93 | +): Route = documentedAuthorized(checker) { patch(builder, body) } |
| 94 | + |
| 95 | +/** |
| 96 | + * Create a new [Route] for HTTP PUT requests that performs an automatic authorization check using the given |
| 97 | + * [checker]. |
| 98 | + */ |
| 99 | +fun Route.put( |
| 100 | + builder: RouteConfig.() -> Unit, |
| 101 | + checker: AuthorizationChecker, |
| 102 | + body: suspend RoutingContext.() -> Unit |
| 103 | +): Route = documentedAuthorized(checker) { put(builder, body) } |
| 104 | + |
| 105 | +/** |
| 106 | + * Create a new [Route] for HTTP DELETE requests that performs an automatic authorization check using the given |
| 107 | + * [checker]. |
| 108 | + */ |
| 109 | +fun Route.delete( |
| 110 | + builder: RouteConfig.() -> Unit, |
| 111 | + checker: AuthorizationChecker, |
| 112 | + body: suspend RoutingContext.() -> Unit |
| 113 | +): Route = documentedAuthorized(checker) { delete(builder, body) } |
| 114 | + |
| 115 | +/** |
| 116 | + * Generic function to create a new [Route] that performs an automatic authorization check using the given [checker]. |
| 117 | + * The content of the route is defined by the given [build] function. |
| 118 | + */ |
| 119 | +private fun Route.documentedAuthorized(checker: AuthorizationChecker, build: Route.() -> Unit): Route { |
| 120 | + val authorizedRoute = createChild(authorizedRouteSelector(checker.toString())) |
| 121 | + authorizedRoute.attributes.put(AuthorizationCheckerKey, checker) |
| 122 | + authorizedRoute.build() |
| 123 | + return authorizedRoute |
| 124 | +} |
| 125 | + |
| 126 | +/** |
| 127 | + * Create a [RouteSelector] for a new authorized [Route] whose string representation is derived from the given [tag]. |
| 128 | + */ |
| 129 | +private fun authorizedRouteSelector(tag: String): RouteSelector = |
| 130 | + object : RouteSelector() { |
| 131 | + override suspend fun evaluate( |
| 132 | + context: RoutingResolveContext, |
| 133 | + segmentIndex: Int |
| 134 | + ): RouteSelectorEvaluation = RouteSelectorEvaluation.Transparent |
| 135 | + |
| 136 | + override fun toString(): String { |
| 137 | + return "(authorized $tag)" |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | +/** |
| 142 | + * Search for an [AuthorizationChecker] object in the context of the current [Route]. The checker has been defined |
| 143 | + * using the routes DSL. It may be available in this route or in any of its parent routes. |
| 144 | + */ |
| 145 | +private fun Route.findAuthorizationChecker(): AuthorizationChecker? = |
| 146 | + this.attributes.getOrNull(AuthorizationCheckerKey) |
| 147 | + ?: parent?.findAuthorizationChecker() |
| 148 | + |
| 149 | +/** |
| 150 | + * Constant for a key in the attributes of a [Route] to store an [AuthorizationChecker]. |
| 151 | + */ |
| 152 | +private val AuthorizationCheckerKey = AttributeKey<AuthorizationChecker>("AuthorizationCheckerKey") |
| 153 | + |
| 154 | +/** |
| 155 | + * An object defining constants for the names of supported authentication providers. |
| 156 | + */ |
| 157 | +object AuthenticationProviders { |
| 158 | + /** |
| 159 | + * The name of the authentication provider for authorization based on JWT tokens. |
| 160 | + */ |
| 161 | + const val TOKEN_PROVIDER = "token" |
| 162 | +} |
0 commit comments