Skip to content

Commit 357cdd4

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 d02ae69 commit 357cdd4

File tree

2 files changed

+628
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)