Skip to content

Commit 3f70822

Browse files
committed
feat(authorization): Handle exceptions during authentication
Exceptions that occur during authentication and the creation of a principal all caught by Ktor and mapped to responses with status code 401. To support different mappings, also based on the `StatusPages` plugin, record such exceptions in the `OrtServerPrincipal`, so that they can be evaluated in route handlers, where they are handled in the usual way. Signed-off-by: Oliver Heger <[email protected]>
1 parent a1b1e83 commit 3f70822

File tree

4 files changed

+89
-18
lines changed

4 files changed

+89
-18
lines changed

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,19 @@ suspend fun ApplicationCall.createAuthorizedPrincipal(
5252
(this as? RoutingPipelineCall)?.let { routingCall ->
5353
val checker = routingCall.route.findAuthorizationChecker()
5454

55-
val effectiveRole = if (checker != null) {
56-
checker.loadEffectiveRole(
57-
service = authorizationService,
58-
userId = payload.getClaim("preferred_username").asString(),
59-
call = this
60-
)
61-
} else {
62-
EffectiveRole.EMPTY
63-
}
64-
65-
OrtServerPrincipal.create(payload, effectiveRole)
55+
runCatching {
56+
val effectiveRole = if (checker != null) {
57+
checker.loadEffectiveRole(
58+
service = authorizationService,
59+
userId = payload.getClaim("preferred_username").asString(),
60+
call = this
61+
)
62+
} else {
63+
EffectiveRole.EMPTY
64+
}
65+
66+
OrtServerPrincipal.create(payload, effectiveRole)
67+
}.getOrElse(OrtServerPrincipal::fromException)
6668
}
6769

6870
/**

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ class OrtServerPrincipal(
3636
/** The full name of the principal. */
3737
val fullName: String,
3838

39+
/**
40+
* An exception that occurred when setting up the principal. If this is not *null*, this exception is re-thrown
41+
* when querying the authorization status. The background of this property is that during authentication, all
42+
* exceptions are caught by Ktor and lead to HTTP 401 responses. However, for some exceptions, different status
43+
* codes are more appropriate, for instance, status 404 if a non-existing hierarchy ID was requested. This can
44+
* only be achieved by storing the exception first and re-throwing it later when a route handler is active.
45+
*/
46+
val validationException: Throwable?,
47+
3948
/**
4049
* The effective role computed for the principal. This can be *null* if either no authorization is required or the
4150
* authorization check failed. In the latter case, an exception is thrown when the role is accessed.
@@ -57,16 +66,32 @@ class OrtServerPrincipal(
5766
userId = payload.subject,
5867
username = payload.getClaim(CLAIM_USERNAME).asString(),
5968
fullName = payload.getClaim(CLAIM_FULL_NAME).asString(),
60-
role = effectiveRole
69+
role = effectiveRole,
70+
validationException = null
71+
)
72+
73+
/**
74+
* Create an [OrtServerPrincipal] for the case that during authentication the given [exception] occurred. This
75+
* exception is recorded, so that it can be handled later.
76+
*/
77+
fun fromException(exception: Throwable): OrtServerPrincipal =
78+
OrtServerPrincipal(
79+
userId = "",
80+
username = "",
81+
fullName = "",
82+
role = null,
83+
validationException = exception
6184
)
6285
}
6386

6487
/**
6588
* A flag indicating whether the principal is authorized. If this is *true*, the effective role of the principal
66-
* can be accessed via [effectiveRole].
89+
* can be accessed via [effectiveRole]. If a [validationException] is recorded in this instance, it is thrown
90+
* when accessing this property. Since this property is typically accessed in the beginning of a route handler,
91+
* this leads to proper exception handling and mapping to HTTP response status codes.
6792
*/
6893
val isAuthorized: Boolean
69-
get() = role != null
94+
get() = validationException?.let { throw it } ?: (role != null)
7095

7196
/**
7297
* The effective role of the principal if authorization was successful. Otherwise, accessing this property throws

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import org.eclipse.apoapsis.ortserver.components.authorization.rights.Permission
6767
import org.eclipse.apoapsis.ortserver.components.authorization.rights.ProductPermission
6868
import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission
6969
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
70+
import org.eclipse.apoapsis.ortserver.components.authorization.service.InvalidHierarchyIdException
7071
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
7172
import org.eclipse.apoapsis.ortserver.model.HierarchyId
7273
import org.eclipse.apoapsis.ortserver.model.OrganizationId
@@ -106,6 +107,9 @@ class AuthorizedRoutesTest : WordSpec() {
106107
exception<AuthorizationException> { call, _ ->
107108
call.respond(HttpStatusCode.Forbidden)
108109
}
110+
exception<InvalidHierarchyIdException> { call, _ ->
111+
call.respond(HttpStatusCode.NotFound)
112+
}
109113
}
110114

111115
routing {
@@ -453,6 +457,29 @@ class AuthorizedRoutesTest : WordSpec() {
453457
}
454458
}
455459
}
460+
461+
"exceptions" should {
462+
"be mapped to correct status codes" {
463+
val service = mockk<AuthorizationService> {
464+
coEvery { checkPermissions(any(), any<HierarchyId>(), any()) } throws
465+
InvalidHierarchyIdException(OrganizationId(42))
466+
}
467+
468+
runAuthorizationTest(
469+
service,
470+
routeBuilder = {
471+
route("test/{organizationId}") {
472+
get(testDocs, requirePermission(OrganizationPermission.READ)) {
473+
call.respond(HttpStatusCode.OK)
474+
}
475+
}
476+
}
477+
) { client ->
478+
val response = client.get("test/$ID_PARAMETER")
479+
response.status shouldBe HttpStatusCode.NotFound
480+
}
481+
}
482+
}
456483
}
457484
}
458485

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ class OrtServerPrincipalTest : WordSpec({
5858
userId = "user-id",
5959
username = "username",
6060
fullName = "Full Name",
61-
role = mockk()
61+
role = mockk(),
62+
validationException = null
6263
)
6364

6465
principal.isAuthorized shouldBe true
@@ -69,11 +70,25 @@ class OrtServerPrincipalTest : WordSpec({
6970
userId = "user-id",
7071
username = "username",
7172
fullName = "Full Name",
72-
role = null
73+
role = null,
74+
validationException = null
7375
)
7476

7577
principal.isAuthorized shouldBe false
7678
}
79+
80+
"re-throw a validation exception if present" {
81+
val exception = IllegalStateException("Validation failed")
82+
val principal = OrtServerPrincipal.fromException(exception)
83+
84+
principal.userId shouldBe ""
85+
principal.username shouldBe ""
86+
principal.fullName shouldBe ""
87+
88+
shouldThrow<IllegalStateException> {
89+
principal.isAuthorized
90+
} shouldBe exception
91+
}
7792
}
7893

7994
"effectiveRole" should {
@@ -83,7 +98,8 @@ class OrtServerPrincipalTest : WordSpec({
8398
userId = "user-id",
8499
username = "username",
85100
fullName = "Full Name",
86-
role = effectiveRole
101+
role = effectiveRole,
102+
validationException = null
87103
)
88104

89105
principal.effectiveRole shouldBe effectiveRole
@@ -94,7 +110,8 @@ class OrtServerPrincipalTest : WordSpec({
94110
userId = "user-id",
95111
username = "username",
96112
fullName = "Full Name",
97-
role = null
113+
role = null,
114+
validationException = null
98115
)
99116

100117
shouldThrow<AuthorizationException> {

0 commit comments

Comments
 (0)