Skip to content

Commit 0f70fbf

Browse files
committed
feat(authorization): Introduce HierarchyPermissions interface
This interface is going to be used in different use cases to check whether specific permissions are available on concrete hierarchy elements. The implementation contains the logic how permissions are inherited through the hierarchy. Signed-off-by: Oliver Heger <[email protected]>
1 parent c2bb55c commit 0f70fbf

File tree

2 files changed

+591
-0
lines changed

2 files changed

+591
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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.rights
21+
22+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
23+
24+
/**
25+
* A class that encapsulates a number of permissions to check on hierarchy elements.
26+
*/
27+
class PermissionChecker(
28+
/** The required permissions on organization level. */
29+
val organizationPermissions: Set<OrganizationPermission> = emptySet(),
30+
31+
/** The required permissions on product level. */
32+
val productPermissions: Set<ProductPermission> = emptySet(),
33+
34+
/** The required permissions on repository level. */
35+
val repositoryPermissions: Set<RepositoryPermission> = emptySet()
36+
) {
37+
/**
38+
* Check whether the given [role] contains all permissions required by this [PermissionChecker].
39+
*/
40+
operator fun invoke(role: Role): Boolean =
41+
organizationPermissions.all { it in role.organizationPermissions } &&
42+
productPermissions.all { it in role.productPermissions } &&
43+
repositoryPermissions.all { it in role.repositoryPermissions }
44+
}
45+
46+
/**
47+
* Alias for a [Map] that groups [CompoundHierarchyId]s by their hierarchy level. The keys correspond to constants
48+
* defined by [CompoundHierarchyId].
49+
*/
50+
typealias IdsByLevel = Map<Int, List<CompoundHierarchyId>>
51+
52+
/**
53+
* A class to manage permissions on different levels of the hierarchy.
54+
*
55+
* This class controls the effect of role assignments to users and how permissions are inherited through the hierarchy.
56+
* It implements the following rules:
57+
* - Role assignments on higher levels in the hierarchy inherit downwards to lower levels. So, if a user is granted
58+
* the `WRITER` role on an organization, they automatically have `WRITER` permissions on all products and
59+
* repositories within that organization.
60+
* - Role assignments on lower levels in the hierarchy can widen the permissions inherited from higher levels, but not
61+
* restrict them. For example, a `WRITER` role assignment for a user on product level could be turned into an `ADMIN`
62+
* role assignment on repository level. However, a `READER` role assignment on repository level would be ignored if
63+
* the user already has `WRITER` permissions on the parent product.
64+
* - Role assignments on lower levels of the hierarchy can trigger implicit permissions on higher levels. So, if a user
65+
* has access to a repository, they implicitly have at least `READER` access to the parent product and organization.
66+
*
67+
* An instance of this class is created for a given set of role assignments and for a specific set of permissions
68+
* controlled by a [PermissionChecker]. The functions of this class can then be used to find out on which hierarchy
69+
* elements these permissions are granted.
70+
*/
71+
interface HierarchyPermissions {
72+
companion object {
73+
/**
74+
* Create a new [HierarchyPermissions] instance for the given collection of role [roleAssignments] that
75+
* evaluates the permissions controlled by the given [checker] function.
76+
*/
77+
fun create(
78+
roleAssignments: Collection<Pair<CompoundHierarchyId, Role>>,
79+
checker: PermissionChecker
80+
): HierarchyPermissions {
81+
val assignmentsByLevel = roleAssignments.groupBy { it.first.level }
82+
83+
return assignmentsByLevel[CompoundHierarchyId.WILDCARD_LEVEL]?.singleOrNull()
84+
?.takeIf { it.second == OrganizationRole.ADMIN }?.let { superuserInstance }
85+
?: createStandardInstance(assignmentsByLevel, checker)
86+
}
87+
88+
/**
89+
* Return a [PermissionChecker] that checks for the presence of all given organization permissions [ps].
90+
*/
91+
fun permissions(vararg ps: OrganizationPermission): PermissionChecker =
92+
PermissionChecker(organizationPermissions = ps.toSet())
93+
94+
/**
95+
* Return a [PermissionChecker] that checks for the presence of all given product permissions [ps].
96+
*/
97+
fun permissions(vararg ps: ProductPermission): PermissionChecker =
98+
PermissionChecker(productPermissions = ps.toSet())
99+
100+
/**
101+
* Return a [PermissionChecker] that checks for the presence of all given repository permissions [ps].
102+
*/
103+
fun permissions(vararg ps: RepositoryPermission): PermissionChecker =
104+
PermissionChecker(repositoryPermissions = ps.toSet())
105+
106+
/**
107+
* Return a [PermissionChecker] that checks for the presence of all permissions defined by the given [role].
108+
*/
109+
fun permissions(role: Role): PermissionChecker =
110+
PermissionChecker(
111+
organizationPermissions = role.organizationPermissions,
112+
productPermissions = role.productPermissions,
113+
repositoryPermissions = role.repositoryPermissions
114+
)
115+
}
116+
117+
/**
118+
* Check whether the permissions evaluated by this instance are granted on the hierarchy element identified by the
119+
* given [compoundHierarchyId].
120+
*/
121+
fun hasPermission(compoundHierarchyId: CompoundHierarchyId): Boolean
122+
123+
/**
124+
* Return a [Map] with the IDs of all hierarchy elements for which a role assignment exists that grants the
125+
* permissions evaluated by this instance. The result is grouped by hierarchy level. This can be used to generate
126+
* filter conditions for database queries selecting elements in the hierarchy.
127+
*/
128+
fun includes(): IdsByLevel
129+
130+
/**
131+
* Return a [Map] with the IDs of hierarchy elements for which the permissions evaluated by this instance are
132+
* implicitly granted due to role assignments on lower levels in the hierarchy. For instance, if READ access is
133+
* granted on a repository, READ access is also needed on the parent product and organization. Such implicit
134+
* permissions are different from explicitly granted ones, since they do not inherit downwards in the hierarchy.
135+
* The result is grouped by hierarchy level. The resulting IDs do not include those returned by [includes]. When
136+
* constructing database query filters, these IDs need to be included alongside those from [includes].
137+
*/
138+
fun implicitIncludes(): IdsByLevel
139+
140+
/**
141+
* Return a flag whether this instance represents superuser permissions.
142+
*/
143+
fun isSuperuser(): Boolean
144+
}
145+
146+
/**
147+
* A special instance of [HierarchyPermissions] that is returned by [HierarchyPermissions.create] when an assignment
148+
* of superuser permissions is detected. This instance grants all permissions and returns corresponding filters.
149+
*/
150+
private val superuserInstance = object : HierarchyPermissions {
151+
override fun hasPermission(compoundHierarchyId: CompoundHierarchyId): Boolean = true
152+
153+
override fun includes(): IdsByLevel =
154+
mapOf(CompoundHierarchyId.WILDCARD_LEVEL to listOf(CompoundHierarchyId.WILDCARD))
155+
156+
override fun implicitIncludes(): IdsByLevel = emptyMap()
157+
158+
override fun isSuperuser(): Boolean = true
159+
}
160+
161+
/**
162+
* Create an instance of [HierarchyPermissions] for standard users based on the given [Map] with
163+
* [assignmentsByLevel] and the [checker] function.
164+
*/
165+
private fun createStandardInstance(
166+
assignmentsByLevel: Map<Int, List<Pair<CompoundHierarchyId, Role>>>,
167+
checker: PermissionChecker
168+
): HierarchyPermissions {
169+
val assignmentsMap = constructAssignmentsMap(assignmentsByLevel, checker)
170+
val implicits = computeImplicitIncludes(assignmentsMap, assignmentsByLevel, checker)
171+
val implicitIds = implicits.values.flatMapTo(mutableSetOf()) { it }
172+
173+
return object : HierarchyPermissions {
174+
override fun hasPermission(compoundHierarchyId: CompoundHierarchyId): Boolean =
175+
findAssignment(assignmentsMap, compoundHierarchyId) || compoundHierarchyId in implicitIds
176+
177+
override fun includes(): IdsByLevel =
178+
assignmentsMap.filter { e -> e.value }
179+
.keys
180+
.byLevel()
181+
182+
override fun implicitIncludes(): IdsByLevel = implicits
183+
184+
override fun isSuperuser(): Boolean = false
185+
}
186+
}
187+
188+
/**
189+
* Return the closest permission check result for the given [id] by traversing up the hierarchy if necessary. If no
190+
* assignment is found for the given [id] or any of its parents, assume that the permissions are not present.
191+
*/
192+
private tailrec fun findAssignment(
193+
assignments: Map<CompoundHierarchyId, Boolean>,
194+
id: CompoundHierarchyId?
195+
): Boolean =
196+
if (id == null) {
197+
false
198+
} else {
199+
assignments[id] ?: findAssignment(assignments, id.parent)
200+
}
201+
202+
/**
203+
* Construct the [Map] with information about available permissions on different levels in the hierarchy based on
204+
* the given [assignmentsByLevel] and the [checker] function.
205+
*/
206+
private fun constructAssignmentsMap(
207+
assignmentsByLevel: Map<Int, List<Pair<CompoundHierarchyId, Role>>>,
208+
checker: PermissionChecker
209+
): MutableMap<CompoundHierarchyId, Boolean> {
210+
val assignmentsMap = mutableMapOf<CompoundHierarchyId, Boolean>()
211+
212+
for (level in CompoundHierarchyId.ORGANIZATION_LEVEL..CompoundHierarchyId.REPOSITORY_LEVEL) {
213+
val levelAssignments = assignmentsByLevel[level].orEmpty()
214+
levelAssignments.forEach { (id, role) ->
215+
val isPresent = checker(role)
216+
val isPresentOnParent = findAssignment(assignmentsMap, id.parent)
217+
218+
// If this assignment does not change the status from a higher level, it can be skipped.
219+
if (isPresent && !isPresentOnParent) {
220+
assignmentsMap[id] = true
221+
}
222+
}
223+
}
224+
225+
return assignmentsMap
226+
}
227+
228+
/**
229+
* Find the IDs of all hierarchy elements from [assignmentsByLevel] that are granted implicit permissions due to role
230+
* assignments on lower levels in the hierarchy. The given [assignmentsMap] has already been populated with explicit
231+
* role assignments. Use the given [checker] function to determine whether permissions are granted.
232+
*/
233+
private fun computeImplicitIncludes(
234+
assignmentsMap: Map<CompoundHierarchyId, Boolean>,
235+
assignmentsByLevel: Map<Int, List<Pair<CompoundHierarchyId, Role>>>,
236+
checker: PermissionChecker
237+
): IdsByLevel {
238+
val implicitIncludes = mutableSetOf<CompoundHierarchyId>()
239+
240+
for (level in CompoundHierarchyId.PRODUCT_LEVEL..CompoundHierarchyId.REPOSITORY_LEVEL) {
241+
assignmentsByLevel[level].orEmpty().filter { (_, role) -> checker(role) }
242+
.forEach { (id, _) ->
243+
val parents = id.parents()
244+
if (parents.none { it in assignmentsMap }) {
245+
implicitIncludes += parents
246+
}
247+
}
248+
}
249+
250+
return implicitIncludes.byLevel()
251+
}
252+
253+
/**
254+
* Group the IDs contained in this [Collection] by their hierarchy level.
255+
*/
256+
private fun Collection<CompoundHierarchyId>.byLevel(): IdsByLevel =
257+
this.groupBy { it.level }
258+
259+
/**
260+
* Return a list with the IDs of all parents of this [CompoundHierarchyId].
261+
*/
262+
private fun CompoundHierarchyId.parents(): List<CompoundHierarchyId> {
263+
val parents = mutableListOf<CompoundHierarchyId>()
264+
265+
tailrec fun findParents(id: CompoundHierarchyId?) {
266+
if (id != null) {
267+
parents += id
268+
findParents(id.parent)
269+
}
270+
}
271+
272+
findParents(parent)
273+
return parents
274+
}

0 commit comments

Comments
 (0)