Skip to content

Commit a6766a0

Browse files
committed
Fikser plugin for å kreve callId på endepunkter.
1 parent 7d7d741 commit a6766a0

File tree

4 files changed

+60
-73
lines changed

4 files changed

+60
-73
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
package no.nav.helse.dusseldorf.ktor.core
22

3-
import io.ktor.server.application.ApplicationCall
4-
import io.ktor.server.application.ApplicationCallPipeline
5-
import io.ktor.server.application.BaseApplicationPlugin
6-
import io.ktor.server.plugins.callid.callId
7-
import io.ktor.server.plugins.callid.CallIdConfig
83
import io.ktor.http.HttpHeaders
94
import io.ktor.http.encodeURLParameter
10-
import io.ktor.server.application.PipelineCall
5+
import io.ktor.server.plugins.callid.CallIdConfig
116
import io.ktor.server.request.header
12-
import io.ktor.server.routing.Route
13-
import io.ktor.server.routing.application
14-
import io.ktor.util.AttributeKey
15-
import io.ktor.util.pipeline.PipelineContext
167
import org.slf4j.Logger
178
import org.slf4j.LoggerFactory
189
import java.util.*
@@ -22,21 +13,27 @@ internal object IdVerifier {
2213
private const val NorskeBokstaver = "æøåÆØÅ"
2314
private const val GeneratedIdPrefix = "generated-"
2415

25-
private val idRegex = "[a-zA-Z0-9_.\\-${NorskeBokstaver}]{5,200}".toRegex()
26-
internal fun verifyId(type: String, id:String) = idRegex.matches(id).also { valid ->
16+
private val idRegex = "[a-zA-Z0-9_.\\-${NorskeBokstaver}]{5,200}".toRegex()
17+
internal fun verifyId(type: String, id: String) = idRegex.matches(id).also { valid ->
2718
if (!valid) logger.warn("Ugyldig $type=[${id.encodeURLParameter()}] (url-encoded)")
2819
}
20+
2921
internal fun generate() = "$GeneratedIdPrefix${UUID.randomUUID()}"
3022
internal fun String.trimId() = removePrefix(GeneratedIdPrefix)
3123
}
3224

33-
fun CallIdConfig.fromFirstNonNullHeader(headers: List<String>, generateOnInvalid: Boolean = false, generateOnNotSet: Boolean = false) {
25+
fun CallIdConfig.fromFirstNonNullHeader(
26+
headers: List<String>,
27+
generateOnInvalid: Boolean = false,
28+
generateOnNotSet: Boolean = false,
29+
) {
3430
retrieve { call ->
3531
when (val fromHeaders = headers.mapNotNull { call.request.header(it) }.firstOrNull()) {
3632
null -> when (generateOnNotSet) {
3733
true -> IdVerifier.generate()
3834
false -> fromHeaders
3935
}
36+
4037
else -> when (IdVerifier.verifyId(type = HttpHeaders.XCorrelationId, id = fromHeaders)) {
4138
true -> fromHeaders
4239
false -> when (generateOnInvalid) {
@@ -51,52 +48,12 @@ fun CallIdConfig.fromFirstNonNullHeader(headers: List<String>, generateOnInvalid
5148
}
5249

5350
// Henter fra CorrelationID (backend tjenester)
54-
fun CallIdConfig.fromXCorrelationIdHeader(generateOnInvalid: Boolean = false, generateOnNotSet: Boolean = false) = fromFirstNonNullHeader(
55-
headers = listOf(HttpHeaders.XCorrelationId), generateOnInvalid = generateOnInvalid, generateOnNotSet = false
56-
)
51+
fun CallIdConfig.fromXCorrelationIdHeader(generateOnInvalid: Boolean = false, generateOnNotSet: Boolean = false) =
52+
fromFirstNonNullHeader(
53+
headers = listOf(HttpHeaders.XCorrelationId), generateOnInvalid = generateOnInvalid, generateOnNotSet = false
54+
)
5755

5856
// Genererer CorrelationID (frontend tjeneste)
5957
fun CallIdConfig.generated() {
6058
generate { IdVerifier.generate() }
6159
}
62-
63-
class Configuration
64-
class CallIdRequired(private val configure: Configuration) {
65-
66-
private val logger = LoggerFactory.getLogger("no.nav.helse.dusseldorf.ktor.core.CallIdRequired")
67-
68-
private val problemDetails = ValidationProblemDetails(
69-
setOf(Violation(
70-
parameterName = HttpHeaders.XCorrelationId,
71-
parameterType = ParameterType.HEADER,
72-
reason = "Correlation ID må settes.",
73-
invalidValue = null
74-
))
75-
)
76-
77-
fun interceptPipeline(route: Route) {
78-
route.application.intercept(ApplicationCallPipeline.Monitoring) {
79-
require(this)
80-
}
81-
}
82-
83-
private suspend fun require(context: PipelineContext<Unit, PipelineCall>) {
84-
val callId = context.context.callId
85-
if (callId == null) {
86-
context.context.respondProblemDetails(problemDetails, logger)
87-
context.finish()
88-
} else {
89-
context.proceed()
90-
}
91-
}
92-
93-
companion object Feature :
94-
BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CallIdRequired> {
95-
96-
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CallIdRequired {
97-
return CallIdRequired(Configuration().apply(configure))
98-
}
99-
100-
override val key = AttributeKey<CallIdRequired>("CallIdRequired")
101-
}
102-
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
package no.nav.helse.dusseldorf.ktor.core
22

3-
import io.ktor.server.application.plugin
4-
import io.ktor.server.routing.*
3+
import io.ktor.http.HttpHeaders
4+
import io.ktor.server.application.PipelineCall
5+
import io.ktor.server.application.createRouteScopedPlugin
6+
import io.ktor.server.plugins.callid.callId
7+
import io.ktor.server.routing.Route
8+
import io.ktor.server.routing.RouteSelector
9+
import io.ktor.server.routing.RouteSelectorEvaluation
10+
import io.ktor.server.routing.RoutingResolveContext
511

6-
fun Route.requiresCallId(
7-
build: Route.() -> Unit
8-
): Route {
9-
val requiresCallIdRoutes: Route = createChild(RequiresCallIdRouteSelector())
10-
application.plugin(CallIdRequired).interceptPipeline(requiresCallIdRoutes)
11-
requiresCallIdRoutes.build()
12-
return requiresCallIdRoutes
12+
fun Route.requiresCallId(build: Route.() -> Unit): Route {
13+
return createChild(object : RouteSelector() {
14+
override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
15+
return RouteSelectorEvaluation.Constant
16+
}
17+
override fun toString(): String = "RequiresCallId"
18+
}).apply {
19+
install(CallIdRequiredPlugin)
20+
build()
21+
}
1322
}
1423

15-
private class RequiresCallIdRouteSelector : RouteSelector() {
16-
override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
17-
return RouteSelectorEvaluation.Constant
18-
}
24+
// Definerer route-scoped plugin som sjekker at det eksisterer callId på request.
25+
val CallIdRequiredPlugin = createRouteScopedPlugin("CallIdRequiredPlugin") {
26+
val problemDetails = ValidationProblemDetails(
27+
setOf(Violation(
28+
parameterName = HttpHeaders.XCorrelationId,
29+
parameterType = ParameterType.HEADER,
30+
reason = "Correlation ID må settes.",
31+
invalidValue = null
32+
))
33+
)
1934

20-
override fun toString() = "RequiresCallId"
35+
onCall { call: PipelineCall ->
36+
if (call.callId == null) {
37+
call.respondProblemDetails(problemDetails)
38+
return@onCall
39+
}
40+
}
2141
}

Diff for: dusseldorf-ktor-testapp/src/main/kotlin/no/nav/App.kt

-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import no.nav.helse.dusseldorf.ktor.client.HttpRequestHealthCheck
1818
import no.nav.helse.dusseldorf.ktor.client.HttpRequestHealthConfig
1919
import no.nav.helse.dusseldorf.ktor.client.SimpleHttpClient.httpGet
2020
import no.nav.helse.dusseldorf.ktor.client.SimpleHttpClient.readTextOrThrow
21-
import no.nav.helse.dusseldorf.ktor.core.CallIdRequired
2221
import no.nav.helse.dusseldorf.ktor.core.DefaultProbeRoutes
2322
import no.nav.helse.dusseldorf.ktor.core.FullførAktiveRequester
2423
import no.nav.helse.dusseldorf.ktor.core.PreStopRoute
@@ -46,7 +45,6 @@ fun Application.app() {
4645
jackson {}
4746
}
4847

49-
install(CallIdRequired)
5048
install(CallId) {
5149
fromXCorrelationIdHeader()
5250
}

Diff for: dusseldorf-ktor-testapp/src/test/kotlin/no/nav/AppTest.kt

+12
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ internal class AppTest {
4848
}
4949
}
5050

51+
@Test
52+
fun `Tester health og metrics endepunkter ikke trenger callId`() {
53+
withNettyEngine(appPort = 1337) {
54+
listOf("/isready", "/isalive", "/metrics", "/health").forEach {
55+
val getResponse: Pair<HttpRequestData, Result<HttpResponse>> = "http://localhost:1337$it".httpGet()
56+
57+
val responseText = getResponse.readTextOrThrow()
58+
assertEquals(HttpStatusCode.OK, responseText.first, responseText.second)
59+
}
60+
}
61+
}
62+
5163
private suspend fun doNTimes(n: Int = 20, block: suspend () -> Any) {
5264
for (i in 1..n) { block() }
5365
}

0 commit comments

Comments
 (0)