diff --git a/apps/bekreftelse-api/build.gradle.kts b/apps/bekreftelse-api/build.gradle.kts index 8c8e3236..fb5d4b90 100644 --- a/apps/bekreftelse-api/build.gradle.kts +++ b/apps/bekreftelse-api/build.gradle.kts @@ -12,40 +12,40 @@ val image: String? by project dependencies { // Project implementation(project(":lib:hoplite-config")) - implementation(project(":lib:error-handling")) + implementation(project(":lib:error-handling-ktor3")) implementation(project(":lib:security")) implementation(project(":lib:kafka-streams")) - implementation(project(":lib:kafka-key-generator-client")) + implementation(project(":lib:kafka-key-generator-client-ktor3")) implementation(project(":domain:bekreftelse-interne-hendelser")) implementation(project(":domain:bekreftelsesmelding-avro-schema")) // Server - implementation(libs.bundles.ktorServerWithNettyAndMicrometer) - implementation(libs.ktor.server.contentNegotiation) - implementation(libs.ktor.server.statusPages) - implementation(libs.ktor.server.cors) - implementation(libs.ktor.server.callId) - implementation(libs.ktor.server.auth) + implementation(libs.bundles.ktor3ServerWithNettyAndMicrometer) + implementation(libs.ktor3.server.contentNegotiation) + implementation(libs.ktor3.server.statusPages) + implementation(libs.ktor3.server.cors) + implementation(libs.ktor3.server.callId) + implementation(libs.ktor3.server.auth) // Client - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.cio) - implementation(libs.ktor.client.contentNegotiation) + implementation(libs.ktor3.client.core) + implementation(libs.ktor3.client.cio) + implementation(libs.ktor3.client.contentNegotiation) // Serialization - implementation(libs.ktor.serialization.jackson) - implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor3.serialization.jackson) + implementation(libs.ktor3.serialization.kotlinx.json) implementation(libs.jackson.datatypeJsr310) // Authentication - implementation(libs.nav.security.tokenValidationKtorV2) + implementation(libs.nav.security.tokenValidationKtorV3) // Authorization implementation(libs.nav.poao.tilgangClient) // Documentation - implementation(libs.ktor.server.openapi) - implementation(libs.ktor.server.swagger) + implementation(libs.ktor3.server.openapi) + implementation(libs.ktor3.server.swagger) // Logging implementation(libs.logbackClassic) @@ -71,8 +71,8 @@ dependencies { implementation(libs.avro.kafkaStreamsSerde) // Test - testImplementation(libs.ktor.server.testJvm) - testImplementation(libs.ktor.client.mock) + testImplementation(libs.ktor3.server.test.host) + testImplementation(libs.ktor3.client.mock) testImplementation(libs.bundles.testLibsWithUnitTesting) testImplementation(libs.test.mockOauth2Server) testImplementation(libs.test.testContainers.postgresql) diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Authentication.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Authentication.kt index 09a3e26e..2369a8cb 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Authentication.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Authentication.kt @@ -3,10 +3,10 @@ package no.nav.paw.bekreftelse.api.plugins import io.ktor.server.application.Application import io.ktor.server.auth.authentication import no.nav.paw.bekreftelse.api.context.ApplicationContext -import no.nav.security.token.support.v2.IssuerConfig -import no.nav.security.token.support.v2.RequiredClaims -import no.nav.security.token.support.v2.TokenSupportConfig -import no.nav.security.token.support.v2.tokenValidationSupport +import no.nav.security.token.support.v3.IssuerConfig +import no.nav.security.token.support.v3.RequiredClaims +import no.nav.security.token.support.v3.TokenSupportConfig +import no.nav.security.token.support.v3.tokenValidationSupport fun Application.configureAuthentication(applicationContext: ApplicationContext) { with(applicationContext.securityConfig) { diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Logging.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Logging.kt index 46f295c9..cc91df72 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Logging.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Logging.kt @@ -2,7 +2,7 @@ package no.nav.paw.bekreftelse.api.plugins import io.ktor.server.application.Application import io.ktor.server.application.install -import io.ktor.server.plugins.callloging.CallLogging +import io.ktor.server.plugins.calllogging.CallLogging import io.ktor.server.request.path fun Application.configureLogging() { diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt index 33e2a3ca..b9ddf3d8 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt @@ -8,7 +8,7 @@ import io.ktor.server.application.ApplicationStopping import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent import io.ktor.server.application.log -import io.ktor.util.KtorDsl +import io.ktor.utils.io.KtorDsl import org.jetbrains.exposed.sql.Database import javax.sql.DataSource @@ -31,7 +31,7 @@ val DataSourcePlugin: ApplicationPlugin = on(MonitoringEvent(ApplicationStarted)) { application -> application.log.info("Initializing data source") Database.connect(dataSource) - application.environment.monitor.raise(DataSourceReady, application) + application.monitor.raise(DataSourceReady, application) } on(MonitoringEvent(ApplicationStopping)) { application -> diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt index ca48a939..cb9ef114 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt @@ -6,7 +6,7 @@ import io.ktor.server.application.ApplicationPlugin import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent import io.ktor.server.application.log -import io.ktor.util.KtorDsl +import io.ktor.utils.io.KtorDsl import org.flywaydb.core.Flyway import javax.sql.DataSource @@ -36,6 +36,6 @@ val FlywayPlugin: ApplicationPlugin = on(MonitoringEvent(DataSourceReady)) { application -> application.log.info("Running database migration") flyway.migrate() - application.environment.monitor.raise(FlywayMigrationCompleted, application) + application.monitor.raise(FlywayMigrationCompleted, application) } } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt index adeb6670..1d5ac274 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt @@ -8,7 +8,7 @@ import io.ktor.server.application.ApplicationStopping import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent import io.ktor.server.application.log -import io.ktor.util.KtorDsl +import io.ktor.utils.io.KtorDsl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt index 00adc9a9..486a173a 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt @@ -5,7 +5,7 @@ import io.ktor.server.application.ApplicationStopping import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent import io.ktor.server.application.log -import io.ktor.util.KtorDsl +import io.ktor.utils.io.KtorDsl import org.apache.kafka.clients.producer.Producer import java.time.Duration diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt index 8b9fd07f..8b95bcc0 100644 --- a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt @@ -6,7 +6,7 @@ import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent import io.ktor.server.application.install import io.ktor.server.application.log -import io.ktor.util.KtorDsl +import io.ktor.utils.io.KtorDsl import no.nav.paw.bekreftelse.api.models.BekreftelseRow import no.nav.paw.bekreftelse.api.plugins.custom.FlywayMigrationCompleted import no.nav.paw.bekreftelse.api.repository.BekreftelseRepository diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/test/ApplicationTestContext.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/test/ApplicationTestContext.kt index 6fd606f9..85028b0e 100644 --- a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/test/ApplicationTestContext.kt +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/test/ApplicationTestContext.kt @@ -33,13 +33,8 @@ import no.nav.paw.health.model.LivenessHealthIndicator import no.nav.paw.health.model.ReadinessHealthIndicator import no.nav.paw.health.repository.HealthIndicatorRepository import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient -import no.nav.paw.security.authentication.config.AuthProvider -import no.nav.paw.security.authentication.config.AuthProviderClaims import no.nav.paw.security.authentication.config.SECURITY_CONFIG import no.nav.paw.security.authentication.config.SecurityConfig -import no.nav.paw.security.authentication.token.AzureAd -import no.nav.paw.security.authentication.token.IdPorten -import no.nav.paw.security.authentication.token.TokenX import no.nav.poao_tilgang.client.PoaoTilgangClient import no.nav.security.mock.oauth2.MockOAuth2Server import org.apache.kafka.clients.consumer.KafkaConsumer diff --git a/lib/error-handling-ktor3/build.gradle.kts b/lib/error-handling-ktor3/build.gradle.kts new file mode 100644 index 00000000..80697f46 --- /dev/null +++ b/lib/error-handling-ktor3/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("jvm") +} + +dependencies { + compileOnly(libs.ktor3.server.cors) + compileOnly(libs.ktor3.serialization.jackson) + compileOnly(libs.kafka.streams.core) + compileOnly(libs.logbackClassic) + + //Test + testImplementation(libs.bundles.testLibsWithUnitTesting) + testImplementation(libs.ktor3.server.test.host) + testImplementation(libs.ktor3.server.contentNegotiation) + testImplementation(libs.ktor3.server.statusPages) + testImplementation(libs.ktor3.serialization.jackson) + testImplementation(libs.ktor3.client.contentNegotiation) + testImplementation(libs.ktor3.server.core) + testImplementation(libs.kafka.streams.core) + testImplementation(libs.jackson.datatypeJsr310) + testImplementation(libs.logbackClassic) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ClientResponseException.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ClientResponseException.kt new file mode 100644 index 00000000..2610ecab --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ClientResponseException.kt @@ -0,0 +1,11 @@ +package no.nav.paw.error.exception + +import io.ktor.http.HttpStatusCode +import java.net.URI + +open class ClientResponseException( + val status: HttpStatusCode, + override val type: URI, + override val message: String, + override val cause: Throwable? = null +) : ErrorTypeAwareException(type, message, cause) \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ErrorTypeAwareException.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ErrorTypeAwareException.kt new file mode 100644 index 00000000..2651f85b --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ErrorTypeAwareException.kt @@ -0,0 +1,11 @@ +package no.nav.paw.error.exception + +import java.net.URI + +open class ErrorTypeAwareException( + open val type: URI, + override val message: String, + override val cause: Throwable? = null +) : + Exception(message, cause) { +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ProblemDetailsException.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ProblemDetailsException.kt new file mode 100644 index 00000000..2a6fb5ba --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ProblemDetailsException.kt @@ -0,0 +1,8 @@ +package no.nav.paw.error.exception + +import no.nav.paw.error.model.ProblemDetails + +open class ProblemDetailsException(val details: ProblemDetails): ErrorTypeAwareException( + type = details.type, + message = details.title +) diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ServerResponseException.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ServerResponseException.kt new file mode 100644 index 00000000..99925df0 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/exception/ServerResponseException.kt @@ -0,0 +1,11 @@ +package no.nav.paw.error.exception + +import io.ktor.http.HttpStatusCode +import java.net.URI + +open class ServerResponseException( + val status: HttpStatusCode, + override val type: URI, + override val message: String, + override val cause: Throwable? = null +) : ErrorTypeAwareException(type, message, cause) \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt new file mode 100644 index 00000000..60b98710 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt @@ -0,0 +1,107 @@ +package no.nav.paw.error.handler + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.plugins.BadRequestException +import io.ktor.server.plugins.ContentTransformationException +import io.ktor.server.request.ApplicationRequest +import io.ktor.server.request.RequestAlreadyConsumedException +import io.ktor.server.request.uri +import io.ktor.server.response.respond +import no.nav.paw.error.exception.ClientResponseException +import no.nav.paw.error.exception.ServerResponseException +import no.nav.paw.error.model.ErrorType +import no.nav.paw.error.model.ProblemDetails +import no.nav.paw.error.model.ProblemDetailsBuilder +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.MDC + +private val logger: Logger = LoggerFactory.getLogger("no.nav.paw.logger.error.http") +private const val MDC_ERROR_ID_KEY = "x_error_id" +private const val MDC_ERROR_TYPE_KEY = "x_error_type" +private const val MDC_EXCEPTION_KEY = "exception" + +suspend fun ApplicationCall.handleException( + throwable: Throwable, + resolver: (throwable: Throwable) -> ProblemDetails? = { null } +) { + val problemDetails = resolveProblemDetails(request, throwable, resolver) + + MDC.put(MDC_ERROR_ID_KEY, problemDetails.id.toString()) + MDC.put(MDC_ERROR_TYPE_KEY, problemDetails.type.toString()) + MDC.put(MDC_EXCEPTION_KEY, throwable.javaClass.canonicalName) + + logger.error(problemDetails.detail, throwable) + + MDC.remove(MDC_ERROR_ID_KEY) + MDC.remove(MDC_ERROR_TYPE_KEY) + MDC.remove(MDC_EXCEPTION_KEY) + + respond(problemDetails.status, problemDetails) +} + +fun resolveProblemDetails( + request: ApplicationRequest, + throwable: Throwable, + resolver: (throwable: Throwable) -> ProblemDetails? = { null } +): ProblemDetails { + val problemDetails = resolver(throwable) + if (problemDetails != null) { + return problemDetails + } + + when (throwable) { + is BadRequestException -> { + return ProblemDetailsBuilder.builder() + .type(ErrorType.domain("http").error("kunne-ikke-tolke-forespoersel").build()) + .status(HttpStatusCode.BadRequest) + .detail("Kunne ikke tolke forespørsel") + .instance(request.uri) + .build() + } + + is ContentTransformationException -> { + return ProblemDetailsBuilder.builder() + .type(ErrorType.domain("http").error("kunne-ikke-tolke-innhold").build()) + .status(HttpStatusCode.BadRequest) + .detail("Kunne ikke tolke innhold i forespørsel") + .instance(request.uri) + .build() + } + + is RequestAlreadyConsumedException -> { + return ProblemDetailsBuilder.builder() + .type(ErrorType.domain("http").error("forespoersel-allerede-mottatt").build()) + .status(HttpStatusCode.InternalServerError) + .detail("Forespørsel er allerede mottatt. Dette er en kodefeil") + .instance(request.uri) + .build() + } + + is ServerResponseException -> { + return ProblemDetailsBuilder.builder() + .type(throwable.type) + .status(throwable.status) + .detail(throwable.message) + .instance(request.uri) + .build() + } + + is ClientResponseException -> { + return ProblemDetailsBuilder.builder() + .type(throwable.type) + .status(throwable.status) + .detail(throwable.message) + .instance(request.uri) + .build() + } + + else -> { + return ProblemDetailsBuilder.builder() + .detail("Forespørsel feilet med ukjent feil") + .instance(request.uri) + .build() + } + } +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/handler/KafkaExceptionHandler.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/handler/KafkaExceptionHandler.kt new file mode 100644 index 00000000..1af2a960 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/handler/KafkaExceptionHandler.kt @@ -0,0 +1,17 @@ +package no.nav.paw.error.handler + +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +private val logger: Logger = LoggerFactory.getLogger("no.nav.paw.logger.error.kafka") + +fun KafkaStreams.withApplicationTerminatingExceptionHandler() { + this.setUncaughtExceptionHandler(createApplicationTerminatingExceptionHandler()) +} + +fun createApplicationTerminatingExceptionHandler() = StreamsUncaughtExceptionHandler { throwable -> + logger.error("Kafka Streams opplevde en uventet feil", throwable) + StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_APPLICATION +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/ErrorType.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/ErrorType.kt new file mode 100644 index 00000000..9b13333a --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/ErrorType.kt @@ -0,0 +1,27 @@ +package no.nav.paw.error.model + +import java.net.URI + +object ErrorTypeDefaults { + const val TEAM = "paw" + const val DOMAIN = "default" + const val ERROR = "ukjent-feil" +} + +class ErrorType( + var team: String = ErrorTypeDefaults.TEAM, + var domain: String = ErrorTypeDefaults.DOMAIN, + var error: String = ErrorTypeDefaults.ERROR, +) { + fun team(team: String) = apply { this.team = team } + fun domain(domain: String) = apply { this.domain = domain } + fun error(error: String) = apply { this.error = error } + fun build(): URI = URI.create("urn:${team.lowercase()}:${domain.lowercase()}:${error.lowercase()}") + + companion object { + fun team(team: String): ErrorType = ErrorType(team = team) + fun domain(domain: String): ErrorType = ErrorType(domain = domain) + fun error(error: String): ErrorType = ErrorType(error = error) + fun default(): ErrorType = ErrorType() + } +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt new file mode 100644 index 00000000..08c170c5 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt @@ -0,0 +1,45 @@ +package no.nav.paw.error.model + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import io.ktor.http.HttpStatusCode +import no.nav.paw.error.serialize.HttpStatusCodeDeserializer +import no.nav.paw.error.serialize.HttpStatusCodeSerializer +import java.net.URI +import java.time.Instant +import java.util.* + +/** + * Object som inneholder detaljer om en oppstått feilsituasjon, basert på RFC 9457. + * @see IETF RFC 9457 + */ +data class ProblemDetails( + val id: UUID = UUID.randomUUID(), + val type: URI = ErrorType.default().build(), + @JsonSerialize(using = HttpStatusCodeSerializer::class) @JsonDeserialize(using = HttpStatusCodeDeserializer::class) val status: HttpStatusCode, + val title: String, + val detail: String? = null, + val instance: String, + val timestamp: Instant = Instant.now() +): Response + +class ProblemDetailsBuilder private constructor( + var type: URI = ErrorType.default().build(), + var status: HttpStatusCode = HttpStatusCode.InternalServerError, + var title: String? = null, + var detail: String? = null, + var instance: String = "/" +) { + fun type(type: URI) = apply { this.type = type } + fun status(status: HttpStatusCode) = apply { this.status = status } + fun title(title: String) = apply { this.title = title } + fun detail(detail: String) = apply { this.detail = detail } + fun instance(instance: String) = apply { this.instance = instance } + fun build(): ProblemDetails = ProblemDetails( + type = type, status = status, title = title ?: status.description, detail = detail, instance = instance + ) + + companion object { + fun builder(): ProblemDetailsBuilder = ProblemDetailsBuilder() + } +} diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/Response.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/Response.kt new file mode 100644 index 00000000..a0ae8d93 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/model/Response.kt @@ -0,0 +1,30 @@ +package no.nav.paw.error.model + +import no.nav.paw.error.exception.ProblemDetailsException + +sealed interface Response + +data class Data(val data: T) : Response + +fun Response.getOrThrow(transform: (ProblemDetails) -> ProblemDetailsException = ::ProblemDetailsException):T { + return when(this) { + is Data -> this.data + is ProblemDetails -> throw transform(this) + } +} + +fun Response.map(transform: (T1) -> T2): Response { + return when(this) { + is Data -> Data(transform(this.data)) + is ProblemDetails -> this + } +} + +fun Response.flatMap(transform: (T1) -> Response): Response { + return when(this) { + is Data -> transform(this.data) + is ProblemDetails -> this + } +} + + diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeDeserializer.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeDeserializer.kt new file mode 100644 index 00000000..e175cf96 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeDeserializer.kt @@ -0,0 +1,12 @@ +package no.nav.paw.error.serialize + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import io.ktor.http.HttpStatusCode + +class HttpStatusCodeDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): HttpStatusCode { + return HttpStatusCode.fromValue(parser.numberValue.toInt()) + } +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeSerializer.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeSerializer.kt new file mode 100644 index 00000000..df0554bd --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeSerializer.kt @@ -0,0 +1,12 @@ +package no.nav.paw.error.serialize + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import io.ktor.http.HttpStatusCode + +class HttpStatusCodeSerializer : JsonSerializer() { + override fun serialize(value: HttpStatusCode, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeNumber(value.value) + } +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/listener/KafkaStreamsStatusListener.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/listener/KafkaStreamsStatusListener.kt new file mode 100644 index 00000000..f407b92e --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/listener/KafkaStreamsStatusListener.kt @@ -0,0 +1,66 @@ +package no.nav.paw.health.listener + +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator +import org.apache.kafka.streams.KafkaStreams +import org.slf4j.LoggerFactory + +private val logger = LoggerFactory.getLogger("no.nav.paw.logger.health.kafka") + +fun KafkaStreams.withHealthIndicatorStateListener( + livenessIndicator: LivenessHealthIndicator, + readinessIndicator: ReadinessHealthIndicator +) { + this.setStateListener(createHealthIndicatorStateListener(livenessIndicator, readinessIndicator)) +} + +fun createHealthIndicatorStateListener( + livenessIndicator: LivenessHealthIndicator, + readinessIndicator: ReadinessHealthIndicator +) = KafkaStreams.StateListener { newState, previousState -> + when (newState) { + KafkaStreams.State.CREATED -> { + readinessIndicator.setUnhealthy() + livenessIndicator.setHealthy() + } + + KafkaStreams.State.RUNNING -> { + readinessIndicator.setHealthy() + livenessIndicator.setHealthy() + } + + KafkaStreams.State.REBALANCING -> { + readinessIndicator.setHealthy() + livenessIndicator.setHealthy() + } + + KafkaStreams.State.PENDING_ERROR -> { + readinessIndicator.setUnhealthy() + livenessIndicator.setHealthy() + } + + KafkaStreams.State.ERROR -> { + readinessIndicator.setUnhealthy() + livenessIndicator.setUnhealthy() + } + + KafkaStreams.State.PENDING_SHUTDOWN -> { + readinessIndicator.setUnhealthy() + livenessIndicator.setUnhealthy() + } + + KafkaStreams.State.NOT_RUNNING -> { + readinessIndicator.setUnhealthy() + livenessIndicator.setUnhealthy() + } + + null -> { + readinessIndicator.setUnknown() + livenessIndicator.setUnknown() + } + } + + logger.debug("Kafka Streams state endret seg ${previousState.name} -> ${newState.name}") + logger.info("Kafka Streams liveness er ${livenessIndicator.getStatus().value}") + logger.info("Kafka Streams readiness er ${readinessIndicator.getStatus().value}") +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/model/Health.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/model/Health.kt new file mode 100644 index 00000000..bf398cce --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/model/Health.kt @@ -0,0 +1,72 @@ +package no.nav.paw.health.model + +import java.util.concurrent.atomic.AtomicReference + +enum class HealthStatus(val value: String) { + UNKNOWN("UNKNOWN"), + HEALTHY("HEALTHY"), + UNHEALTHY("UNHEALTHY"), +} + +interface HealthIndicator { + fun getStatus(): HealthStatus +} + +interface MutableHealthIndicator : HealthIndicator { + fun setUnknown(); + + fun setHealthy(); + + fun setUnhealthy(); +} + +open class DefaultHealthIndicator(initialStatus: HealthStatus) : MutableHealthIndicator { + + private val status = AtomicReference(initialStatus) + + override fun setUnknown() { + status.set(HealthStatus.UNKNOWN) + } + + override fun setHealthy() { + status.set(HealthStatus.HEALTHY) + } + + override fun setUnhealthy() { + status.set(HealthStatus.UNHEALTHY) + } + + override fun getStatus(): HealthStatus { + return status.get() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DefaultHealthIndicator) return false + return status == other.status + } + + override fun hashCode(): Int { + return status.hashCode() + } +} + +typealias HealthIndicatorList = MutableList + +fun HealthIndicatorList.getAggregatedStatus(): HealthStatus { + return if (this.isEmpty()) { + HealthStatus.HEALTHY // Default til healthy + } else if (this.all { it.getStatus() == HealthStatus.HEALTHY }) { + HealthStatus.HEALTHY + } else if (this.any { it.getStatus() == HealthStatus.UNHEALTHY }) { + HealthStatus.UNHEALTHY + } else { + HealthStatus.UNKNOWN + } +} + +class ReadinessHealthIndicator(initialStatus: HealthStatus = HealthStatus.UNKNOWN) : + DefaultHealthIndicator(initialStatus) + +class LivenessHealthIndicator(initialStatus: HealthStatus = HealthStatus.HEALTHY) : + DefaultHealthIndicator(initialStatus) \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/repository/HealthIndicatorRepository.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/repository/HealthIndicatorRepository.kt new file mode 100644 index 00000000..2c5540c6 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/repository/HealthIndicatorRepository.kt @@ -0,0 +1,28 @@ +package no.nav.paw.health.repository + +import no.nav.paw.health.model.HealthIndicator +import no.nav.paw.health.model.HealthIndicatorList + +class HealthIndicatorRepository { + + private val readinessIndicators: HealthIndicatorList = mutableListOf() + private val livenessIndicators: HealthIndicatorList = mutableListOf() + + fun addReadinessIndicator(healthIndicator: T): T { + readinessIndicators.add(healthIndicator) + return healthIndicator + } + + fun addLivenessIndicator(healthIndicator: T): T { + livenessIndicators.add(healthIndicator) + return healthIndicator + } + + fun getReadinessIndicators(): HealthIndicatorList { + return readinessIndicators + } + + fun getLivenessIndicators(): HealthIndicatorList { + return livenessIndicators + } +} \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/route/HealthRoutes.kt b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/route/HealthRoutes.kt new file mode 100644 index 00000000..12ce62e6 --- /dev/null +++ b/lib/error-handling-ktor3/src/main/kotlin/no/nav/paw/health/route/HealthRoutes.kt @@ -0,0 +1,45 @@ +package no.nav.paw.health.route + +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import no.nav.paw.health.model.HealthStatus +import no.nav.paw.health.model.getAggregatedStatus +import no.nav.paw.health.repository.HealthIndicatorRepository + +fun Route.healthRoutes( + healthIndicatorRepository: HealthIndicatorRepository, +) { + + get("/internal/isAlive") { + val livenessIndicators = healthIndicatorRepository.getLivenessIndicators() + when (val status = livenessIndicators.getAggregatedStatus()) { + HealthStatus.HEALTHY -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.OK + ) { status.value } + + else -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.ServiceUnavailable + ) { status.value } + } + } + + get("/internal/isReady") { + val readinessIndicators = healthIndicatorRepository.getReadinessIndicators() + when (val status = readinessIndicators.getAggregatedStatus()) { + HealthStatus.HEALTHY -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.OK + ) { status.value } + + else -> call.respondText( + ContentType.Text.Plain, + HttpStatusCode.ServiceUnavailable + ) { status.value } + } + } +} diff --git a/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt new file mode 100644 index 00000000..0ea10119 --- /dev/null +++ b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt @@ -0,0 +1,67 @@ +package no.nav.paw.error.handler + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.kotlinModule +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.ApplicationCall +import io.ktor.server.plugins.BadRequestException +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.routing.IgnoreTrailingSlash +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import no.nav.paw.error.model.ErrorType +import no.nav.paw.error.model.ProblemDetails +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import io.ktor.server.application.install as serverInstall +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +class HttpExceptionHandlerTest : FreeSpec({ + "Skal håndtere exceptions og returnere ProblemDetails response" { + testApplication { + application { + serverInstall(IgnoreTrailingSlash) + serverInstall(StatusPages) { + exception { call: ApplicationCall, cause: Throwable -> + call.handleException(cause) + } + } + serverInstall(ServerContentNegotiation) { + jackson { + registerModule(JavaTimeModule()) + kotlinModule() + } + } + routing { + get("/api/400") { + throw BadRequestException("It's bad") + } + } + } + + val client = createClient { + install(ClientContentNegotiation) { + jackson { + registerModule(JavaTimeModule()) + kotlinModule() + } + } + } + + val response400 = client.get("/api/400") + val responseBody400 = response400.body() + response400.status shouldBe HttpStatusCode.BadRequest + responseBody400.type shouldBe ErrorType + .domain("http") + .error("kunne-ikke-tolke-forespoersel") + .build() + responseBody400.status shouldBe HttpStatusCode.BadRequest + responseBody400.title shouldBe HttpStatusCode.BadRequest.description + } + } +}) \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/listener/KafkaStreamsStatusListenerTest.kt b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/listener/KafkaStreamsStatusListenerTest.kt new file mode 100644 index 00000000..3330af75 --- /dev/null +++ b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/listener/KafkaStreamsStatusListenerTest.kt @@ -0,0 +1,60 @@ +package no.nav.paw.health.listener + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import no.nav.paw.health.model.HealthStatus +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator +import no.nav.paw.health.model.getAggregatedStatus +import no.nav.paw.health.repository.HealthIndicatorRepository +import org.apache.kafka.streams.KafkaStreams + +class KafkaStreamsStatusListenerTest : FreeSpec({ + + "Kafka Streams Status Listener skal returnere korrekt helsesjekk-status" { + val healthIndicatorRepository = HealthIndicatorRepository() + + val liveness = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + val readiness = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + + val listener = createHealthIndicatorStateListener(liveness, readiness) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNKNOWN + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + + listener.onChange(KafkaStreams.State.CREATED, KafkaStreams.State.CREATED) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + + listener.onChange(KafkaStreams.State.RUNNING, KafkaStreams.State.RUNNING) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + + listener.onChange(KafkaStreams.State.REBALANCING, KafkaStreams.State.REBALANCING) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + + listener.onChange(KafkaStreams.State.PENDING_ERROR, KafkaStreams.State.PENDING_ERROR) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + + listener.onChange(KafkaStreams.State.ERROR, KafkaStreams.State.ERROR) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + + listener.onChange(KafkaStreams.State.PENDING_SHUTDOWN, KafkaStreams.State.PENDING_SHUTDOWN) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + + listener.onChange(KafkaStreams.State.NOT_RUNNING, KafkaStreams.State.NOT_RUNNING) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + } +}) \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/repository/HealthIndicatorRepositoryTest.kt b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/repository/HealthIndicatorRepositoryTest.kt new file mode 100644 index 00000000..c98219e1 --- /dev/null +++ b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/repository/HealthIndicatorRepositoryTest.kt @@ -0,0 +1,58 @@ +package no.nav.paw.health.repository + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import no.nav.paw.health.model.HealthStatus +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator +import no.nav.paw.health.model.getAggregatedStatus + +class HealthIndicatorRepositoryTest : FreeSpec({ + + "Skal returnere korrekt helsesjekk-status" { + val healthIndicatorRepository = HealthIndicatorRepository() + + val liveness1 = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + val liveness2 = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + val liveness3 = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + + val readiness1 = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + val readiness2 = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + val readiness3 = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNKNOWN + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + + readiness1.setUnhealthy() + liveness1.setUnhealthy() + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + + readiness2.setUnhealthy() + readiness3.setUnhealthy() + liveness2.setUnhealthy() + liveness3.setUnhealthy() + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + + readiness1.setHealthy() + liveness1.setHealthy() + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + + readiness2.setHealthy() + liveness2.setHealthy() + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.UNHEALTHY + + readiness3.setHealthy() + liveness3.setHealthy() + + healthIndicatorRepository.getReadinessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + healthIndicatorRepository.getLivenessIndicators().getAggregatedStatus() shouldBe HealthStatus.HEALTHY + } +}) \ No newline at end of file diff --git a/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/route/HealthRoutesTest.kt b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/route/HealthRoutesTest.kt new file mode 100644 index 00000000..b52ee494 --- /dev/null +++ b/lib/error-handling-ktor3/src/test/kotlin/no/nav/paw/health/route/HealthRoutesTest.kt @@ -0,0 +1,127 @@ +package no.nav.paw.health.route + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.jackson.jackson +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.routing.IgnoreTrailingSlash +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import no.nav.paw.health.model.HealthStatus +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator +import no.nav.paw.health.repository.HealthIndicatorRepository +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import io.ktor.server.application.install as serverInstall +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +class HealthRoutesTest : FreeSpec({ + + "Endepunkter for helsesjekk skal returnere korrekt helsesjekk-status" { + + val healthIndicatorRepository = HealthIndicatorRepository() + + testApplication { + application { + serverInstall(IgnoreTrailingSlash) + serverInstall(StatusPages) + serverInstall(ServerContentNegotiation) { + jackson {} + } + routing { + healthRoutes(healthIndicatorRepository) + } + } + + val client = createClient { + install(ClientContentNegotiation) { + jackson {} + } + } + + val livenessResponse1 = client.get("/internal/isAlive") + val readinessResponse1 = client.get("/internal/isReady") + + livenessResponse1.status shouldBe HttpStatusCode.OK + livenessResponse1.body() shouldBe HealthStatus.HEALTHY.value + readinessResponse1.status shouldBe HttpStatusCode.OK + readinessResponse1.body() shouldBe HealthStatus.HEALTHY.value + + val liveness1 = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + val liveness2 = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + val liveness3 = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) + + val readiness1 = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + val readiness2 = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + val readiness3 = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) + + val livenessResponse2 = client.get("/internal/isAlive") + val readinessResponse2 = client.get("/internal/isReady") + + livenessResponse2.status shouldBe HttpStatusCode.OK + livenessResponse2.body() shouldBe HealthStatus.HEALTHY.value + readinessResponse2.status shouldBe HttpStatusCode.ServiceUnavailable + readinessResponse2.body() shouldBe HealthStatus.UNKNOWN.value + + readiness1.setUnhealthy() + liveness1.setUnhealthy() + + val livenessResponse3 = client.get("/internal/isAlive") + val readinessResponse3 = client.get("/internal/isReady") + + livenessResponse3.status shouldBe HttpStatusCode.ServiceUnavailable + livenessResponse3.body() shouldBe HealthStatus.UNHEALTHY.value + readinessResponse3.status shouldBe HttpStatusCode.ServiceUnavailable + readinessResponse3.body() shouldBe HealthStatus.UNHEALTHY.value + + readiness2.setUnhealthy() + readiness3.setUnhealthy() + liveness2.setUnhealthy() + liveness3.setUnhealthy() + + val livenessResponse4 = client.get("/internal/isAlive") + val readinessResponse4 = client.get("/internal/isReady") + + livenessResponse4.status shouldBe HttpStatusCode.ServiceUnavailable + livenessResponse4.body() shouldBe HealthStatus.UNHEALTHY.value + readinessResponse4.status shouldBe HttpStatusCode.ServiceUnavailable + readinessResponse4.body() shouldBe HealthStatus.UNHEALTHY.value + + readiness1.setHealthy() + liveness1.setHealthy() + + val livenessResponse5 = client.get("/internal/isAlive") + val readinessResponse5 = client.get("/internal/isReady") + + livenessResponse5.status shouldBe HttpStatusCode.ServiceUnavailable + livenessResponse5.body() shouldBe HealthStatus.UNHEALTHY.value + readinessResponse5.status shouldBe HttpStatusCode.ServiceUnavailable + readinessResponse5.body() shouldBe HealthStatus.UNHEALTHY.value + + readiness2.setHealthy() + liveness2.setHealthy() + + val livenessResponse6 = client.get("/internal/isAlive") + val readinessResponse6 = client.get("/internal/isReady") + + livenessResponse6.status shouldBe HttpStatusCode.ServiceUnavailable + livenessResponse6.body() shouldBe HealthStatus.UNHEALTHY.value + readinessResponse6.status shouldBe HttpStatusCode.ServiceUnavailable + readinessResponse6.body() shouldBe HealthStatus.UNHEALTHY.value + + readiness3.setHealthy() + liveness3.setHealthy() + + val livenessResponse7 = client.get("/internal/isAlive") + val readinessResponse7 = client.get("/internal/isReady") + + livenessResponse7.status shouldBe HttpStatusCode.OK + livenessResponse7.body() shouldBe HealthStatus.HEALTHY.value + readinessResponse7.status shouldBe HttpStatusCode.OK + readinessResponse7.body() shouldBe HealthStatus.HEALTHY.value + } + } +}) \ No newline at end of file diff --git a/lib/kafka-key-generator-client-ktor3/build.gradle.kts b/lib/kafka-key-generator-client-ktor3/build.gradle.kts new file mode 100644 index 00000000..2ca79956 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + kotlin("jvm") +} +val jvmMajorVersion: String by project +val jvmVersion = JavaVersion.valueOf("VERSION_$jvmMajorVersion") + +dependencies { + implementation(project(":lib:hoplite-config")) + implementation(libs.jackson.datatypeJsr310) + implementation(libs.jackson.kotlin) + implementation(libs.ktor3.client.contentNegotiation) + implementation(libs.ktor3.client.core) + implementation(libs.ktor3.client.cio) + implementation(libs.ktor3.serialization.jackson) + implementation(libs.nav.security.tokenClientCore) + api(libs.nav.common.tokenClient) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(jvmVersion.majorVersion) + } +} diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt new file mode 100644 index 00000000..3f2f4e80 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureAdMachineToMachineTokenClientFactory.kt @@ -0,0 +1,39 @@ +package no.nav.paw.kafkakeygenerator.auth + +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.RSAKey +import no.nav.common.token_client.builder.AzureAdTokenClientBuilder +import no.nav.common.token_client.cache.CaffeineTokenCache +import no.nav.common.token_client.client.AzureAdMachineToMachineTokenClient +import no.nav.paw.config.env.Local +import no.nav.paw.config.env.RuntimeEnvironment +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +fun azureAdM2MTokenClient(runtimeEnvironment: RuntimeEnvironment, azureProviderConfig: AzureM2MConfig): AzureAdMachineToMachineTokenClient = + when (runtimeEnvironment) { + is Local -> AzureAdTokenClientBuilder.builder() + .withClientId(azureProviderConfig.clientId) + .withPrivateJwk(createMockRSAKey("azure")) + .withTokenEndpointUrl(azureProviderConfig.tokenEndpointUrl) + .buildMachineToMachineTokenClient() + + else -> AzureAdTokenClientBuilder.builder() + .withNaisDefaults() + .withCache(CaffeineTokenCache()) + .buildMachineToMachineTokenClient() + } + +fun createMockRSAKey(keyID: String): String? = KeyPairGenerator + .getInstance("RSA").let { + it.initialize(2048) + it.generateKeyPair() + }.let { + RSAKey.Builder(it.public as RSAPublicKey) + .privateKey(it.private as RSAPrivateKey) + .keyUse(KeyUse.SIGNATURE) + .keyID(keyID) + .build() + .toJSONString() + } \ No newline at end of file diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureM2MConfig.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureM2MConfig.kt new file mode 100644 index 00000000..3f2a6cf2 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/auth/AzureM2MConfig.kt @@ -0,0 +1,8 @@ +package no.nav.paw.kafkakeygenerator.auth + +const val AZURE_M2M_CONFIG = "azure_m2m.toml" + +data class AzureM2MConfig( + val tokenEndpointUrl: String, + val clientId: String +) \ No newline at end of file diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Alias.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Alias.kt new file mode 100644 index 00000000..bd8d6761 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Alias.kt @@ -0,0 +1,23 @@ +package no.nav.paw.kafkakeygenerator.client + + +data class AliasRequest( + val antallPartisjoner: Int, + val identer: List +) + +data class AliasResponse( + val alias: List +) + +data class LokaleAlias( + val identitetsnummer: String, + val koblinger: List +) + +data class Alias ( + val identitetsnummer: String, + val arbeidsoekerId: Long, + val recordKey: Long, + val partition: Int, +) diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt new file mode 100644 index 00000000..7c0edc13 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Factory.kt @@ -0,0 +1,41 @@ +package no.nav.paw.kafkakeygenerator.client + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.jackson.jackson +import no.nav.common.token_client.client.AzureAdMachineToMachineTokenClient +import no.nav.paw.config.env.currentRuntimeEnvironment +import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration +import no.nav.paw.kafkakeygenerator.auth.AZURE_M2M_CONFIG +import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig +import no.nav.paw.kafkakeygenerator.auth.azureAdM2MTokenClient + +fun createKafkaKeyGeneratorClient(m2mTokenClient: AzureAdMachineToMachineTokenClient? = null): KafkaKeysClient { + val kafkaKeyConfig = loadNaisOrLocalConfiguration(KAFKA_KEY_GENERATOR_CLIENT_CONFIG) + val m2mTC = m2mTokenClient ?: azureAdM2MTokenClient( + currentRuntimeEnvironment, + loadNaisOrLocalConfiguration(AZURE_M2M_CONFIG) + ) + return kafkaKeysClient(kafkaKeyConfig) { + m2mTC.createMachineToMachineToken(kafkaKeyConfig.scope) + } +} + +fun kafkaKeysClient(konfigurasjon: KafkaKeyConfig, m2mTokenFactory: () -> String): KafkaKeysClient = + when (konfigurasjon.url) { + "MOCK" -> inMemoryKafkaKeysMock() + else -> kafkaKeysMedHttpClient(konfigurasjon, m2mTokenFactory) + } + +private fun kafkaKeysMedHttpClient(config: KafkaKeyConfig, m2mTokenFactory: () -> String): KafkaKeysClient { + val httpClient = HttpClient { + install(ContentNegotiation) { + jackson() + } + } + return StandardKafkaKeysClient( + httpClient = httpClient, + kafkaKeysUrl = config.url, + kafkaKeysLokalInfoUrl = config.urlLokalInfo + ) { m2mTokenFactory() } +} diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/KafkaKeyConfig.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/KafkaKeyConfig.kt new file mode 100644 index 00000000..8d69fa96 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/KafkaKeyConfig.kt @@ -0,0 +1,9 @@ +package no.nav.paw.kafkakeygenerator.client + +const val KAFKA_KEY_GENERATOR_CLIENT_CONFIG = "kafka_key_generator_client_config.toml" + +data class KafkaKeyConfig( + val url: String, + val urlLokalInfo: String, + val scope: String +) \ No newline at end of file diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/KafkaKeysClient.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/KafkaKeysClient.kt new file mode 100644 index 00000000..3191fa7c --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/KafkaKeysClient.kt @@ -0,0 +1,70 @@ +package no.nav.paw.kafkakeygenerator.client + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import java.lang.IllegalStateException + +data class KafkaKeysResponse( + val id: Long, + val key: Long +) + +data class KafkaKeysRequest( + val ident: String +) + +interface KafkaKeysClient { + suspend fun getIdAndKeyOrNull(identitetsnummer: String): KafkaKeysResponse? + suspend fun getIdAndKey(identitetsnummer: String): KafkaKeysResponse = + getIdAndKeyOrNull(identitetsnummer) ?: throw IllegalStateException("Kafka-key-client: Uventet feil mot server: http-status=404") + + suspend fun getAlias(antallPartisjoner: Int, identitetsnummer: List): AliasResponse +} + +class StandardKafkaKeysClient( + private val httpClient: HttpClient, + private val kafkaKeysUrl: String, + private val kafkaKeysLokalInfoUrl: String, + private val getAccessToken: () -> String +) : KafkaKeysClient { + override suspend fun getIdAndKeyOrNull(identitetsnummer: String): KafkaKeysResponse? = + httpClient.post(kafkaKeysUrl) { + header("Authorization", "Bearer ${getAccessToken()}") + contentType(ContentType.Application.Json) + setBody(KafkaKeysRequest(identitetsnummer)) + }.let { response -> + when (response.status) { + io.ktor.http.HttpStatusCode.OK -> { + response.body() + } + + io.ktor.http.HttpStatusCode.NotFound -> null + else -> { + throw Exception("Kunne ikke hente kafka key, http_status=${response.status}, melding=${response.body()}") + } + } + } + + override suspend fun getAlias(antallPartisjoner: Int, identitetsnummer: List): AliasResponse { + return httpClient.post(kafkaKeysLokalInfoUrl) { + header("Authorization", "Bearer ${getAccessToken()}") + contentType(ContentType.Application.Json) + setBody(AliasRequest(antallPartisjoner, identitetsnummer)) + }.let { response -> + when (response.status) { + io.ktor.http.HttpStatusCode.OK -> { + response.body() + } + + else -> { + throw Exception("Kunne ikke hente alias, http_status=${response.status}}") + } + } + } + } +} diff --git a/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Mock.kt b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Mock.kt new file mode 100644 index 00000000..eb6e69df --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/kotlin/no/nav/paw/kafkakeygenerator/client/Mock.kt @@ -0,0 +1,39 @@ +package no.nav.paw.kafkakeygenerator.client + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.atomic.AtomicLong + +fun inMemoryKafkaKeysMock(): KafkaKeysClient { + val naisClusterName = System.getenv("NAIS_CLUSTER_NAME") + if (naisClusterName != null) { + throw IllegalStateException("Kan ikke bruke inMemoryKafkaKeysMock i $naisClusterName") + } + val sekvens = AtomicLong(0) + val map: ConcurrentMap = ConcurrentHashMap() + return object: KafkaKeysClient { + override suspend fun getIdAndKeyOrNull(identitetsnummer: String): KafkaKeysResponse { + val id = map.computeIfAbsent(identitetsnummer) { sekvens.incrementAndGet() } + return KafkaKeysResponse(id, id % 2) + } + + override suspend fun getAlias(antallPartisjoner: Int, identitetsnummer: List): AliasResponse = + identitetsnummer + .mapNotNull { id -> + map[id]?.let { id to it } + }.map { (ident, arbeidssoekerId) -> + ident to map + .filterValues { it == arbeidssoekerId } + .map { (id, key) -> + Alias( + identitetsnummer = id, + arbeidsoekerId = key, + recordKey = key % 2, + partition = (key % 2).toInt() % antallPartisjoner + ) + } + }.map { (ident, aliases) -> + LokaleAlias(ident, aliases) + }.let { AliasResponse(it) } + } +} diff --git a/lib/kafka-key-generator-client-ktor3/src/main/resources/local/kafka_key_generator_client_config.toml b/lib/kafka-key-generator-client-ktor3/src/main/resources/local/kafka_key_generator_client_config.toml new file mode 100644 index 00000000..20ba6a69 --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/resources/local/kafka_key_generator_client_config.toml @@ -0,0 +1,3 @@ +url = "MOCK" +urlLokalInfo = "MOCK" +scope = "api://../.default" \ No newline at end of file diff --git a/lib/kafka-key-generator-client-ktor3/src/main/resources/nais/azure_m2m.toml b/lib/kafka-key-generator-client-ktor3/src/main/resources/nais/azure_m2m.toml new file mode 100644 index 00000000..ffe2977e --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/resources/nais/azure_m2m.toml @@ -0,0 +1,2 @@ +tokenEndpointUrl = "${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT}" +clientId = "${AZURE_APP_CLIENT_ID}" diff --git a/lib/kafka-key-generator-client-ktor3/src/main/resources/nais/kafka_key_generator_client_config.toml b/lib/kafka-key-generator-client-ktor3/src/main/resources/nais/kafka_key_generator_client_config.toml new file mode 100644 index 00000000..1f8503ed --- /dev/null +++ b/lib/kafka-key-generator-client-ktor3/src/main/resources/nais/kafka_key_generator_client_config.toml @@ -0,0 +1,3 @@ +url = "http://paw-kafka-key-generator/api/v2/hentEllerOpprett" +urlLokalInfo = "http://paw-kafka-key-generator/api/v2/lokalInfo" +scope = "${KAFKA_KEYS_SCOPE}" diff --git a/lib/security/build.gradle.kts b/lib/security/build.gradle.kts index 02f9a88b..adadee4c 100644 --- a/lib/security/build.gradle.kts +++ b/lib/security/build.gradle.kts @@ -3,19 +3,19 @@ plugins { } dependencies { - implementation(project(":lib:error-handling")) - implementation(libs.ktor.server.auth) + implementation(project(":lib:error-handling-ktor3")) + implementation(libs.ktor3.server.auth) implementation(libs.logbackClassic) - implementation(libs.nav.security.tokenValidationKtorV2) + implementation(libs.nav.security.tokenValidationKtorV3) //Test testImplementation(project(":lib:pdl-client")) testImplementation(libs.nav.poao.tilgangClient) testImplementation(libs.nav.security.mockOauth2Server) testImplementation(libs.bundles.testLibsWithUnitTesting) - testImplementation(libs.ktor.server.testJvm) - testImplementation(libs.ktor.client.contentNegotiation) - testImplementation(libs.ktor.serialization.jackson) + testImplementation(libs.ktor3.server.test.host) + testImplementation(libs.ktor3.client.contentNegotiation) + testImplementation(libs.ktor3.serialization.jackson) testImplementation(libs.jackson.datatypeJsr310) } diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt index 2a537822..0714b73d 100644 --- a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt @@ -1,12 +1,10 @@ package no.nav.paw.security.authorization.context import io.ktor.http.HttpHeaders -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call import io.ktor.server.auth.principal import io.ktor.server.request.ApplicationRequest -import io.ktor.util.pipeline.PipelineContext -import no.nav.security.token.support.v2.TokenValidationContextPrincipal +import io.ktor.server.routing.RoutingContext +import no.nav.security.token.support.v3.TokenValidationContextPrincipal sealed class NavHeader(val name: String) @@ -29,7 +27,7 @@ data class RequestContext( val principal: TokenValidationContextPrincipal? ) -fun PipelineContext.resolveRequestContext(): RequestContext { +fun RoutingContext.resolveRequestContext(): RequestContext { return RequestContext( request = call.request, headers = resolveRequestHeaders(), @@ -37,7 +35,7 @@ fun PipelineContext.resolveRequestContext(): RequestConte ) } -fun PipelineContext.resolveRequestHeaders(): RequestHeaders { +fun RoutingContext.resolveRequestHeaders(): RequestHeaders { return RequestHeaders( authorization = call.request.headers[HttpHeaders.Authorization], traceParent = call.request.headers[TraceParent.name], diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt index f23023d0..3c319500 100644 --- a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt @@ -1,7 +1,6 @@ package no.nav.paw.security.authorization.interceptor -import io.ktor.server.application.ApplicationCall -import io.ktor.util.pipeline.PipelineContext +import io.ktor.server.routing.RoutingContext import no.nav.paw.security.authorization.context.AuthorizationContext import no.nav.paw.security.authorization.context.resolveRequestContext import no.nav.paw.security.authorization.context.resolveSecurityContext @@ -11,11 +10,11 @@ import org.slf4j.LoggerFactory private val logger = LoggerFactory.getLogger("no.nav.paw.logger.security.authorization") -suspend fun PipelineContext.authorize( +suspend fun RoutingContext.authorize( action: Action, accessPolicies: List = emptyList(), - body: suspend PipelineContext.(AuthorizationContext) -> Unit -): PipelineContext { + body: suspend RoutingContext.(AuthorizationContext) -> Unit +): RoutingContext { logger.debug("Kjører autorisasjon") val requestContext = resolveRequestContext() val securityContext = requestContext.resolveSecurityContext() diff --git a/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt b/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt index 38aa3234..79e087d4 100644 --- a/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt +++ b/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.module.kotlin.registerKotlinModule import io.ktor.client.HttpClient import io.ktor.serialization.jackson.jackson import io.ktor.server.application.Application -import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.auth.authenticate import io.ktor.server.auth.authentication @@ -23,10 +22,10 @@ import no.nav.paw.security.authorization.interceptor.authorize import no.nav.paw.security.authorization.model.Action import no.nav.paw.security.authorization.policy.AccessPolicy import no.nav.security.mock.oauth2.MockOAuth2Server -import no.nav.security.token.support.v2.IssuerConfig -import no.nav.security.token.support.v2.RequiredClaims -import no.nav.security.token.support.v2.TokenSupportConfig -import no.nav.security.token.support.v2.tokenValidationSupport +import no.nav.security.token.support.v3.IssuerConfig +import no.nav.security.token.support.v3.RequiredClaims +import no.nav.security.token.support.v3.TokenSupportConfig +import no.nav.security.token.support.v3.tokenValidationSupport import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation diff --git a/settings.gradle.kts b/settings.gradle.kts index f9f0e77f..13d1a473 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,12 +13,14 @@ rootProject.name = "paw-arbeidssoekerregisteret-monorepo-intern" include( "lib:hoplite-config", "lib:error-handling", + "lib:error-handling-ktor3", "lib:security", "lib:http-client-utils", "lib:http-client-utils-ktorv3", "lib:kafka", "lib:kafka-streams", "lib:kafka-key-generator-client", + "lib:kafka-key-generator-client-ktor3", "lib:pdl-client", "lib:aareg-client", "lib:tilgangskontroll-client",