https://kotlinlang.org logo
#ktor
Title
# ktor
j

Joost Klitsie

11/22/2020, 9:38 AM
Hi everyone, is it possible to override features? For example: I have a feature that allows actions from a user (GET/PUT/Whatever) based on a role, but also based on an exception in this rule. I want all my routes to default that you would need to be logged in as ADMIN, except if I override it (which could be route specific) For example:
Copy code
routing {
    withRole(ADMIN) {
         route("admin_only_route") {
         }

         withRole(USER) {
             route("special_route") {
             }
         }
           
    }
}
j

Joris PZ

11/22/2020, 2:15 PM
I guess it depends on the implementation of
withRole
how these two role checks would combine. The implementation I wrote some time ago would (I think, haven't checked) combine these two such that to access
special_route
you would need to have both
ADMIN
and
USER
roles.
j

Joost Klitsie

11/22/2020, 2:45 PM
@Joris PZ I based it on your article indeed, I just have a bit different needs. It was a great help to get things started, great explanation and I was happy to look into the code as well, so thanks for that :) ktor documentation lacks a bit. Yes for now it seems like if I call it nested, both will be run. I am trying to find a way to only run the deeper nested one. I was thinking about somehow ordering the interceptors in a reverse way, pass on something to a interceptor context so if I see a context exists, I do not the validation again. Just no luck thusfar, I did not see a way to proceed the interceptor somehow and run a child interceptor first, or as I said try to reverse the order of adding interceptors
j

Joris PZ

11/22/2020, 2:54 PM
Thanks, glad you found it useful! Is there a big need for the nested structure? Because I think you could also do something like
Copy code
routing {
    withrole("ADMIN) {
        route("foo") {
            route("xxyyz") {
                //
            }
        } 
    }
    withrole("USER) {
        route("foo") {
            route("bla") {
                //
            }
        } 
    }
}
and Ktor would figure it all out (and it has the added benefit of clearly showing which routes are admin-only and which are user) Otherwise, maybe you could in your interceptor walk up the route tree and delete/cancel any earlier AuthorizedRoutes you find? That would do it too
This would be construction time, btw, not runtime
j

Joost Klitsie

11/22/2020, 3:33 PM
Well I would like to give a default: Admin only, and then only override it for specific routes
when I need more lenient authentication
j

Joris PZ

11/22/2020, 4:22 PM
What I would do is create a
withDefaultRole
extension function on
Route
that adds an interceptor in a new phase, say
DefaultAuthorizationPhase
, that executes right after your regular
AuthorizationPhase
. Then, in your regular authorization interceptor you can add a flag in the
PipelineContext
that signals to the downstream interceptor if authorization has already taken place. If it didn't, you can then proceed with your default authorization, if it didn't you can just do nothing.
j

Joost Klitsie

11/22/2020, 5:12 PM
okee it seems like this works:
Copy code
fun Route.test(
    message: String,
    build: Route.() -> Unit,
): Route {
    val testRoute = createChild(SimpleSelector("message"))
    application.feature(SimpleInterceptor).apply {
        interceptPipeline(testRoute, message, countSimpleSelectors())
    }
    testRoute.build()
    return testRoute
}

fun Route.countSimpleSelectors(): Int = children.sumBy {  child ->
    child.countSimpleSelectors()
} + if (selector is SimpleSelector) 1 else 0
here we count how many of the same selectors are already in the route
Copy code
fun interceptPipeline(
        pipeline: ApplicationCallPipeline,
        message: String,
        counter: Int,
    ) {
        pipeline.insertPhaseAfter(ApplicationCallPipeline.Features, Authentication.ChallengePhase)
        val phases = (counter downTo 1).map {
            simplePhase(it)
        }
        phases.fold(Authentication.ChallengePhase) { old, new ->
            pipeline.insertPhaseAfter(old, new)
            new
        }

        pipeline.intercept(phases.first()) {
            val call = call
            val simpleContext = SimpleContext.from(call)
            TestPipeline().apply {
                val subject = SimpleContext.from(call)
                println("original subject: $message, new subject: ${subject.someMessage}")
                subject.someMessage = message
            }.execute(call, simpleContext)
        }
    }
And then we just add a lot of phases
in reverse order
which feels a bit hacky
but this is not production code so f*** it 🤐
🙂 1
To give some context: I am making a pub quiz, where I have an admin panel that can create and control the entire quiz. Users can login to a team, and then start participating in the quiz. However, they can only view the current questions (and not future ones) and only give answers while the quiz in progress, and of course a team can only submit answers for itself (and not in the name of other teams). So basically, the authorization is constantly changing, as new questions get revealed, rounds start and rounds end, so users might or might not be allowed certain actions. So my basic route has the rule that users should be an admin to perform any action, and then later on every endpoint can have a custom authorization, based on the progress of the current quiz and the users that are logged in:
Copy code
install(AccessRulesInterceptor) {
        minimalRole = Role.ADMIN
    }
    routing {
        route("/") {
             authenticate("jwt") {
                routeWithRules {
                    // All components registered here
Then my admin account can of course perform all the actions, but also if the user is logged in he/she can do certain things. For example, if the user wants to update the name of the team, it should be allowed to do that:
Copy code
override fun Route.installRouting() {
        routeWithRules(Api.v0.team("{id}"), ruleException = { principal ->
            val teamId = call.parameters["id"].toUuidOrNull() ?: return@routeWithRules false
            teamService.canUserUpdateTeam(teamId, principal)
        }) {
            put {
                // .. save the new team info somehwere
So here I basically add an exception to the endpoint, allowing a user to update its own team name when calling
PUT api/v0/team/<id>
. You can also override the minimal role that you need to perform a certain call, for example allow all users with role USER (or upper) to call certain things. So I am for now pretty happy with this simple api I created, should fit my needs.
2 Views