Skip to content

Commit 0479b56

Browse files
committed
feat(authorization): Support filters on hierarchy IDs
Add a function to `AuthorizationService` that computes a `HierarchyFilter` for the IDs of elements in the hierarchy on which a user has specific access rights. This can then be used to generate filter conditions for database queries. Signed-off-by: Oliver Heger <[email protected]>
1 parent ffdafd9 commit 0479b56

File tree

3 files changed

+562
-15
lines changed

3 files changed

+562
-15
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
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.OrganizationPermission
2324
import org.eclipse.apoapsis.ortserver.components.authorization.rights.PermissionChecker
25+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission
26+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission
2427
import org.eclipse.apoapsis.ortserver.components.authorization.rights.Role
2528
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2629
import org.eclipse.apoapsis.ortserver.model.HierarchyId
30+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
2731

2832
/**
2933
* A service interface providing functionality to query and manage roles and permissions for users on entities in the
@@ -94,4 +98,43 @@ interface AuthorizationService {
9498
* higher levels, such as organization admins.
9599
*/
96100
suspend fun listUsers(compoundHierarchyId: CompoundHierarchyId): Map<String, Set<Role>>
101+
102+
/**
103+
* Return a [HierarchyFilter] with information about all hierarchy elements for which the specified [userId] has at
104+
* least all the permissions defined by [organizationPermissions], [productPermissions], and
105+
* [repositoryPermissions]. With the optional [containedIn] ID, the filter can be restricted to elements that
106+
* belong to this root element (directly or indirectly). This function can be used to generate filters for queries
107+
* based on the access rights of a user. For the returned [CompoundHierarchyId]s not necessarily all components
108+
* corresponding to the requested permissions are filled. For instance, if all repositories with READ access are
109+
* requested and the user has the ADMIN role on the product, the result will contain the ID of this product. This
110+
* basically means that all repositories under this product are accessible to the user.
111+
*/
112+
suspend fun filterHierarchyIds(
113+
userId: String,
114+
organizationPermissions: Set<OrganizationPermission> = emptySet(),
115+
productPermissions: Set<ProductPermission> = emptySet(),
116+
repositoryPermissions: Set<RepositoryPermission> = emptySet(),
117+
containedIn: HierarchyId? = null
118+
): HierarchyFilter
119+
120+
/**
121+
* Return a [HierarchyFilter] with information about all hierarchy elements for which the specified [userId] has at
122+
* least the permissions defined by the given [requiredRole], optionally restricted to the hierarchical structure
123+
* defined by [containedIn]. This is an overload of the function with the same name that obtains the required
124+
* permissions from the passed in role. Note that this does not perform an exact match of the role, but checks
125+
* whether all the permissions defined by the role are granted to the user. So, for instance, when searching for a
126+
* READER role, also WRITER and ADMIN roles will match.
127+
*/
128+
suspend fun filterHierarchyIds(
129+
userId: String,
130+
requiredRole: Role,
131+
containedIn: HierarchyId? = null
132+
): HierarchyFilter =
133+
filterHierarchyIds(
134+
userId = userId,
135+
organizationPermissions = requiredRole.organizationPermissions,
136+
productPermissions = requiredRole.productPermissions,
137+
repositoryPermissions = requiredRole.repositoryPermissions,
138+
containedIn = containedIn
139+
)
97140
}

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

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20+
@file:Suppress("TooManyFunctions")
21+
2022
package org.eclipse.apoapsis.ortserver.components.authorization.service
2123

2224
import org.eclipse.apoapsis.ortserver.components.authorization.db.RoleAssignmentsTable
2325
import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole
2426
import org.eclipse.apoapsis.ortserver.components.authorization.rights.HierarchyPermissions
27+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.IdsByLevel
2528
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission
2629
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationRole
2730
import org.eclipse.apoapsis.ortserver.components.authorization.rights.PermissionChecker
@@ -38,6 +41,7 @@ import org.eclipse.apoapsis.ortserver.model.HierarchyId
3841
import org.eclipse.apoapsis.ortserver.model.OrganizationId
3942
import org.eclipse.apoapsis.ortserver.model.ProductId
4043
import org.eclipse.apoapsis.ortserver.model.RepositoryId
44+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
4145

4246
import org.jetbrains.exposed.sql.Database
4347
import org.jetbrains.exposed.sql.JoinType
@@ -199,6 +203,54 @@ class DbAuthorizationService(
199203
.mapValues { it.value.filterNotNullTo(mutableSetOf()) }
200204
}
201205

206+
override suspend fun filterHierarchyIds(
207+
userId: String,
208+
organizationPermissions: Set<OrganizationPermission>,
209+
productPermissions: Set<ProductPermission>,
210+
repositoryPermissions: Set<RepositoryPermission>,
211+
containedIn: HierarchyId?
212+
): HierarchyFilter {
213+
val assignments = loadAssignments(userId, null)
214+
val checker = PermissionChecker(
215+
organizationPermissions,
216+
productPermissions,
217+
repositoryPermissions
218+
)
219+
val permissions = HierarchyPermissions.create(assignments, checker)
220+
221+
val containedInId = containedIn?.let { resolveCompoundId(it) }
222+
val includes = includesDominatedByContainsFilter(permissions, containedInId)
223+
?: permissions.includes().filterContainedIn(containedInId)
224+
225+
return HierarchyFilter(
226+
transitiveIncludes = includes,
227+
nonTransitiveIncludes = permissions.implicitIncludes().filterContainedIn(containedInId),
228+
isWildcard = permissions.isSuperuser()
229+
)
230+
}
231+
232+
/**
233+
* Check if the given [containedInId] filter is contained in any of the includes in the given [permissions]. If so,
234+
* the user can access all elements under the [containedInId] ID, so return a map of includes with just this ID.
235+
* If this is not the case or [containedInId] is undefined, return *null*.
236+
*/
237+
private fun includesDominatedByContainsFilter(
238+
permissions: HierarchyPermissions,
239+
containedInId: CompoundHierarchyId?
240+
): IdsByLevel? =
241+
containedInId?.let {
242+
val containedInLevel = it.level
243+
val isFilterCovered = permissions.includes().entries.any { (level, ids) ->
244+
level < containedInLevel && ids.any { id -> id.contains(containedInId) }
245+
}
246+
247+
if (isFilterCovered) {
248+
mapOf(containedInId.level to listOf(containedInId))
249+
} else {
250+
null
251+
}
252+
}
253+
202254
/**
203255
* Retrieve the missing components to construct a [CompoundHierarchyId] from the given [hierarchyId]. Throw a
204256
* meaningful exception if this fails.
@@ -246,19 +298,20 @@ class DbAuthorizationService(
246298
/**
247299
* Load all role assignments for the given [userId] in the hierarchy defined by [compoundHierarchyId]. Return a
248300
* 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.
301+
* the whole hierarchy below the organization referenced by [compoundHierarchyId] or all assignments of the given
302+
* [userId] if no ID is specified. This makes sure that all relevant assignments are found.
251303
*/
252304
private suspend fun loadAssignments(
253305
userId: String,
254-
compoundHierarchyId: CompoundHierarchyId
306+
compoundHierarchyId: CompoundHierarchyId?
255307
): List<Pair<CompoundHierarchyId, Role>> = db.dbQuery {
256308
RoleAssignmentsTable.selectAll()
257309
.where {
258-
(RoleAssignmentsTable.userId eq userId) and (
259-
(RoleAssignmentsTable.organizationId eq compoundHierarchyId.organizationId?.value) or
260-
(RoleAssignmentsTable.organizationId eq null)
261-
)
310+
val hierarchyCondition = compoundHierarchyId?.let { id ->
311+
(RoleAssignmentsTable.organizationId eq id.organizationId?.value) or
312+
(RoleAssignmentsTable.organizationId eq null)
313+
} ?: Op.TRUE
314+
(RoleAssignmentsTable.userId eq userId) and hierarchyCondition
262315
}.mapNotNull { row ->
263316
row.extractRole()?.let { role ->
264317
row.extractHierarchyId() to role
@@ -277,15 +330,15 @@ class DbAuthorizationService(
277330
(RoleAssignmentsTable.productId eq compoundHierarchyId.productId?.value) and
278331
(RoleAssignmentsTable.repositoryId eq compoundHierarchyId.repositoryId?.value)
279332
} == 1
280-
).also {
281-
if (it) {
282-
logger.info(
283-
"Removed role assignment for user '{}' on hierarchy element {}.",
284-
userId,
285-
compoundHierarchyId
286-
)
333+
).also {
334+
if (it) {
335+
logger.info(
336+
"Removed role assignment for user '{}' on hierarchy element {}.",
337+
userId,
338+
compoundHierarchyId
339+
)
340+
}
287341
}
288-
}
289342
}
290343

291344
/**
@@ -387,6 +440,21 @@ private fun SqlExpressionBuilder.roleCondition(role: Role): Op<Boolean> =
387440
is RepositoryRole -> RoleAssignmentsTable.repositoryRole eq role.name
388441
}
389442

443+
/**
444+
* Filter the IDs in this [IdsByLevel] to only include those that are contained in the given [containedInId]. If
445+
* [containedInId] is *null*, return this object unmodified.
446+
*/
447+
private fun IdsByLevel.filterContainedIn(
448+
containedInId: CompoundHierarchyId?
449+
): IdsByLevel =
450+
if (containedInId == null) {
451+
this
452+
} else {
453+
this.mapValues { (_, ids) ->
454+
ids.filter { id -> id in containedInId }
455+
}.filterValues { it.isNotEmpty() }
456+
}
457+
390458
/**
391459
* Return a collection with the roles that are relevant on the hierarchy level defined by [hierarchyId].
392460
*/

0 commit comments

Comments
 (0)