Skip to content

Commit 2f683e0

Browse files
committed
feat(authorization): Add more AuthorizationChecker implementations
Add support for permissions on product and repository level and superuser checks. Signed-off-by: Oliver Heger <[email protected]>
1 parent 3db9325 commit 2f683e0

File tree

2 files changed

+171
-1
lines changed

2 files changed

+171
-1
lines changed

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ import io.ktor.server.application.ApplicationCall
2323

2424
import org.eclipse.apoapsis.ortserver.components.authorization.rights.EffectiveRole
2525
import org.eclipse.apoapsis.ortserver.components.authorization.rights.OrganizationPermission
26+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission
27+
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission
2628
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
29+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2730
import org.eclipse.apoapsis.ortserver.model.OrganizationId
31+
import org.eclipse.apoapsis.ortserver.model.ProductId
32+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
2833
import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter
2934

3035
/**
@@ -57,6 +62,15 @@ interface AuthorizationChecker {
5762
fun checkAuthorization(effectiveRole: EffectiveRole): Boolean
5863
}
5964

65+
/** The name of the request parameter referring to the organization ID. */
66+
private const val ORGANIZATION_ID_PARAM = "organizationId"
67+
68+
/** The name of the request parameter referring to the product ID. */
69+
private const val PRODUCT_ID_PARAM = "productId"
70+
71+
/** The name of the request parameter referring to the repository ID. */
72+
private const val REPOSITORY_ID_PARAM = "repositoryId"
73+
6074
/**
6175
* Create an [AuthorizationChecker] that checks for the presence of the given organization-level [permission].
6276
*/
@@ -67,10 +81,64 @@ fun requirePermission(permission: OrganizationPermission): AuthorizationChecker
6781
userId: String,
6882
call: ApplicationCall
6983
): EffectiveRole =
70-
service.getEffectiveRole(userId, OrganizationId(call.requireIdParameter("organizationId")))
84+
service.getEffectiveRole(userId, OrganizationId(call.requireIdParameter(ORGANIZATION_ID_PARAM)))
7185

7286
override fun checkAuthorization(effectiveRole: EffectiveRole): Boolean =
7387
effectiveRole.hasOrganizationPermission(permission)
7488

7589
override fun toString(): String = "RequireOrganizationPermission($permission)"
7690
}
91+
92+
/**
93+
* Create an [AuthorizationChecker] that checks for the presence of the given product-level [permission].
94+
*/
95+
fun requirePermission(permission: ProductPermission): AuthorizationChecker =
96+
object : AuthorizationChecker {
97+
override suspend fun loadEffectiveRole(
98+
service: AuthorizationService,
99+
userId: String,
100+
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)
106+
107+
override fun toString(): String = "RequireProductPermission($permission)"
108+
}
109+
110+
/**
111+
* Create an [AuthorizationChecker] that checks for the presence of the given repository-level [permission].
112+
*/
113+
fun requirePermission(permission: RepositoryPermission): AuthorizationChecker =
114+
object : AuthorizationChecker {
115+
override suspend fun loadEffectiveRole(
116+
service: AuthorizationService,
117+
userId: String,
118+
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)
124+
125+
override fun toString(): String = "RequireRepositoryPermission($permission)"
126+
}
127+
128+
/**
129+
* Create an [AuthorizationChecker] that checks whether the user is a superuser.
130+
*/
131+
fun requireSuperuser(): AuthorizationChecker =
132+
object : AuthorizationChecker {
133+
override suspend fun loadEffectiveRole(
134+
service: AuthorizationService,
135+
userId: String,
136+
call: ApplicationCall
137+
): EffectiveRole =
138+
service.getEffectiveRole(userId, CompoundHierarchyId.WILDCARD)
139+
140+
override fun checkAuthorization(effectiveRole: EffectiveRole): Boolean =
141+
effectiveRole.isSuperuser
142+
143+
override fun toString(): String = "RequireSuperuser"
144+
}

components/authorization/backend/src/test/kotlin/routes/AuthorizedRoutesTest.kt

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import io.ktor.server.testing.testApplication
5454
import io.mockk.coEvery
5555
import io.mockk.every
5656
import io.mockk.mockk
57+
import io.mockk.verify
5758

5859
import java.util.Date
5960

@@ -64,6 +65,8 @@ import org.eclipse.apoapsis.ortserver.components.authorization.rights.Repository
6465
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
6566
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
6667
import org.eclipse.apoapsis.ortserver.model.OrganizationId
68+
import org.eclipse.apoapsis.ortserver.model.ProductId
69+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
6770

6871
class AuthorizedRoutesTest : WordSpec() {
6972
/**
@@ -270,6 +273,105 @@ class AuthorizedRoutesTest : WordSpec() {
270273
response.status shouldBe HttpStatusCode.OK
271274
}
272275
}
276+
277+
"support requests on product level" {
278+
val effectiveRole = mockk<EffectiveRole> {
279+
every { hasProductPermission(ProductPermission.DELETE) } returns true
280+
}
281+
val service = mockk<AuthorizationService> {
282+
coEvery {
283+
getEffectiveRole(USERNAME, ProductId(ID_PARAMETER))
284+
} returns effectiveRole
285+
}
286+
287+
runAuthorizationTest(
288+
service,
289+
routeBuilder = {
290+
route("test/{productId}") {
291+
get(testDocs, requirePermission(ProductPermission.DELETE)) {
292+
call.principal<OrtServerPrincipal>().shouldNotBeNull {
293+
username shouldBe USERNAME
294+
}
295+
296+
call.respond(HttpStatusCode.OK)
297+
}
298+
}
299+
}
300+
) { client ->
301+
val response = client.get("test/$ID_PARAMETER")
302+
response.status shouldBe HttpStatusCode.OK
303+
304+
verify {
305+
effectiveRole.hasProductPermission(ProductPermission.DELETE)
306+
}
307+
}
308+
}
309+
310+
"support requests on repository level" {
311+
val effectiveRole = mockk<EffectiveRole> {
312+
every { hasRepositoryPermission(RepositoryPermission.READ) } returns true
313+
}
314+
val service = mockk<AuthorizationService> {
315+
coEvery {
316+
getEffectiveRole(USERNAME, RepositoryId(ID_PARAMETER))
317+
} returns effectiveRole
318+
}
319+
320+
runAuthorizationTest(
321+
service,
322+
routeBuilder = {
323+
route("test/{repositoryId}") {
324+
get(testDocs, requirePermission(RepositoryPermission.READ)) {
325+
call.principal<OrtServerPrincipal>().shouldNotBeNull {
326+
username shouldBe USERNAME
327+
}
328+
329+
call.respond(HttpStatusCode.OK)
330+
}
331+
}
332+
}
333+
) { client ->
334+
val response = client.get("test/$ID_PARAMETER")
335+
response.status shouldBe HttpStatusCode.OK
336+
337+
verify {
338+
effectiveRole.hasRepositoryPermission(RepositoryPermission.READ)
339+
}
340+
}
341+
}
342+
343+
"support requests that require superuser rights" {
344+
val effectiveRole = mockk<EffectiveRole> {
345+
every { isSuperuser } returns true
346+
}
347+
val service = mockk<AuthorizationService> {
348+
coEvery {
349+
getEffectiveRole(USERNAME, CompoundHierarchyId.WILDCARD)
350+
} returns effectiveRole
351+
}
352+
353+
runAuthorizationTest(
354+
service,
355+
routeBuilder = {
356+
route("test/{organizationId}") {
357+
get(testDocs, requireSuperuser()) {
358+
call.principal<OrtServerPrincipal>().shouldNotBeNull {
359+
username shouldBe USERNAME
360+
}
361+
362+
call.respond(HttpStatusCode.OK)
363+
}
364+
}
365+
}
366+
) { client ->
367+
val response = client.get("test/$ID_PARAMETER")
368+
response.status shouldBe HttpStatusCode.OK
369+
370+
verify {
371+
effectiveRole.isSuperuser
372+
}
373+
}
374+
}
273375
}
274376

275377
"failed authorization checks" should {

0 commit comments

Comments
 (0)