Skip to content

Commit 9dd3bbd

Browse files
committed
feat(authorization): Rework permission checks in AuthorizationService
Introduce new `checkPermissions` functions that are based on `HierarchyPermissions` and check whether specific permissions are granted. These are going to be used for permission checks in the REST API. Rework the `getEffectiveRole` functions in `DbAuthorizationService` to use `HierarchyPermissions` as well. Modify the permission sets in the role classes, so that permissions granted on higher levels of the hierarchy are inherited downwards. Signed-off-by: Oliver Heger <[email protected]>
1 parent 0f70fbf commit 9dd3bbd

File tree

5 files changed

+310
-118
lines changed

5 files changed

+310
-118
lines changed

components/authorization/backend/src/main/kotlin/rights/OrganizationRole.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,25 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights
2222
/**
2323
* This enum contains the available roles for [organizations][org.eclipse.apoapsis.ortserver.model.Organization]. It
2424
* maps the permissions available for an organization to the default roles [READER], [WRITER], and [ADMIN].
25+
*
26+
* Notes:
27+
* - Permissions are inherited down the hierarchy. This is implemented by including the lower level permissions
28+
* of the corresponding roles in the sets of permissions managed by the single instances.
29+
* - The constants are expected to be listed in increasing order of permissions.
2530
*/
2631
enum class OrganizationRole(
2732
override val organizationPermissions: Set<OrganizationPermission>,
28-
override val productPermissions: Set<ProductPermission> = emptySet(),
29-
override val repositoryPermissions: Set<RepositoryPermission> = emptySet()
33+
override val productPermissions: Set<ProductPermission>,
34+
override val repositoryPermissions: Set<RepositoryPermission>
3035
) : Role {
3136
/** A role that grants read permissions for an [org.eclipse.apoapsis.ortserver.model.Organization]. */
3237
READER(
3338
organizationPermissions = setOf(
3439
OrganizationPermission.READ,
3540
OrganizationPermission.READ_PRODUCTS
36-
)
41+
),
42+
productPermissions = ProductRole.READER.productPermissions,
43+
repositoryPermissions = RepositoryRole.READER.repositoryPermissions
3744
),
3845

3946
/** A role that grants write permissions for an [org.eclipse.apoapsis.ortserver.model.Organization]. */
@@ -43,7 +50,9 @@ enum class OrganizationRole(
4350
OrganizationPermission.WRITE,
4451
OrganizationPermission.READ_PRODUCTS,
4552
OrganizationPermission.CREATE_PRODUCT
46-
)
53+
),
54+
productPermissions = ProductRole.WRITER.productPermissions,
55+
repositoryPermissions = RepositoryRole.WRITER.repositoryPermissions
4756
),
4857

4958
/**

components/authorization/backend/src/main/kotlin/rights/ProductRole.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,24 @@ package org.eclipse.apoapsis.ortserver.components.authorization.rights
2222
/**
2323
* This enum contains the available roles for [products][org.eclipse.apoapsis.ortserver.model.Product]. It
2424
* maps the permissions available for a product to the default roles [READER], [WRITER], and [ADMIN].
25+
*
26+
* Notes:
27+
* - Permissions are inherited down the hierarchy. This is implemented by including the lower level permissions
28+
* of the corresponding roles in the sets of permissions managed by the single instances.
29+
* - The constants are expected to be listed in increasing order of permissions.
2530
*/
2631
enum class ProductRole(
2732
override val organizationPermissions: Set<OrganizationPermission> = setOf(OrganizationPermission.READ),
2833
override val productPermissions: Set<ProductPermission>,
29-
override val repositoryPermissions: Set<RepositoryPermission> = emptySet()
34+
override val repositoryPermissions: Set<RepositoryPermission>
3035
) : Role {
3136
/** A role that grants read permissions for a [org.eclipse.apoapsis.ortserver.model.Product]. */
3237
READER(
3338
productPermissions = setOf(
3439
ProductPermission.READ,
3540
ProductPermission.READ_REPOSITORIES
36-
)
41+
),
42+
repositoryPermissions = RepositoryRole.READER.repositoryPermissions
3743
),
3844

3945
/** A role that grants write permissions for a [org.eclipse.apoapsis.ortserver.model.Product]. */
@@ -44,7 +50,8 @@ enum class ProductRole(
4450
ProductPermission.READ_REPOSITORIES,
4551
ProductPermission.CREATE_REPOSITORY,
4652
ProductPermission.TRIGGER_ORT_RUN
47-
)
53+
),
54+
repositoryPermissions = RepositoryRole.WRITER.repositoryPermissions
4855
),
4956

5057
/**

components/authorization/backend/src/main/kotlin/service/AuthorizationService.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.eclipse.apoapsis.ortserver.components.authorization.service
2121

2222
import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole
23+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.PermissionChecker
2324
import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role
2425
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2526
import org.eclipse.apoapsis.ortserver.model.HierarchyId
@@ -29,6 +30,29 @@ import org.eclipse.apoapsis.ortserver.model.HierarchyId
2930
* hierarchy.
3031
*/
3132
interface AuthorizationService {
33+
/**
34+
* Check whether the user identified by [userId] has the permissions defined by the given [checker] on the element
35+
* identified by [compoundHierarchyId]. If the check is successful, the function returns an [EffectiveRole] object
36+
* with the requested permissions and the [compoundHierarchyId]; otherwise, it returns *null*.
37+
*/
38+
suspend fun checkPermissions(
39+
userId: String,
40+
compoundHierarchyId: CompoundHierarchyId,
41+
checker: PermissionChecker
42+
): EffectiveRole?
43+
44+
/**
45+
* Check whether the user identified by [userId] has the permissions defined by the given [checker] on the element
46+
* identified by [hierarchyId]. This function constructs a [CompoundHierarchyId] based on the passed in
47+
* [hierarchyId]. If such a [CompoundHierarchyId] is already known, using the overloaded function is more
48+
* efficient.
49+
*/
50+
suspend fun checkPermissions(
51+
userId: String,
52+
hierarchyId: HierarchyId,
53+
checker: PermissionChecker
54+
): EffectiveRole?
55+
3256
/**
3357
* Return an [EffectiveRole] object for the specified [userId] that contains all permissions for this user on the
3458
* element identified by [compoundHierarchyId].

components/authorization/backend/src/main/kotlin/service/DbAuthorizationService.kt

Lines changed: 98 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ package org.eclipse.apoapsis.ortserver.components.authorization.service
2121

2222
import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable
2323
import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole
24+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.HierarchyPermissions
2425
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission
2526
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole
27+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.PermissionChecker
2628
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission
2729
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductRole
2830
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission
@@ -63,24 +65,61 @@ class DbAuthorizationService(
6365
/** The database to use. */
6466
private val db: Database
6567
) : AuthorizationService {
68+
override suspend fun checkPermissions(
69+
userId: String,
70+
compoundHierarchyId: CompoundHierarchyId,
71+
checker: PermissionChecker
72+
): EffectiveRole? {
73+
val roleAssignments = loadAssignments(userId, compoundHierarchyId)
74+
75+
val permissions = HierarchyPermissions.create(roleAssignments, checker)
76+
return if (permissions.hasPermission(compoundHierarchyId)) {
77+
EffectiveRoleImpl(
78+
elementId = compoundHierarchyId,
79+
isSuperuser = permissions.isSuperuser(),
80+
permissions = checker
81+
)
82+
} else {
83+
null
84+
}
85+
}
86+
87+
override suspend fun checkPermissions(
88+
userId: String,
89+
hierarchyId: HierarchyId,
90+
checker: PermissionChecker
91+
): EffectiveRole? {
92+
val compoundHierarchyId = resolveCompoundId(hierarchyId)
93+
94+
return compoundHierarchyId.takeUnless { it.isInvalid() }?.let {
95+
checkPermissions(userId, it, checker)
96+
}
97+
}
98+
6699
override suspend fun getEffectiveRole(
67100
userId: String,
68101
compoundHierarchyId: CompoundHierarchyId
69102
): EffectiveRole {
70103
val roleAssignments = loadAssignments(userId, compoundHierarchyId)
71-
val permissions = roleAssignments.map { it.toHierarchyPermissions() }
72-
.takeUnless { it.isEmpty() }?.reduce(::reducePermissions) ?: EMPTY_PERMISSIONS
73-
val isSuperuser = roleAssignments.any {
74-
it[RoleAssignmentsTable.organizationId] == null &&
75-
it[RoleAssignmentsTable.productId] == null &&
76-
it[RoleAssignmentsTable.repositoryId] == null &&
77-
it.extractRole() == OrganizationRole.ADMIN
78-
}
79-
80-
return EffectiveRoleImpl(
104+
val roles = rolesForLevel(compoundHierarchyId).reversed().asSequence()
105+
106+
// Check for all roles on the current level, starting with the ADMIN role, whether all its permissions are
107+
// granted. This is then the effective role. This assumes that constants in the enums for roles are ordered
108+
// by the number of permissions they grant in ascending order. If this changes, there should be failing tests.
109+
return roles.mapNotNull { role ->
110+
val permissionChecker = HierarchyPermissions.permissions(role)
111+
HierarchyPermissions.create(roleAssignments, permissionChecker)
112+
.takeIf { it.hasPermission(compoundHierarchyId) }?.let {
113+
EffectiveRoleImpl(
114+
elementId = compoundHierarchyId,
115+
isSuperuser = it.isSuperuser(),
116+
permissions = permissionChecker
117+
)
118+
}
119+
}.firstOrNull() ?: EffectiveRoleImpl(
81120
elementId = compoundHierarchyId,
82-
isSuperuser = isSuperuser,
83-
permissions = permissions
121+
isSuperuser = false,
122+
permissions = PermissionChecker()
84123
)
85124
}
86125

@@ -96,7 +135,7 @@ class DbAuthorizationService(
96135
EffectiveRoleImpl(
97136
elementId = compoundHierarchyId,
98137
isSuperuser = false,
99-
permissions = EMPTY_PERMISSIONS
138+
permissions = PermissionChecker()
100139
)
101140
} else {
102141
getEffectiveRole(userId, compoundHierarchyId)
@@ -206,20 +245,25 @@ class DbAuthorizationService(
206245

207246
/**
208247
* Load all role assignments for the given [userId] in the hierarchy defined by [compoundHierarchyId]. Return a
209-
* list of [HierarchyPermissions] instances for the entities that were found.
248+
* list with pairs of IDs and assigned roles for the entities that were found. The function selects assignments in
249+
* the whole hierarchy below the organization referenced by [compoundHierarchyId]. This makes sure that all
250+
* relevant assignments are found.
210251
*/
211252
private suspend fun loadAssignments(
212253
userId: String,
213254
compoundHierarchyId: CompoundHierarchyId
214-
): List<ResultRow> = db.dbQuery {
255+
): List<Pair<CompoundHierarchyId, Role>> = db.dbQuery {
215256
RoleAssignmentsTable.selectAll()
216257
.where {
217258
(RoleAssignmentsTable.userId eq userId) and (
218-
repositoryCondition(compoundHierarchyId) or
219-
productWildcardCondition(compoundHierarchyId) or
220-
organizationWildcardCondition()
259+
(RoleAssignmentsTable.organizationId eq compoundHierarchyId.organizationId?.value) or
260+
(RoleAssignmentsTable.organizationId eq null)
221261
)
222-
}.toList()
262+
}.mapNotNull { row ->
263+
row.extractRole()?.let { role ->
264+
row.extractHierarchyId() to role
265+
}
266+
}
223267
}
224268

225269
/**
@@ -249,27 +293,6 @@ class DbAuthorizationService(
249293
*/
250294
private const val INVALID_ID = -1L
251295

252-
/**
253-
* An internally used data class to store the available permissions on all levels of the hierarchy for a user.
254-
*/
255-
private data class HierarchyPermissions(
256-
/** The permissions granted on organization level. */
257-
val organizationPermissions: Set<OrganizationPermission>,
258-
259-
/** The permissions granted on product level. */
260-
val productPermissions: Set<ProductPermission>,
261-
262-
/** The permissions granted on repository level. */
263-
val repositoryPermissions: Set<RepositoryPermission>
264-
)
265-
266-
/** An instance of [HierarchyPermissions] with no permissions at all. */
267-
private val EMPTY_PERMISSIONS = HierarchyPermissions(
268-
organizationPermissions = emptySet(),
269-
productPermissions = emptySet(),
270-
repositoryPermissions = emptySet()
271-
)
272-
273296
/**
274297
* An implementation of the [EffectiveRole] interface used by [DbAuthorizationService].
275298
*/
@@ -279,7 +302,7 @@ private class EffectiveRoleImpl(
279302
override val isSuperuser: Boolean,
280303

281304
/** The permissions granted on the different levels of the hierarchy. */
282-
private val permissions: HierarchyPermissions
305+
private val permissions: PermissionChecker
283306
) : EffectiveRole {
284307
override fun hasOrganizationPermission(permission: OrganizationPermission): Boolean =
285308
permission in permissions.organizationPermissions
@@ -313,42 +336,38 @@ private fun ResultRow.extractRole(): Role? = runCatching {
313336
}.getOrNull()
314337

315338
/**
316-
* Obtain the information about roles from this [ResultRow] and construct a [HierarchyPermissions] object from it.
339+
* Extract the [CompoundHierarchyId] on the correct level from this [ResultRow].
317340
*/
318-
private fun ResultRow.toHierarchyPermissions(): HierarchyPermissions =
319-
extractRole()?.let { role ->
320-
HierarchyPermissions(
321-
organizationPermissions = role.organizationPermissions,
322-
productPermissions = role.productPermissions,
323-
repositoryPermissions = role.repositoryPermissions
324-
)
325-
} ?: EMPTY_PERMISSIONS
326-
327-
/**
328-
* Combine two [HierarchyPermissions] instances [p1] and [p2] by constructing the union of their permissions on all
329-
* levels.
330-
*/
331-
private fun reducePermissions(
332-
p1: HierarchyPermissions,
333-
p2: HierarchyPermissions
334-
): HierarchyPermissions =
335-
HierarchyPermissions(
336-
organizationPermissions = p1.organizationPermissions + p2.organizationPermissions,
337-
productPermissions = p1.productPermissions + p2.productPermissions,
338-
repositoryPermissions = p1.repositoryPermissions + p2.repositoryPermissions
339-
)
341+
private fun ResultRow.extractHierarchyId(): CompoundHierarchyId {
342+
val orgId = this[RoleAssignmentsTable.organizationId]?.let { OrganizationId(it.value) }
343+
if (orgId == null) {
344+
return CompoundHierarchyId.WILDCARD
345+
} else {
346+
val productId = this[RoleAssignmentsTable.productId]?.let { ProductId(it.value) }
347+
if (productId == null) {
348+
return CompoundHierarchyId.forOrganization(orgId)
349+
} else {
350+
val repositoryId = this[RoleAssignmentsTable.repositoryId]?.let { RepositoryId(it.value) }
351+
return if (repositoryId == null) {
352+
CompoundHierarchyId.forProduct(orgId, productId)
353+
} else {
354+
CompoundHierarchyId.forRepository(orgId, productId, repositoryId)
355+
}
356+
}
357+
}
358+
}
340359

341360
/**
342361
* Generate the SQL condition to match the repository part of this [hierarchyId]. The condition also has to select
343362
* assignments on higher levels in the same hierarchy.
344363
*/
345364
private fun SqlExpressionBuilder.repositoryCondition(hierarchyId: CompoundHierarchyId): Op<Boolean> =
346365
(
347-
(RoleAssignmentsTable.repositoryId eq hierarchyId.repositoryId?.value) or
348-
(RoleAssignmentsTable.repositoryId eq null)
349-
) and
350-
(RoleAssignmentsTable.productId eq hierarchyId.productId?.value) and
351-
(RoleAssignmentsTable.organizationId eq hierarchyId.organizationId?.value)
366+
(RoleAssignmentsTable.repositoryId eq hierarchyId.repositoryId?.value) or
367+
(RoleAssignmentsTable.repositoryId eq null)
368+
) and
369+
(RoleAssignmentsTable.productId eq hierarchyId.productId?.value) and
370+
(RoleAssignmentsTable.organizationId eq hierarchyId.organizationId?.value)
352371

353372
/**
354373
* Generate the SQL condition to match role assignments for the given [hierarchyId] for which no product ID is
@@ -358,12 +377,6 @@ private fun SqlExpressionBuilder.productWildcardCondition(hierarchyId: CompoundH
358377
(RoleAssignmentsTable.productId eq null) and
359378
(RoleAssignmentsTable.organizationId eq hierarchyId.organizationId?.value)
360379

361-
/**
362-
* Generate the SQL condition to match role assignments for which no organization ID is defined.
363-
*/
364-
private fun SqlExpressionBuilder.organizationWildcardCondition(): Op<Boolean> =
365-
RoleAssignmentsTable.organizationId eq null
366-
367380
/**
368381
* Generate the SQL condition to match role assignments for the given [role].
369382
*/
@@ -373,3 +386,13 @@ private fun SqlExpressionBuilder.roleCondition(role: Role): Op<Boolean> =
373386
is ProductRole -> RoleAssignmentsTable.productRole eq role.name
374387
is RepositoryRole -> RoleAssignmentsTable.repositoryRole eq role.name
375388
}
389+
390+
/**
391+
* Return a collection with the roles that are relevant on the hierarchy level defined by [hierarchyId].
392+
*/
393+
private fun rolesForLevel(hierarchyId: CompoundHierarchyId): Collection<Role> =
394+
when (hierarchyId.level) {
395+
CompoundHierarchyId.REPOSITORY_LEVEL -> RepositoryRole.entries
396+
CompoundHierarchyId.PRODUCT_LEVEL -> ProductRole.entries
397+
else -> OrganizationRole.entries
398+
}

0 commit comments

Comments
 (0)