If and how can I install a ktor plugin locally within a route for GET but not for POST? - kotlin

For a ktor (2.0.3) application (kotlin 1.7.10) I want to have two endpoints on the same route (/feedback) but with different http methods, one GET and one POST.
So far no problem.
Now I would like to install an AuthorizationPlugin on only one of them.
I know how to install a plugin for a specific route only, but is it also possible to separately install it for different http methods on the same route?
So far I could not figure out a solution that does not require me to either introduce different routes (e.g. /feedback/read, /feedback/new) or handle the authorization check within the GET and POST callbacks directly.
The following is a reduced code containing two tests demonstrating the problem.
package some.example.package
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.apache.http.auth.AuthenticationException
import kotlin.test.Test
import kotlin.test.assertEquals
internal enum class AuthRole {
Admin, User
}
#kotlinx.serialization.Serializable
internal data class AuthUserSession(val username: String, val roles: Set<AuthRole> = setOf()) : Principal
const val authName = "form-auth"
const val usernameFormField = "username"
const val passwordFormField = "password"
/**
* Plugin Implementation
*/
internal val AuthorizationPlugin = createRouteScopedPlugin(
name = "AuthorizationPlugin",
createConfiguration = ::RoleBaseConfiguration
) {
pluginConfig.apply {
on(AuthenticationChecked) { call ->
val principal =
call.authentication.principal<AuthUserSession>() ?: throw Exception("Missing principal")
val userRoles = principal.roles
val denyReasons = mutableListOf<String>()
roles?.let {
if (roles!!.none { it in userRoles }) {
denyReasons += "Principal $principal has none of the sufficient role(s) ${
roles!!.joinToString(
" or "
)
}"
}
}
if (denyReasons.isNotEmpty()) {
val message = denyReasons.joinToString(". ")
throw Exception(message)
}
}
}
}
internal class RoleBaseConfiguration (
var roles: Set<AuthRole>? = null,
)
/**
* Server setup
*/
internal fun Application.setupConfig() {
install(Authentication) {
form(authName) {
userParamName = usernameFormField
passwordParamName = passwordFormField
challenge {
throw AuthenticationException()
}
validate { cred: UserPasswordCredential ->
if (cred.name == AuthRole.Admin.name) {
AuthUserSession(username = "admin", roles = setOf(AuthRole.Admin))
} else {
AuthUserSession(username = "user", roles = setOf(AuthRole.User))
}
}
}
}
routing {
route("feedback") {
authenticate(authName) {
post {
call.respond(HttpStatusCode.Created, "Submitting feedback")
}
install(AuthorizationPlugin) {
roles = setOf(AuthRole.Admin)
}
get {
call.respond(HttpStatusCode.OK, "Getting feedback")
}
}
}
}
}
/**
* Tests
*/
internal class PluginIssueTest {
/**
* For a valid solution this test should succeed.
*/
#Test
fun testGiveFeedback() = testApplication {
application {
setupConfig()
}
client.post("/feedback") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(
listOf(
usernameFormField to AuthRole.User.name,
passwordFormField to "mypassword"
).formUrlEncode()
)
}.apply {
assertEquals(HttpStatusCode.Created, status)
}
}
/**
* For this test the plugin is successfully called and required role is checked.
*/
#Test
fun testReadFeedback() = testApplication {
application {
setupConfig()
}
client.get("/feedback") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(
listOf(
usernameFormField to AuthRole.Admin.name,
passwordFormField to "mypassword"
).formUrlEncode()
)
}.apply {
assertEquals(HttpStatusCode.OK, status)
}
}
}
I made most of the things internal so they would not interfere with the implementations for my actual application. It should not have any influence on the tests.
Suggestions are highly appreciated.
If I forgot some important information please let me know.

I took a closer look at the implementation of authenticate and found a solution there.
It uses the createChild of the Route class with a custom RouteSelector that always evaluates to a "transparent" quality, to define a specific route for the authentication.
Since I currently only need a single RouteSelector instance I simplified it to be an object instead of a class.
By adding the following implementation to my code...
fun Route.authorize(
roles: Set<AuthRole>,
build: Route.() -> Unit
): Route {
val authenticatedRoute = createChild(AuthorizationRouteSelector)
authenticatedRoute.install(AuthorizationPlugin) {
this.roles = roles
}
authenticatedRoute.build()
return authenticatedRoute
}
object AuthorizationRouteSelector : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Transparent
}
override fun toString(): String = "(authorize \"default\" )"
}
...I was able to use my authorization plugin as follows:
routing {
route("feedback") {
authenticate(authName) {
post {
call.respond(HttpStatusCode.Created, "Submitting feedback")
}
authorize(setOf(AuthRole.Admin)) {
get {
call.respond(HttpStatusCode.OK, "Getting feedback")
}
}
}
}
}
making both tests succeed.

Related

Ktor Server/Application request/response body logging

Is there any way to log the request and response body from the ktor server communication?
The buildin CallLogging feature only logs the metadata of a call. I tried writing my own logging feature like in this example: https://github.com/Koriit/ktor-logging/blob/master/src/main/kotlin/korrit/kotlin/ktor/features/logging/Logging.kt
class Logging(private val logger: Logger) {
class Configuration {
var logger: Logger = LoggerFactory.getLogger(Logging::class.java)
}
private suspend fun logRequest(call: ApplicationCall) {
logger.info(StringBuilder().apply {
appendLine("Received request:")
val requestURI = call.request.path()
appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" })
call.request.headers.forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
try {
appendLine()
appendLine(String(call.receive<ByteArray>()))
} catch (e: RequestAlreadyConsumedException) {
logger.error("Logging payloads requires DoubleReceive feature to be installed with receiveEntireContent=true", e)
}
}.toString())
}
private suspend fun logResponse(call: ApplicationCall, subject: Any) {
logger.info(StringBuilder().apply {
appendLine("Sent response:")
appendLine("${call.request.httpVersion} ${call.response.status()}")
call.response.headers.allValues().forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
when (subject) {
is TextContent -> appendLine(subject.text)
is OutputStreamContent -> appendLine() // ToDo: How to get response body??
else -> appendLine("unknown body type")
}
}.toString())
}
/**
* Feature installation.
*/
fun install(pipeline: Application) {
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
logRequest(call)
proceedWith(subject)
}
pipeline.sendPipeline.addPhase(responseLoggingPhase)
pipeline.sendPipeline.intercept(responseLoggingPhase) {
logResponse(call, subject)
}
}
companion object Feature : ApplicationFeature<Application, Configuration, Logging> {
override val key = AttributeKey<Logging>("Logging Feature")
val responseLoggingPhase = PipelinePhase("ResponseLogging")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Logging {
val configuration = Configuration().apply(configure)
return Logging(configuration.logger).apply { install(pipeline) }
}
}
}
It works fine for logging the request body using the DoubleReceive plugin. And if the response is plain text i can log the response as the subject in the sendPipeline interception will be of type TextContent or like in the example ByteArrayContent.
But in my case i am responding a data class instance with Jackson ContentNegotiation. In this case the subject is of type OutputStreamContent and i see no options to geht the serialized body from it.
Any idea how to log the serialized response json in my logging feature? Or maybe there is another option using the ktor server? I mean i could serialize my object manually and respond plain text, but thats an ugly way to do it.
I'm not shure about if this is the best way to do it, but here it is:
public fun ApplicationResponse.toLogString(subject: Any): String = when(subject) {
is TextContent -> subject.text
is OutputStreamContent -> {
val channel = ByteChannel(true)
runBlocking {
(subject as OutputStreamContent).writeTo(channel)
val buffer = StringBuilder()
while (!channel.isClosedForRead) {
channel.readUTF8LineTo(buffer)
}
buffer.toString()
}
}
else -> String()
}

how implement a gRPC/Proto endpoint with repeated in message (i.e nested types in request and response)

Goal: I want to code a microservice exposing an endpoint receiving and responding a message with repeated. I tried apply what I learned from Proto official guide and I coded this proto:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.mybank.endpoint";
option java_outer_classname = "TransactionsProto";
option objc_class_prefix = "HLW";
package com.mybank.endpoint;
import "google/protobuf/wrappers.proto";
service TransactionsService {
rpc PostTransactions(TransactionsRequest) returns (TransactionsReply);
}
message TransactionsRequest {
string transactionDesc = 1;
repeated Transaction transactions = 2;
}
message Transaction {
string id = 1;
string name = 2;
string description = 3;
}
message TransactionsReply {
string message = 1;
}
I could gradle build and I got this TransactionsServiceGrpcKt autogenerated
package com.mybank.endpoint
import com.mybank.endpoint.TransactionsServiceGrpc.getServiceDescriptor
import io.grpc.CallOptions
import io.grpc.CallOptions.DEFAULT
import io.grpc.Channel
import io.grpc.Metadata
import io.grpc.MethodDescriptor
import io.grpc.ServerServiceDefinition
import io.grpc.ServerServiceDefinition.builder
import io.grpc.ServiceDescriptor
import io.grpc.Status.UNIMPLEMENTED
import io.grpc.StatusException
import io.grpc.kotlin.AbstractCoroutineServerImpl
import io.grpc.kotlin.AbstractCoroutineStub
import io.grpc.kotlin.ClientCalls.unaryRpc
import io.grpc.kotlin.ServerCalls.unaryServerMethodDefinition
import io.grpc.kotlin.StubFor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/**
* Holder for Kotlin coroutine-based client and server APIs for
* com.mybank.endpoint.TransactionsService.
*/
object TransactionsServiceGrpcKt {
#JvmStatic
val serviceDescriptor: ServiceDescriptor
get() = TransactionsServiceGrpc.getServiceDescriptor()
val postTransactionsMethod: MethodDescriptor<TransactionsRequest, TransactionsReply>
#JvmStatic
get() = TransactionsServiceGrpc.getPostTransactionsMethod()
/**
* A stub for issuing RPCs to a(n) com.mybank.endpoint.TransactionsService service as suspending
* coroutines.
*/
#StubFor(TransactionsServiceGrpc::class)
class TransactionsServiceCoroutineStub #JvmOverloads constructor(
channel: Channel,
callOptions: CallOptions = DEFAULT
) : AbstractCoroutineStub<TransactionsServiceCoroutineStub>(channel, callOptions) {
override fun build(channel: Channel, callOptions: CallOptions): TransactionsServiceCoroutineStub
= TransactionsServiceCoroutineStub(channel, callOptions)
/**
* Executes this RPC and returns the response message, suspending until the RPC completes
* with [`Status.OK`][io.grpc.Status]. If the RPC completes with another status, a
* corresponding
* [StatusException] is thrown. If this coroutine is cancelled, the RPC is also cancelled
* with the corresponding exception as a cause.
*
* #param request The request message to send to the server.
*
* #return The single response from the server.
*/
suspend fun postTransactions(request: TransactionsRequest): TransactionsReply = unaryRpc(
channel,
TransactionsServiceGrpc.getPostTransactionsMethod(),
request,
callOptions,
Metadata()
)}
/**
* Skeletal implementation of the com.mybank.endpoint.TransactionsService service based on Kotlin
* coroutines.
*/
abstract class TransactionsServiceCoroutineImplBase(
coroutineContext: CoroutineContext = EmptyCoroutineContext
) : AbstractCoroutineServerImpl(coroutineContext) {
/**
* Returns the response to an RPC for com.mybank.endpoint.TransactionsService.PostTransactions.
*
* If this method fails with a [StatusException], the RPC will fail with the corresponding
* [io.grpc.Status]. If this method fails with a [java.util.concurrent.CancellationException],
* the RPC will fail
* with status `Status.CANCELLED`. If this method fails for any other reason, the RPC will
* fail with `Status.UNKNOWN` with the exception as a cause.
*
* #param request The request from the client.
*/
open suspend fun postTransactions(request: TransactionsRequest): TransactionsReply = throw
StatusException(UNIMPLEMENTED.withDescription("Method com.mybank.endpoint.TransactionsService.PostTransactions is unimplemented"))
final override fun bindService(): ServerServiceDefinition = builder(getServiceDescriptor())
.addMethod(unaryServerMethodDefinition(
context = this.context,
descriptor = TransactionsServiceGrpc.getPostTransactionsMethod(),
implementation = ::postTransactions
)).build()
}
}
So far so good. Now I want to implement it and I am completed stuck.
Here is all three tentatives and its errors
package com.mybank.endpoint
import io.grpc.stub.StreamObserver
import javax.inject.Singleton
#Singleton
class TransactionsEndpoint : TransactionsServiceGrpc.TransactionsServiceImplBase(){
//First tentative
//This complains "'postTransactions' overrides nothing" and IntelliJ suggest second next approach
//override fun postTransactions(request: TransactionsRequest?) : TransactionsReply {
//Second Tentative
// override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) {
// //it complains Type mismatch... Found: TransactionsReply
// return TransactionsReply.newBuilder().setMessage("teste").build()
// }
//Third Tentative
//This causes:
//Return type is 'TransactionsReply', which is not a subtype of overridden public open
// fun postTransactions(request: TransactionsRequest!, responseObserver: StreamObserver<TransactionsReply!>!):
// Unit defined in com.mybank.endpoint.TransactionsServiceGrpc.TransactionsServiceImplBase
//override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) : TransactionsReply {
// return TransactionsReply.newBuilder().setMessage("teste").build()
// }
}
gradle.build
plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.72"
id "org.jetbrains.kotlin.kapt" version "1.3.72"
id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72"
id "application"
id 'com.google.protobuf' version '0.8.13'
}
version "0.2"
group "account-control"
repositories {
mavenLocal()
jcenter()
}
configurations {
// for dependencies that are needed for development only
developmentOnly
}
dependencies {
kapt(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion"))
kapt("io.micronaut:micronaut-inject-java")
kapt("io.micronaut:micronaut-validation")
implementation(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
implementation("io.micronaut:micronaut-runtime")
// implementation("io.micronaut.grpc:micronaut-grpc-runtime")
implementation("io.micronaut.grpc:micronaut-grpc-server-runtime:$micronautGrpcVersion")
implementation("io.micronaut.grpc:micronaut-grpc-client-runtime:$micronautGrpcVersion")
implementation("io.grpc:grpc-kotlin-stub:${grpcKotlinVersion}")
//Kafka
implementation("io.micronaut.kafka:micronaut-kafka")
runtimeOnly("ch.qos.logback:logback-classic:1.2.3")
runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
kaptTest("io.micronaut:micronaut-inject-java")
testImplementation enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.0")
testImplementation("io.micronaut.test:micronaut-test-junit5")
testImplementation("org.mockito:mockito-junit-jupiter:2.22.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.3.0")
testRuntime("org.jetbrains.spek:spek-junit-platform-engine:1.1.5")
}
test.classpath += configurations.developmentOnly
mainClassName = "account-control.Application"
test {
useJUnitPlatform()
}
allOpen {
annotation("io.micronaut.aop.Around")
}
compileKotlin {
kotlinOptions {
jvmTarget = '11'
//Will retain parameter names for Java reflection
javaParameters = true
}
}
//compileKotlin.dependsOn(generateProto)
compileTestKotlin {
kotlinOptions {
jvmTarget = '11'
javaParameters = true
}
}
tasks.withType(JavaExec) {
classpath += configurations.developmentOnly
jvmArgs('-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/grpckt'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:${grpcKotlinVersion}" }
}
generateProtoTasks {
all()*.plugins {
grpc {}
grpckt {}
}
}
}
I have successfully created my first grpc endpoint with very basic request/reply based on String and now I want to move forward by creating a list of a message. As an analogy, let's say I want a DTO/Pojo which contains a list of an entity.
Honestly, I am totally stuck. So, my main question is: how implement a proto service with repeated message?
An useful comment that can give a north is, why I see in autogenerated stub a method to be implemented with "..., responseObserver: StreamObserver?" instead of a simple "TransactionsReply" as I clearly specify in my proto? What relationship between StreamObserver and repeated message?
Here is the whole project in my GitHub develop branch
You will find two protos: one well successful implement with simple request/reply and other failing as explained above.
*** edited after first answer from Louis
I am quite confused.
With a simple proto as
...
service Account {
rpc SendDebit (DebitRequest) returns (DebitReply) {}
}
message DebitRequest {
string name = 1;
}
message DebitReply {
string message = 1;
}
I can implemented with
override suspend fun sendDebit(request: DebitRequest): DebitReply {
return DebitReply.newBuilder().setMessage("teste").build()
}
Nevertheless, with
...
service TransactionsService {
rpc PostTransactions(TransactionsRequest) returns (TransactionsReply);
}
message TransactionsRequest {
string transactionDesc = 1;
repeated Transaction transactions = 2;
}
message Transaction {
string id = 1;
string name = 2;
string description = 3;
}
message TransactionsReply {
string message = 1;
}
I can't override with same type of response (note that the reply is exactly the same)
neither this implementation proposed from IntelliJ
override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) {
super.postTransactions(request, responseObserver)
return TransactionsReply.newBuilder().setMessage("testReply").build()
}
nor this with similar response approach
override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) :TransactionsReply {
super.postTransactions(request, responseObserver)
return TransactionsReply.newBuilder().setMessage("testReply").build()
}
On top of that, why if the reply is exactly the same, in my first approach I didn't get StreamObserver proposed from IntelliJ?
*** Final solution thanks to Louis' help. I extended wrong abstract class
override suspend fun postTransactions(request: TransactionsRequest): TransactionsReply {
return TransactionsReply.newBuilder().setMessage("testReply").build()
}
You're looking for
class TransactionsEndpoint : TransactionsServiceGrpcKt.TransactionsServiceCoroutineImplBase(){
override suspend fun postTransactions(request: TransactionsRequest) : TransactionsReply {
...
}
}
...with the suspend modifier and no ? on the request, extending TransactionsServiceCoroutineImplBase with the Coroutine in there.
Note that this has nothing to do with a repeated message. Your RPC has a single input proto, which gets sent only once -- which may well be what you want, unless you want a stream of requests, in which case your proto file should look like
service TransactionsService {
rpc PostTransactions(stream TransactionsRequest) returns (TransactionsReply);
}
...and the Kotlin generated code will look different in that case.

Kotlin Coroutine - Ktor Server WebSocket

I made a kotlin-ktor application, what i wanted to achieve is that it is modular that anytime any pipelines inside the application maybe removed from the source code without breaking any functionalities. So i decided i want to move a websocket implementation to separate class
but i faced an issue where the coroutine inside the lambda expression terminates immediately.
link-github issue
Can someone enlighten me about the coroutine setup on this, and how I can still keep this as modular without this kind of issue
working ktor websocket
fun Application.socketModule() = runBlocking {
// other declarations
......
routing {
val sessionService = SocketSessionService()
webSocket("/home") {
val chatSession = call.sessions.get<ChatSession>()
println("request session: $chatSession")
if (chatSession == null) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "empty Session"))
return#webSocket
}
send(Frame.Text("connected to server"))
sessionService.addLiveSocket(chatSession.id, this)
sessionService.checkLiveSocket()
}
thread(start = true, name = "socket-monitor") {
launch {
sessionService.checkLiveSocket()
}
}
}
}
kotlin-ktor auto-close web socket
code below closes the socket automatically
Socket Module
class WebSocketServer {
fun createWebSocket(root: String, routing: Routing) {
println("creating web socket server")
routing.installSocketRoute(root)
}
private fun Routing.installSocketRoute(root: String) {
val base = "/message/so"
val socketsWeb = SocketSessionService()
webSocket("$root$base/{type}") {
call.parameters["type"] ?: throw Exception("missing type")
val session = call.sessions.get<ChatSession>()
if (session == null) {
println( "WEB-SOCKET:: client session is null" )
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "No Session"))
return#webSocket
}
socketsWeb.addLiveSocket(session.id, this)
thread(start= true, name = "thread-live-socket") {
launch {
socketsWeb.checkLiveSocket()
}
}
}
}
}
Application Module
fun Application.socketModule() = runBlocking {
// other delcarations
.....
install(Sessions) {
cookie<ChatSession>("SESSION")
}
intercept(ApplicationCallPipeline.Features) {
if (call.sessions.get<ChatSession>() == null) {
val sessionID = generateNonce()
println("generated Session: $sessionID")
call.sessions.set(ChatSession(sessionID))
}
}
routing {
webSocketServer.createWebSocket("/home", this)
}
}
I quite dont understand why the coroutine insdie webSocket lamda is completed.
Can someone show me other/right approach on this one.

How to extract access rights validation in Kotlin's Ktor

I have Ktor based REST API application which uses the jwt token's as authentication. Then I have to restrict certain routes for the specific role. In order to do it I am creating principal, containing the relevant info:
data class UserPrincipal (
val id: Long,
val username: String,
val roleId: Long,
): Princpal {
override fun getName() = username
}
object AuthLogin {
fun Application.auth(jwt: JwtProvider) {
install(Authentication) {
jwt("jwt") {
realm = jwt.realm()
verifier(jwt.verifier())
validate {
val userId = it.payload.getClaim("id").asLong()
val username = it.payload.getClain("name")
val roleId = it.payload.getClaim("roleId").asLong()
UserPrincipal(userId, username, roleId)
}
}
}
}
}
The claims with userId and roleId are being provided when signing the correctly logged in user. Now I can restrict REST endpoints like that:
object RestModule {
fun Application.enititiesOne(userRepo: UserRepo) {
routing {
authenticate("jwt") {
route("/entities1") {
get {
val principal = call.principal<UserPrincipal>()
when(userRepo.hasAccessByRole(principal!!.roleId, "CAN_R_E1") {
false -> call.respond(HttpStatusCode.Forbidden)
true -> // some retrieval logic
}
post {
val principal = call.principal<UserPrincipal>()
when(userRepo.hasAccessByRole(principal!!.roleId, "CAN_W_E1") {
false -> call.respond(HttpStatusCode.Forbidden)
true -> // some update logic
}
}
}
}
}
}
As you can see even inside one routing function I have to duplicate code that checks the principal's role twice. I can move it out to function but what I want is a single place to define my security roles. Something like that:
authenticate {
val principal = call.principal<UserPrincipal()
val rights = userRepo.rightsByRole(principal.roleId)
when(routes) {
get("/entities1/**") ->
if(rights.contain("CAN_R_E1")) call.proceed
else call.respond(HttpStatusCode.Forbidden)
post("/entites1) -> rights.contain("CAN_W_E1") // similar
get("/entities2/**") -> rights.contain("CAN_R_E2") // similar
else -> call.respond(401)
}
}
And then plug it into the rest endpoints. Or is there some similar approach that I can use in Kotlin's Ktor? Seems like interceptors is what I need but I'm not sure how to use them in an intended way.
You can check the method and uri in the validate block.
install(Authentication) {
jwt {
validate {
val userId = it.payload.getClaim("id").asLong()
val username = it.payload.getClaim("name").asString()
val roleId = it.payload.getClaim("roleId").asLong()
UserPrincipal(userId, username, roleId)
val requiredRole = when (request.httpMethod) {
HttpMethod.Get -> // get role
HttpMethod.Post -> // get other role
}
// check if role exists in repo
}
}
}
install(Routing) {
get {
val principal = call.principal<UserPrincipal>()!!
call.respond(principal)
}
post {
val principal = call.principal<UserPrincipal>()!!
call.respond(principal)
}
}
By the way, there were several issues with the code you posted, so it wouldn't compile.

How to be notified when all futures in Future.compose chain succeeded?

My application (typical REST server that calls other REST services internally) has two main classes to perform the bootstrapping procedure.
There is the Application.kt class that is supposed to configure the vertx instance itself and to register certain modules (jackson kotlin integration for example):
class Application(
private val profileSetting: String? = System.getenv("ACTIVE_PROFILES"),
private val logger: Logger = LoggerFactory.getLogger(Application::class.java)!!
) {
fun bootstrap() {
val profiles = activeProfiles()
val meterRegistry = configureMeters()
val vertx = bootstrapVertx(meterRegistry)
vertx.deployVerticle(ApplicationBootstrapVerticle(profiles)) { startup ->
if (startup.succeeded()) {
logger.info("Application startup finished")
} else {
logger.error("Application startup failed", startup.cause())
vertx.close()
}
}
}
}
In addition there is a ApplicationBootstrapVerticle.kt class that is supposed to deploy the different verticles in a defined order. Some of them in sequence, some of them in parallel:
class ApplicationBootstrapVerticle(
private val profiles: List<String>,
private val logger: Logger = LoggerFactory.getLogger(ApplicationBootstrapVerticle::class.java)
) : AbstractVerticle() {
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}
private fun initializeApplicationConfig(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
ApplicationConfigVerticle(profiles),
it.completer()
)
}
}
private fun initializeScheduledJobs(): CompositeFuture {
val stationsJob = Future.future<String>()
val capabilitiesJob = Future.future<String>()
return CompositeFuture.all(stationsJob, capabilitiesJob).also {
vertx.deployVerticle(
StationQualitiesVerticle(),
stationsJob.completer()
)
vertx.deployVerticle(
VideoCapabilitiesVerticle(),
capabilitiesJob.completer()
)
}
}
private fun initializeRestEndpoints(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
RestEndpointVerticle(dispatcherFactory = RouteDispatcherFactory(vertx)),
it.completer()
)
}
}
}
I am not sure if this is the supposed way to bootstrap an application, if there is any. More important though, I am not sure if I understand the Future.compose mechanics correctly.
The application starts up successfully and I see all desired log messages except the
Application startup finished
message. Also the following code is never called in case of successs:
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
In case of an failure though, for example when my application configuration files (yaml) cannot be parsed because there is an unknown field in the destination entity, the log message
Application startup failed
appears in the logs and also the code above is invoked.
I am curious what is wrong with my composed futures chain. I thought that the handler would be called after the previous futures succeeded or one of them failed but I think it's only called in case of success.
Update
I suppose that an invocation of startFuture.complete() was missing. By adapting the start method, it finally worked:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture.complete()
startFuture
}.setHandler(
startFuture.completer()
)
}
I am not sure though, if this is the supposed way to handle this future chain.
The solution that worked for me looks like this:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.setHandler { ar ->
if(ar.succeeded()) {
logger.info("Http server started")
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}