Skip to content

Commit 2cbd90d

Browse files
committed
feat(authorization): Rework authorized routes
To perform authorization checks in routes automatically, now call the `checkPermissions` function of the authorization service. Simplify the `AuthorizationChecker` interface. The two-step logic of first loading the effective role and then checking permissions is not necessary; a single function to load and check permissions is sufficient. Signed-off-by: Oliver Heger <[email protected]>
1 parent f33bb66 commit 2cbd90d

File tree

5 files changed

+189
-144
lines changed

5 files changed

+189
-144
lines changed

components/authorization/backend/src/main/kotlin/routes/AuthorizationChecker.kt

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ package org.eclipse.apoapsis.ortserver.components.authorization.routes
2222
import io.ktor.server.application.ApplicationCall
2323

2424
import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole
25+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.HierarchyPermissions
2526
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission
27+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole
2628
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission
2729
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission
2830
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
@@ -51,15 +53,10 @@ interface AuthorizationChecker {
5153
* Use the provided [service] to load the [EffectiveRole] of the user with the given [userId] for the current
5254
* [call]. A typical implementation will figure out the ID of an element in the hierarchy (organization, product,
5355
* or repository) based on current call parameters. Then it can invoke the [service] to query the permissions on
54-
* this element.
56+
* this element. If the permission check is successful, return a properly initialized [EffectiveRole] instance.
57+
* Otherwise, return *null*, which cause the request to fail with a 403 error.
5558
*/
56-
suspend fun loadEffectiveRole(service: AuthorizationService, userId: String, call: ApplicationCall): EffectiveRole
57-
58-
/**
59-
* Check whether the given [effectiveRole] contains the permission(s) required by this [AuthorizationChecker].
60-
* This function is called with the [EffectiveRole] that was loaded via [loadEffectiveRole].
61-
*/
62-
fun checkAuthorization(effectiveRole: EffectiveRole): Boolean
59+
suspend fun loadEffectiveRole(service: AuthorizationService, userId: String, call: ApplicationCall): EffectiveRole?
6360
}
6461

6562
/** The name of the request parameter referring to the organization ID. */
@@ -80,11 +77,12 @@ fun requirePermission(permission: OrganizationPermission): AuthorizationChecker
8077
service: AuthorizationService,
8178
userId: String,
8279
call: ApplicationCall
83-
): EffectiveRole =
84-
service.getEffectiveRole(userId, OrganizationId(call.requireIdParameter(ORGANIZATION_ID_PARAM)))
85-
86-
override fun checkAuthorization(effectiveRole: EffectiveRole): Boolean =
87-
effectiveRole.hasOrganizationPermission(permission)
80+
): EffectiveRole? =
81+
service.checkPermissions(
82+
userId,
83+
OrganizationId(call.requireIdParameter(ORGANIZATION_ID_PARAM)),
84+
HierarchyPermissions.permissions(permission)
85+
)
8886

8987
override fun toString(): String = "RequireOrganizationPermission($permission)"
9088
}
@@ -98,11 +96,12 @@ fun requirePermission(permission: ProductPermission): AuthorizationChecker =
9896
service: AuthorizationService,
9997
userId: String,
10098
call: ApplicationCall
101-
): EffectiveRole =
102-
service.getEffectiveRole(userId, ProductId(call.requireIdParameter(PRODUCT_ID_PARAM)))
103-
104-
override fun checkAuthorization(effectiveRole: EffectiveRole): Boolean =
105-
effectiveRole.hasProductPermission(permission)
99+
): EffectiveRole? =
100+
service.checkPermissions(
101+
userId,
102+
ProductId(call.requireIdParameter(PRODUCT_ID_PARAM)),
103+
HierarchyPermissions.permissions(permission)
104+
)
106105

107106
override fun toString(): String = "RequireProductPermission($permission)"
108107
}
@@ -116,11 +115,12 @@ fun requirePermission(permission: RepositoryPermission): AuthorizationChecker =
116115
service: AuthorizationService,
117116
userId: String,
118117
call: ApplicationCall
119-
): EffectiveRole =
120-
service.getEffectiveRole(userId, RepositoryId(call.requireIdParameter(REPOSITORY_ID_PARAM)))
121-
122-
override fun checkAuthorization(effectiveRole: EffectiveRole): Boolean =
123-
effectiveRole.hasRepositoryPermission(permission)
118+
): EffectiveRole? =
119+
service.checkPermissions(
120+
userId,
121+
RepositoryId(call.requireIdParameter(REPOSITORY_ID_PARAM)),
122+
HierarchyPermissions.permissions(permission)
123+
)
124124

125125
override fun toString(): String = "RequireRepositoryPermission($permission)"
126126
}
@@ -134,11 +134,12 @@ fun requireSuperuser(): AuthorizationChecker =
134134
service: AuthorizationService,
135135
userId: String,
136136
call: ApplicationCall
137-
): EffectiveRole =
138-
service.getEffectiveRole(userId, CompoundHierarchyId.WILDCARD)
139-
140-
override fun checkAuthorization(effectiveRole: EffectiveRole): Boolean =
141-
effectiveRole.isSuperuser
137+
): EffectiveRole? =
138+
service.checkPermissions(
139+
userId,
140+
CompoundHierarchyId.WILDCARD,
141+
HierarchyPermissions.permissions(OrganizationRole.ADMIN)
142+
)?.takeIf { it.isSuperuser }
142143

143144
override fun toString(): String = "RequireSuperuser"
144145
}

components/authorization/backend/src/main/kotlin/routes/AuthorizedRoutes.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ suspend fun ApplicationCall.createAuthorizedPrincipal(
5252
(this as? RoutingPipelineCall)?.let { routingCall ->
5353
val checker = routingCall.route.findAuthorizationChecker()
5454

55-
val effectiveRole = checker?.loadEffectiveRole(
56-
service = authorizationService,
57-
userId = payload.getClaim("preferred_username").asString(),
58-
call = this
59-
) ?: EffectiveRole.EMPTY
55+
val effectiveRole = if (checker != null) {
56+
checker.loadEffectiveRole(
57+
service = authorizationService,
58+
userId = payload.getClaim("preferred_username").asString(),
59+
call = this
60+
)
61+
} else {
62+
EffectiveRole.EMPTY
63+
}
6064

6165
OrtServerPrincipal.create(payload, effectiveRole)
6266
}
@@ -122,9 +126,8 @@ private fun Route.documentedAuthorized(
122126
authorizedRoute.attributes.put(AuthorizationCheckerKey, checker)
123127

124128
val authorizedBody: suspend RoutingContext.() -> Unit = {
125-
val principal = call.principal<OrtServerPrincipal>() ?: throw AuthorizationException()
126-
127-
if (!checker.checkAuthorization(principal.effectiveRole)) {
129+
// Check whether an authorized principal is available in the call.
130+
if (call.principal<OrtServerPrincipal>()?.isAuthorized != true) {
128131
throw AuthorizationException()
129132
}
130133

components/authorization/backend/src/main/kotlin/routes/OrtServerPrincipal.kt

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ class OrtServerPrincipal(
3636
/** The full name of the principal. */
3737
val fullName: String,
3838

39-
/** The effective role computed for the principal. */
40-
val effectiveRole: EffectiveRole
39+
/**
40+
* The effective role computed for the principal. This can be *null* if either no authorization is required or the
41+
* authorization check failed. In the latter case, an exception is thrown when the role is accessed.
42+
*/
43+
private val role: EffectiveRole?
4144
) {
4245
companion object {
4346
/** Constant for the name of the claim containing the username. */
@@ -49,12 +52,26 @@ class OrtServerPrincipal(
4952
/**
5053
* Create an [OrtServerPrincipal] from the given JWT [payload] and [effectiveRole].
5154
*/
52-
fun create(payload: Payload, effectiveRole: EffectiveRole): OrtServerPrincipal =
55+
fun create(payload: Payload, effectiveRole: EffectiveRole?): OrtServerPrincipal =
5356
OrtServerPrincipal(
5457
userId = payload.subject,
5558
username = payload.getClaim(CLAIM_USERNAME).asString(),
5659
fullName = payload.getClaim(CLAIM_FULL_NAME).asString(),
57-
effectiveRole = effectiveRole
60+
role = effectiveRole
5861
)
5962
}
63+
64+
/**
65+
* A flag indicating whether the principal is authorized. If this is *true*, the effective role of the principal
66+
* can be accessed via [effectiveRole].
67+
*/
68+
val isAuthorized: Boolean
69+
get() = role != null
70+
71+
/**
72+
* The effective role of the principal if authorization was successful. Otherwise, accessing this property throws
73+
* an [AuthorizationException].
74+
*/
75+
val effectiveRole: EffectiveRole
76+
get() = role ?: throw AuthorizationException()
6077
}

0 commit comments

Comments
 (0)