From 2a714fccf6d21a06191337267faec6747056b5f1 Mon Sep 17 00:00:00 2001 From: Nils Martin Sande Date: Tue, 10 Sep 2024 13:30:16 +0200 Subject: [PATCH] Flyttet bekreftelse-api til intern --- apps/bekreftelse-api/nais/nais-dev.yaml | 46 ++++++ apps/bekreftelse-api/nais/nais-prod.yaml | 46 ++++++ .../nav/paw/rapportering/api/Application.kt | 78 +++++++++++ .../paw/rapportering/api/ApplicationInfo.kt | 5 + .../nav/paw/rapportering/api/Dependencies.kt | 131 ++++++++++++++++++ .../no/nav/paw/rapportering/api/Health.kt | 44 ++++++ .../api/config/ApplicationConfig.kt | 39 ++++++ .../api/domain/request/RapporteringRequest.kt | 11 ++ .../TilgjengeligRapporteringerRequest.kt | 5 + .../TilgjengeligeRapporteringerResponse.kt | 23 +++ .../kafka/RapporteringHendelseProcessor.kt | 61 ++++++++ .../api/kafka/RapporteringProducer.kt | 71 ++++++++++ .../RapporteringTilgjengeligStateSerde.kt | 28 ++++ .../paw/rapportering/api/kafka/Topology.kt | 19 +++ .../api/plugins/Authentication.kt | 30 ++++ .../nav/paw/rapportering/api/plugins/HTTP.kt | 26 ++++ .../paw/rapportering/api/plugins/Logging.kt | 13 ++ .../paw/rapportering/api/plugins/Metrics.kt | 32 +++++ .../nav/paw/rapportering/api/plugins/Otel.kt | 18 +++ .../rapportering/api/plugins/Serialization.kt | 21 +++ .../rapportering/api/routes/HealthRoutes.kt | 22 +++ .../api/routes/RapporteringRoutes.kt | 105 ++++++++++++++ .../rapportering/api/routes/RequestScope.kt | 48 +++++++ .../rapportering/api/routes/SwaggerRoutes.kt | 8 ++ .../api/services/AutorisasjonService.kt | 37 +++++ .../nav/paw/rapportering/api/utils/Claims.kt | 45 ++++++ .../nav/paw/rapportering/api/utils/Logger.kt | 32 +++++ .../rapportering/api/utils/ObjectMapper.kt | 23 +++ .../resources/local/application_config.toml | 30 ++++ .../resources/local/azure_m2m_key_config.toml | 2 + .../local/kafka_configuration_schemareg.toml | 3 + .../kafka_key_generator_client_config.toml | 2 + ...kafka_streams_configuration_schemareg.toml | 4 + .../src/main/resources/logback.xml | 44 ++++++ .../resources/nais/application_config.toml | 33 +++++ .../resources/nais/azure_m2m_key_config.toml | 2 + .../kafka_key_generator_client_config.toml | 2 + .../resources/openapi/rapporteringapi.yaml | 0 .../resources/rapporteringTilgjengelig.json | 10 ++ settings.gradle.kts | 3 +- 40 files changed, 1201 insertions(+), 1 deletion(-) create mode 100644 apps/bekreftelse-api/nais/nais-dev.yaml create mode 100644 apps/bekreftelse-api/nais/nais-prod.yaml create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Application.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/ApplicationInfo.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Dependencies.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Health.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/config/ApplicationConfig.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/RapporteringRequest.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/TilgjengeligRapporteringerRequest.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/response/TilgjengeligeRapporteringerResponse.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringHendelseProcessor.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringProducer.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringTilgjengeligStateSerde.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/Topology.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Authentication.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/HTTP.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Logging.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Metrics.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Otel.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Serialization.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/HealthRoutes.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RapporteringRoutes.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RequestScope.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/SwaggerRoutes.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/services/AutorisasjonService.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Claims.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Logger.kt create mode 100644 apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/ObjectMapper.kt create mode 100644 apps/bekreftelse-api/src/main/resources/local/application_config.toml create mode 100644 apps/bekreftelse-api/src/main/resources/local/azure_m2m_key_config.toml create mode 100644 apps/bekreftelse-api/src/main/resources/local/kafka_configuration_schemareg.toml create mode 100644 apps/bekreftelse-api/src/main/resources/local/kafka_key_generator_client_config.toml create mode 100644 apps/bekreftelse-api/src/main/resources/local/kafka_streams_configuration_schemareg.toml create mode 100644 apps/bekreftelse-api/src/main/resources/logback.xml create mode 100644 apps/bekreftelse-api/src/main/resources/nais/application_config.toml create mode 100644 apps/bekreftelse-api/src/main/resources/nais/azure_m2m_key_config.toml create mode 100644 apps/bekreftelse-api/src/main/resources/nais/kafka_key_generator_client_config.toml create mode 100644 apps/bekreftelse-api/src/main/resources/openapi/rapporteringapi.yaml create mode 100644 apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json diff --git a/apps/bekreftelse-api/nais/nais-dev.yaml b/apps/bekreftelse-api/nais/nais-dev.yaml new file mode 100644 index 00000000..5d7afb19 --- /dev/null +++ b/apps/bekreftelse-api/nais/nais-dev.yaml @@ -0,0 +1,46 @@ +apiVersion: nais.io/v1alpha1 +kind: Application +metadata: + name: paw-rapportering-api + namespace: paw + labels: + team: paw +spec: + image: {{ image }} + port: 8080 + env: + - name: KAFKA_KEY_SCOPE + value: "api://dev-gcp.paw.paw-kafka-key-generator/.default" + resources: + limits: + memory: 1024Mi + requests: + cpu: 200m + memory: 256Mi + tokenx: + enabled: true + azure: + application: + enabled: true + allowAllUsers: true + claims: + extra: + - NAVident + replicas: + min: 1 + max: 1 + liveness: + path: /internal/isAlive + initialDelay: 10 + readiness: + path: /internal/isReady + initialDelay: 10 + prometheus: + enabled: true + path: /internal/metrics + observability: + autoInstrumentation: + enabled: true + runtime: java + kafka: + pool: nav-dev diff --git a/apps/bekreftelse-api/nais/nais-prod.yaml b/apps/bekreftelse-api/nais/nais-prod.yaml new file mode 100644 index 00000000..a17d0c11 --- /dev/null +++ b/apps/bekreftelse-api/nais/nais-prod.yaml @@ -0,0 +1,46 @@ +apiVersion: nais.io/v1alpha1 +kind: Application +metadata: + name: paw-rapportering-api + namespace: paw + labels: + team: paw +spec: + image: {{ image }} + port: 8080 + env: + - name: KAFKA_KEY_SCOPE + value: "api://prod-gcp.paw.paw-kafka-key-generator/.default" + resources: + limits: + memory: 1024Mi + requests: + cpu: 200m + memory: 256Mi + tokenx: + enabled: true + azure: + application: + enabled: true + allowAllUsers: true + claims: + extra: + - NAVident + replicas: + min: 1 + max: 1 + liveness: + path: /internal/isAlive + initialDelay: 10 + readiness: + path: /internal/isReady + initialDelay: 10 + prometheus: + enabled: true + path: /internal/metrics + observability: + autoInstrumentation: + enabled: true + runtime: java + kafka: + pool: nav-prod diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Application.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Application.kt new file mode 100644 index 00000000..487e4ddc --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Application.kt @@ -0,0 +1,78 @@ +package no.nav.paw.rapportering.api + +import io.ktor.server.application.Application +import io.ktor.server.engine.addShutdownHook +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.routing.routing +import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration +import no.nav.paw.config.kafka.KAFKA_CONFIG_WITH_SCHEME_REG +import no.nav.paw.config.kafka.KAFKA_STREAMS_CONFIG_WITH_SCHEME_REG +import no.nav.paw.config.kafka.KafkaConfig +import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig +import no.nav.paw.kafkakeygenerator.client.KafkaKeyConfig +import no.nav.paw.rapportering.api.config.APPLICATION_CONFIG_FILE_NAME +import no.nav.paw.rapportering.api.config.ApplicationConfig +import no.nav.paw.rapportering.api.plugins.configureAuthentication +import no.nav.paw.rapportering.api.plugins.configureHTTP +import no.nav.paw.rapportering.api.plugins.configureLogging +import no.nav.paw.rapportering.api.plugins.configureMetrics +import no.nav.paw.rapportering.api.plugins.configureOtel +import no.nav.paw.rapportering.api.plugins.configureSerialization +import no.nav.paw.rapportering.api.routes.healthRoutes +import no.nav.paw.rapportering.api.routes.rapporteringRoutes +import no.nav.paw.rapportering.api.routes.swaggerRoutes +import org.slf4j.LoggerFactory + +fun main() { + val logger = LoggerFactory.getLogger("rapportering-api") + logger.info("Starter: ${ApplicationInfo.id}") + + val applicationConfig = loadNaisOrLocalConfiguration(APPLICATION_CONFIG_FILE_NAME) + val kafkaConfig = loadNaisOrLocalConfiguration(KAFKA_CONFIG_WITH_SCHEME_REG) + val kafkaStreamsConfig = loadNaisOrLocalConfiguration(KAFKA_STREAMS_CONFIG_WITH_SCHEME_REG) + val azureM2MConfig = loadNaisOrLocalConfiguration("azure_m2m_key_config.toml") + val kafkaKeyConfig = loadNaisOrLocalConfiguration("kafka_key_generator_client_config.toml") + + val dependencies = createDependencies( + applicationConfig, + kafkaConfig, + kafkaStreamsConfig, + azureM2MConfig, + kafkaKeyConfig + ) + + embeddedServer(Netty, port = 8080) { + module(applicationConfig, dependencies) + }.apply { + addShutdownHook { stop(300, 300) } + start(wait = true) + } +} + +fun Application.module( + applicationConfig: ApplicationConfig, + dependencies: Dependencies +) { + configureMetrics(dependencies.prometheusMeterRegistry) + configureHTTP() + configureAuthentication(applicationConfig.authProviders) + configureLogging() + configureSerialization() + configureOtel() + + routing { + healthRoutes(dependencies.prometheusMeterRegistry, dependencies.health) + swaggerRoutes() + rapporteringRoutes( + kafkaKeyClient = dependencies.kafkaKeyClient, + rapporteringStateStore = dependencies.rapporteringStateStore, + rapporteringStateStoreName = applicationConfig.rapporteringStateStoreName, + kafkaStreams = dependencies.kafkaStreams, + httpClient = dependencies.httpClient, + rapporteringProducer = dependencies.rapporteringProducer, + autorisasjonService = dependencies.autorisasjonService + ) + } +} + diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/ApplicationInfo.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/ApplicationInfo.kt new file mode 100644 index 00000000..8caca816 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/ApplicationInfo.kt @@ -0,0 +1,5 @@ +package no.nav.paw.rapportering.api + +object ApplicationInfo { + val id = System.getenv("IMAGE_WITH_VERSION") ?: "UNSPECIFIED" +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Dependencies.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Dependencies.kt new file mode 100644 index 00000000..f90dfe61 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Dependencies.kt @@ -0,0 +1,131 @@ +package no.nav.paw.rapportering.api + +import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.jackson.jackson +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import io.micrometer.prometheusmetrics.PrometheusConfig +import no.nav.paw.config.kafka.KafkaConfig +import no.nav.paw.config.kafka.streams.KafkaStreamsFactory +import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig +import no.nav.paw.kafkakeygenerator.auth.azureAdM2MTokenClient +import no.nav.paw.kafkakeygenerator.client.KafkaKeyConfig +import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient +import no.nav.paw.kafkakeygenerator.client.kafkaKeysKlient +import no.nav.paw.rapportering.api.config.ApplicationConfig +import no.nav.paw.rapportering.api.kafka.RapporteringProducer +import no.nav.paw.rapportering.api.kafka.RapporteringTilgjengeligState +import no.nav.paw.rapportering.api.kafka.RapporteringTilgjengeligStateSerde +import no.nav.paw.rapportering.api.kafka.appTopology +import no.nav.paw.rapportering.api.services.AutorisasjonService +import no.nav.poao_tilgang.client.PoaoTilgangCachedClient +import no.nav.poao_tilgang.client.PoaoTilgangHttpClient +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.StoreQueryParameters +import org.apache.kafka.streams.StreamsBuilder +import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler +import org.apache.kafka.streams.state.QueryableStoreTypes +import org.apache.kafka.streams.state.ReadOnlyKeyValueStore +import org.apache.kafka.streams.state.Stores +import org.slf4j.LoggerFactory + +fun createDependencies( + applicationConfig: ApplicationConfig, + kafkaConfig: KafkaConfig, + kafkaStreamsConfig: KafkaConfig, + azureM2MConfig: AzureM2MConfig, + kafkaKeyConfig: KafkaKeyConfig +): Dependencies { + val logger = LoggerFactory.getLogger("rapportering-api") + + val azureM2MTokenClient = azureAdM2MTokenClient(applicationConfig.naisEnv, azureM2MConfig) + val kafkaKeyClient = kafkaKeysKlient(kafkaKeyConfig) { + azureM2MTokenClient.createMachineToMachineToken(kafkaKeyConfig.scope) + } + + val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT) + + val httpClient = HttpClient { + install(ContentNegotiation) { + jackson() + } + } + + val streamsConfig = KafkaStreamsFactory(applicationConfig.applicationIdSuffix, kafkaStreamsConfig) + .withDefaultKeySerde(Serdes.LongSerde::class) + .withDefaultValueSerde(SpecificAvroSerde::class) + + val streamsBuilder = StreamsBuilder() + .addStateStore( + Stores.keyValueStoreBuilder( + Stores.persistentKeyValueStore(applicationConfig.rapporteringStateStoreName), + Serdes.Long(), + RapporteringTilgjengeligStateSerde(), + ) + ) + + val topology = streamsBuilder.appTopology( + prometheusRegistry = prometheusMeterRegistry, + rapporteringHendelseLoggTopic = applicationConfig.rapporteringHendelseLoggTopic, + rapporteringStateStoreName = applicationConfig.rapporteringStateStoreName, + ) + + val kafkaStreams = KafkaStreams( + topology, + streamsConfig.properties.apply { + put("application.server", applicationConfig.hostname) + } + ) + + kafkaStreams.setUncaughtExceptionHandler { throwable -> + logger.error("Uventet feil: ${throwable.message}", throwable) + StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_APPLICATION + } + + kafkaStreams.start() + + val rapporteringStateStore: ReadOnlyKeyValueStore = kafkaStreams.store( + StoreQueryParameters.fromNameAndType( + applicationConfig.rapporteringStateStoreName, + QueryableStoreTypes.keyValueStore() + ) + ) + + val health = Health(kafkaStreams) + + val rapporteringProducer = RapporteringProducer(kafkaConfig, applicationConfig) + + + val poaoTilgangClient = PoaoTilgangCachedClient( + PoaoTilgangHttpClient( + applicationConfig.poaoClientConfig.url, + { azureM2MTokenClient.createMachineToMachineToken(applicationConfig.poaoClientConfig.scope) } + ) + ) + + val autorisasjonService = AutorisasjonService(poaoTilgangClient) + + return Dependencies( + kafkaKeyClient, + httpClient, + kafkaStreams, + prometheusMeterRegistry, + rapporteringStateStore, + health, + rapporteringProducer, + autorisasjonService + ) +} + +data class Dependencies( + val kafkaKeyClient: KafkaKeysClient, + val httpClient: HttpClient, + val kafkaStreams: KafkaStreams, + val prometheusMeterRegistry: PrometheusMeterRegistry, + val rapporteringStateStore: ReadOnlyKeyValueStore, + val health: Health, + val rapporteringProducer: RapporteringProducer, + val autorisasjonService: AutorisasjonService +) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Health.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Health.kt new file mode 100644 index 00000000..e1f0e681 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/Health.kt @@ -0,0 +1,44 @@ +package no.nav.paw.rapportering.api + +import io.ktor.http.HttpStatusCode +import org.apache.kafka.streams.KafkaStreams + +class Health(private val kafkaStreams: KafkaStreams) { + fun alive(): Status { + val state = kafkaStreams.state() + val httpStatusCode = when (state) { + KafkaStreams.State.CREATED -> HttpStatusCode.OK + KafkaStreams.State.REBALANCING -> HttpStatusCode.OK + KafkaStreams.State.RUNNING -> HttpStatusCode.OK + KafkaStreams.State.PENDING_SHUTDOWN -> HttpStatusCode.ServiceUnavailable + KafkaStreams.State.NOT_RUNNING -> HttpStatusCode.ServiceUnavailable + KafkaStreams.State.PENDING_ERROR -> HttpStatusCode.InternalServerError + KafkaStreams.State.ERROR -> HttpStatusCode.InternalServerError + null -> HttpStatusCode.InternalServerError + } + return status(httpStatusCode, state) + } + + fun ready(): Status { + val state = kafkaStreams.state() + val httpStatusCode = when (state) { + KafkaStreams.State.RUNNING -> HttpStatusCode.OK + KafkaStreams.State.CREATED -> HttpStatusCode.ServiceUnavailable + KafkaStreams.State.REBALANCING -> HttpStatusCode.ServiceUnavailable + KafkaStreams.State.PENDING_SHUTDOWN -> HttpStatusCode.ServiceUnavailable + KafkaStreams.State.NOT_RUNNING -> HttpStatusCode.ServiceUnavailable + KafkaStreams.State.PENDING_ERROR -> HttpStatusCode.InternalServerError + KafkaStreams.State.ERROR -> HttpStatusCode.InternalServerError + null -> HttpStatusCode.InternalServerError + } + return status(httpStatusCode, state) + } + + private fun status(kode: HttpStatusCode, kafkaStreamsTilstand: KafkaStreams.State?): Status = + Status(kode, "KafkaStreams tilstand: '${kafkaStreamsTilstand?.name}'") +} + +data class Status( + val code: HttpStatusCode, + val message: String +) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/config/ApplicationConfig.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/config/ApplicationConfig.kt new file mode 100644 index 00000000..6d3d619f --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/config/ApplicationConfig.kt @@ -0,0 +1,39 @@ +package no.nav.paw.rapportering.api.config + +import no.nav.paw.config.env.NaisEnv +import no.nav.paw.config.env.currentNaisEnv +import java.net.InetAddress + +const val APPLICATION_CONFIG_FILE_NAME = "application_config.toml" + +data class ApplicationConfig( + val applicationIdSuffix: String, + val producerId: String, + val rapporteringTopic: String, + val rapporteringHendelseLoggTopic: String, + val rapporteringStateStoreName: String, + val authProviders: AuthProviders, + val naisEnv: NaisEnv = currentNaisEnv, + val hostname: String = InetAddress.getLocalHost().hostName, + val poaoClientConfig: ServiceClientConfig +) + +data class ServiceClientConfig( + val url: String, + val scope: String +) + +data class AuthProvider( + val name: String, + val discoveryUrl: String, + val tokenEndpointUrl: String, + val clientId: String, + val claims: Claims +) + +typealias AuthProviders = List + +data class Claims( + val map: List, + val combineWithOr: Boolean = false +) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/RapporteringRequest.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/RapporteringRequest.kt new file mode 100644 index 00000000..a7396e4e --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/RapporteringRequest.kt @@ -0,0 +1,11 @@ +package no.nav.paw.rapportering.api.domain.request + +import java.util.* + +data class RapporteringRequest( + // Identitetsnummer må sendes med hvis det er en veileder som rapporterer + val identitetsnummer: String? = null, + val rapporteringsId: UUID, + val harJobbetIDennePerioden: Boolean, + val vilFortsetteSomArbeidssoeker: Boolean +) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/TilgjengeligRapporteringerRequest.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/TilgjengeligRapporteringerRequest.kt new file mode 100644 index 00000000..6109b434 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/request/TilgjengeligRapporteringerRequest.kt @@ -0,0 +1,5 @@ +package no.nav.paw.rapportering.api.domain.request + +data class TilgjengeligeRapporteringerRequest( + val identitetsnummer: String? +) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/response/TilgjengeligeRapporteringerResponse.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/response/TilgjengeligeRapporteringerResponse.kt new file mode 100644 index 00000000..e27b1181 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/domain/response/TilgjengeligeRapporteringerResponse.kt @@ -0,0 +1,23 @@ +package no.nav.paw.rapportering.api.domain.response + +import java.time.Instant +import java.util.* + +data class TilgjengeligRapportering( + val periodeId: UUID, + val rapporteringsId: UUID, + val gjelderFra: Instant, + val gjelderTil: Instant, +) + +typealias TilgjengeligRapporteringerResponse = List + +fun List.toResponse(): TilgjengeligRapporteringerResponse = this.map { + TilgjengeligRapportering( + periodeId = it.periodeId, + rapporteringsId = it.rapporteringsId, + gjelderFra = it.gjelderFra, + gjelderTil = it.gjelderTil + ) +} + diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringHendelseProcessor.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringHendelseProcessor.kt new file mode 100644 index 00000000..6be17148 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringHendelseProcessor.kt @@ -0,0 +1,61 @@ +package no.nav.paw.rapportering.api.kafka + +import no.nav.paw.rapportering.internehendelser.PeriodeAvsluttet +import no.nav.paw.rapportering.internehendelser.RapporteringTilgjengelig +import no.nav.paw.rapportering.internehendelser.RapporteringsHendelse +import no.nav.paw.rapportering.internehendelser.RapporteringsMeldingMottatt +import org.apache.kafka.streams.kstream.KStream +import org.apache.kafka.streams.kstream.Named +import org.apache.kafka.streams.processor.api.Processor +import org.apache.kafka.streams.processor.api.ProcessorContext +import org.apache.kafka.streams.state.KeyValueStore +import org.apache.kafka.streams.processor.api.Record + +fun KStream.oppdaterRapporteringHendelseState( + rapporteringStateStoreName: String +): KStream { + val processor = { + RapporteringHendelseProcessor(rapporteringStateStoreName) + } + return process(processor, Named.`as`("rapporteringHendelseProcessor"), rapporteringStateStoreName) +} + +class RapporteringHendelseProcessor( + private val stateStoreName: String, +): Processor { + private var stateStore: KeyValueStore? = null + private var context: ProcessorContext? = null + + override fun init(context: ProcessorContext?) { + super.init(context) + this.context = context + stateStore = context?.getStateStore(stateStoreName) + } + + override fun process(record: Record?) { + val value = record?.value() ?: return + val hendelseStore = requireNotNull(stateStore) { "State store is not initialized" } + when (value) { + is RapporteringTilgjengelig -> { + hendelseStore.get(value.arbeidssoekerId)?.let { + hendelseStore.put(value.arbeidssoekerId, RapporteringTilgjengeligState(it.rapporteringer.plus(value))) + } ?: hendelseStore.put(value.arbeidssoekerId, RapporteringTilgjengeligState(listOf(value))) + } + is RapporteringsMeldingMottatt -> { + hendelseStore.get(value.arbeidssoekerId)?.let { state -> + state.rapporteringer + .filterNot { it.rapporteringsId == value.rapporteringsId } + .let { rapporteringer -> + if (rapporteringer.isEmpty()) hendelseStore.delete(value.arbeidssoekerId) + else hendelseStore.put(value.arbeidssoekerId, RapporteringTilgjengeligState(rapporteringer)) + } + } + } + is PeriodeAvsluttet -> { + hendelseStore.get(value.arbeidssoekerId)?.let { + hendelseStore.delete(value.arbeidssoekerId) + } + } + } + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringProducer.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringProducer.kt new file mode 100644 index 00000000..362fa4c5 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringProducer.kt @@ -0,0 +1,71 @@ +package no.nav.paw.rapportering.api.kafka + +import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde +import no.nav.paw.config.kafka.KafkaConfig +import no.nav.paw.config.kafka.KafkaFactory +import no.nav.paw.config.kafka.sendDeferred +import no.nav.paw.rapportering.api.ApplicationInfo +import no.nav.paw.rapportering.api.config.ApplicationConfig +import no.nav.paw.rapportering.api.domain.request.RapporteringRequest +import no.nav.paw.rapportering.api.utils.logger +import no.nav.paw.rapportering.internehendelser.RapporteringTilgjengelig +import no.nav.paw.rapportering.melding.v1.Melding +import no.nav.paw.rapportering.melding.v1.vo.Bruker +import no.nav.paw.rapportering.melding.v1.vo.Metadata +import no.nav.paw.rapportering.melding.v1.vo.Svar +import org.apache.kafka.clients.producer.Producer +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.common.serialization.LongSerializer +import java.time.Instant + +class RapporteringProducer( + private val kafkaConfig: KafkaConfig, + private val applicationConfig: ApplicationConfig, +) { + private lateinit var producer: Producer + private val meldingSerde = SpecificAvroSerde().apply { + configure(mapOf("schema.registry.url" to kafkaConfig.schemaRegistry), false) + } + + init { + initializeProducer() + } + + private fun initializeProducer() { + val kafkaFactory = KafkaFactory(kafkaConfig) + producer = + kafkaFactory.createProducer( + clientId = applicationConfig.producerId, + keySerializer = LongSerializer::class, + valueSerializer = meldingSerde.serializer()::class + ) + } + + suspend fun produceMessage(key: Long, message: Melding) { + val topic = applicationConfig.rapporteringTopic + val record = ProducerRecord(topic, key, message) + val recordMetadata = producer.sendDeferred(record).await() + logger.trace("Sendte melding til kafka: offset={}", recordMetadata.offset()) + } + + fun closeProducer() { + producer.close() + } +} + +fun createMelding(state: RapporteringTilgjengelig, rapportering: RapporteringRequest): Melding = TODO() + //Melding.newBuilder() +// .setId(ApplicationInfo.id) +// .setNamespace("paw") +// .setPeriodeId(state.periodeId) +// .setSvar(Svar( +// Metadata( +// Instant.now(), +// Bruker +// ) +// )) +// .setGjelderFra(state.gjelderFra) +// .setGjelderTil(state.gjelderTil) +// .setVilFortsetteSomArbeidssoeker(rapportering.vilFortsetteSomArbeidssoeker) +// .setHarJobbetIDennePerioden(rapportering.harJobbetIDennePerioden) +// .build() diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringTilgjengeligStateSerde.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringTilgjengeligStateSerde.kt new file mode 100644 index 00000000..69ebbb5b --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/RapporteringTilgjengeligStateSerde.kt @@ -0,0 +1,28 @@ +package no.nav.paw.rapportering.api.kafka + +import no.nav.paw.rapportering.api.utils.JsonUtil +import no.nav.paw.rapportering.internehendelser.RapporteringTilgjengelig +import org.apache.kafka.common.serialization.Deserializer +import org.apache.kafka.common.serialization.Serde +import org.apache.kafka.common.serialization.Serializer + +data class RapporteringTilgjengeligState( + val rapporteringer: List +) + +class RapporteringTilgjengeligStateSerde: Serde { + override fun serializer() = RapporteringTilgjengeligStateSerializer() + override fun deserializer(): Deserializer = RapporteringTilgjengeligStateDeserializer() +} + +class RapporteringTilgjengeligStateSerializer : Serializer { + override fun serialize(topic: String?, data: RapporteringTilgjengeligState?): ByteArray { + return JsonUtil.objectMapper.writeValueAsBytes(data) + } +} + +class RapporteringTilgjengeligStateDeserializer: Deserializer { + override fun deserialize(topic: String?, data: ByteArray?): RapporteringTilgjengeligState? { + return JsonUtil.objectMapper.readValue(data, RapporteringTilgjengeligState::class.java) + } +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/Topology.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/Topology.kt new file mode 100644 index 00000000..a82a6efe --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/kafka/Topology.kt @@ -0,0 +1,19 @@ +package no.nav.paw.rapportering.api.kafka + +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import no.nav.paw.rapportering.internehendelser.RapporteringsHendelseSerde +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.streams.StreamsBuilder +import org.apache.kafka.streams.Topology +import org.apache.kafka.streams.kstream.Consumed + +fun StreamsBuilder.appTopology( + prometheusRegistry: PrometheusMeterRegistry, + rapporteringHendelseLoggTopic: String, + rapporteringStateStoreName: String, +): Topology { + stream(rapporteringHendelseLoggTopic, Consumed.with(Serdes.Long(), RapporteringsHendelseSerde())) + .oppdaterRapporteringHendelseState(rapporteringStateStoreName) + + return build() +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Authentication.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Authentication.kt new file mode 100644 index 00000000..382b0305 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Authentication.kt @@ -0,0 +1,30 @@ +package no.nav.paw.rapportering.api.plugins + +import io.ktor.server.application.Application +import io.ktor.server.auth.authentication +import no.nav.paw.rapportering.api.config.AuthProviders +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 + +fun Application.configureAuthentication(authProviders: AuthProviders) = + authentication { + authProviders.forEach { provider -> + tokenValidationSupport( + name = provider.name, + requiredClaims = RequiredClaims( + provider.name, + provider.claims.map.toTypedArray(), + provider.claims.combineWithOr + ), + config = TokenSupportConfig( + IssuerConfig( + name = provider.name, + discoveryUrl = provider.discoveryUrl, + acceptedAudience = listOf(provider.clientId) + ) + ) + ) + } + } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/HTTP.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/HTTP.kt new file mode 100644 index 00000000..173f8d27 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/HTTP.kt @@ -0,0 +1,26 @@ +package no.nav.paw.rapportering.api.plugins + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.routing.IgnoreTrailingSlash + +fun Application.configureHTTP() { + install(IgnoreTrailingSlash) + install(StatusPages) + install(CORS) { + anyHost() + + allowMethod(io.ktor.http.HttpMethod.Options) + allowMethod(io.ktor.http.HttpMethod.Put) + allowMethod(io.ktor.http.HttpMethod.Patch) + allowMethod(io.ktor.http.HttpMethod.Delete) + + allowHeader(io.ktor.http.HttpHeaders.Authorization) + allowHeader(io.ktor.http.HttpHeaders.ContentType) + allowHeader(io.ktor.http.HttpHeaders.AccessControlAllowOrigin) + + allowHeadersPrefixed("nav-") + } +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Logging.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Logging.kt new file mode 100644 index 00000000..b5387ec2 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Logging.kt @@ -0,0 +1,13 @@ +package no.nav.paw.rapportering.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.request.path + +fun Application.configureLogging() { + install(CallLogging) { + disableDefaultColors() + filter { !it.request.path().startsWith("/internal") } + } +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Metrics.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Metrics.kt new file mode 100644 index 00000000..383f7747 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Metrics.kt @@ -0,0 +1,32 @@ +package no.nav.paw.rapportering.api.plugins + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.metrics.micrometer.MicrometerMetrics +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics +import io.micrometer.core.instrument.binder.system.ProcessorMetrics +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import java.time.Duration + +fun Application.configureMetrics(prometheusMeterRegistry: PrometheusMeterRegistry) { + install(MicrometerMetrics) { + registry = prometheusMeterRegistry + meterBinders = listOf( + JvmMemoryMetrics(), + JvmGcMetrics(), + ProcessorMetrics() + ) + distributionStatisticConfig = + DistributionStatisticConfig.builder() + .percentilesHistogram(true) + .maximumExpectedValue(Duration.ofSeconds(1).toNanos().toDouble()) + .minimumExpectedValue(Duration.ofMillis(20).toNanos().toDouble()) + .serviceLevelObjectives( + Duration.ofMillis(150).toNanos().toDouble(), + Duration.ofMillis(500).toNanos().toDouble() + ) + .build() + } +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Otel.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Otel.kt new file mode 100644 index 00000000..e269c125 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Otel.kt @@ -0,0 +1,18 @@ +package no.nav.paw.rapportering.api.plugins + +import io.ktor.server.application.Application +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.install +import io.opentelemetry.api.trace.Span + + +fun Application.configureOtel() { + install( + createApplicationPlugin("OtelTraceIdPlugin") { + onCallRespond { call, _ -> + runCatching { Span.current().spanContext.traceId } + .onSuccess { call.response.headers.append("x-trace-id", it) } + } + } + ) +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Serialization.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Serialization.kt new file mode 100644 index 00000000..cb0a9f1f --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/plugins/Serialization.kt @@ -0,0 +1,21 @@ +package no.nav.paw.rapportering.api.plugins + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation + +fun Application.configureSerialization() { + install(ContentNegotiation) { + jackson { + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + registerModule(JavaTimeModule()) + registerKotlinModule() + } + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/HealthRoutes.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/HealthRoutes.kt new file mode 100644 index 00000000..72e1a3cd --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/HealthRoutes.kt @@ -0,0 +1,22 @@ +package no.nav.paw.rapportering.api.routes + +import io.ktor.server.application.call +import io.ktor.server.response.respond +import io.ktor.server.routing.Routing +import io.ktor.server.routing.get +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import no.nav.paw.rapportering.api.Health + +fun Routing.healthRoutes(prometheusMeterRegistry: PrometheusMeterRegistry, health: Health) { + get("/internal/isAlive") { + val status = health.alive() + call.respond(status.code, status.message) + } + get("/internal/isReady") { + val status = health.ready() + call.respond(status.code, status.message) + } + get("/internal/metrics") { + call.respond(prometheusMeterRegistry.scrape()) + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RapporteringRoutes.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RapporteringRoutes.kt new file mode 100644 index 00000000..b7b9c9d6 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RapporteringRoutes.kt @@ -0,0 +1,105 @@ +package no.nav.paw.rapportering.api.routes + +import io.ktor.client.HttpClient +import io.ktor.client.call.* +import io.ktor.client.request.bearerAuth +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.auth.* +import io.ktor.server.response.respond +import io.ktor.server.routing.* +import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient +import no.nav.paw.rapportering.api.domain.request.RapporteringRequest +import no.nav.paw.rapportering.api.domain.request.TilgjengeligeRapporteringerRequest +import no.nav.paw.rapportering.api.domain.response.TilgjengeligRapporteringerResponse +import no.nav.paw.rapportering.api.domain.response.toResponse +import no.nav.paw.rapportering.api.kafka.RapporteringProducer +import no.nav.paw.rapportering.api.kafka.RapporteringTilgjengeligState +import no.nav.paw.rapportering.api.kafka.createMelding +import no.nav.paw.rapportering.api.services.AutorisasjonService +import no.nav.paw.rapportering.api.utils.logger +import no.nav.poao_tilgang.client.TilgangType +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.KeyQueryMetadata +import org.apache.kafka.streams.state.ReadOnlyKeyValueStore + +fun Route.rapporteringRoutes( + kafkaKeyClient: KafkaKeysClient, + rapporteringStateStoreName: String, + rapporteringStateStore: ReadOnlyKeyValueStore, + kafkaStreams: KafkaStreams, + httpClient: HttpClient, + rapporteringProducer: RapporteringProducer, + autorisasjonService: AutorisasjonService +) { + route("/api/v1") { + authenticate("tokenx", "azure") { + post("/tilgjengelige-rapporteringer") { request -> + with(requestScope(request.identitetsnummer, autorisasjonService, kafkaKeyClient, TilgangType.LESE)) { + val arbeidssoekerId = this + + rapporteringStateStore + .get(arbeidssoekerId) + ?.rapporteringer + ?.toResponse() + ?.let { rapporteringerTilgjengelig -> + logger.info("Fant ${rapporteringerTilgjengelig.size} rapporteringer") + return@post call.respond(HttpStatusCode.OK, rapporteringerTilgjengelig) + } + + val metadata = kafkaStreams.queryMetadataForKey( + rapporteringStateStoreName, arbeidssoekerId, Serdes.Long().serializer() + ) + + if (metadata == null || metadata == KeyQueryMetadata.NOT_AVAILABLE) { + logger.info("Fant ikke metadata for arbeidsoeker, $metadata") + return@post call.respond(HttpStatusCode.OK, emptyList()) + } else { + val response = httpClient.post("http://${metadata.activeHost().host()}/api/v1/tilgjengelige-rapporteringer") { + call.request.headers["Authorization"]?.let { bearerAuth(it) } + setBody(request) + } + return@post call.respond(response.status, response.body()) + } + } + + } + post("/rapportering") { rapportering -> + with(requestScope(rapportering.identitetsnummer, autorisasjonService, kafkaKeyClient, TilgangType.SKRIVE)) { + val arbeidsoekerId = this + + rapporteringStateStore + .get(arbeidsoekerId) + ?.rapporteringer + ?.firstOrNull { it.rapporteringsId == rapportering.rapporteringsId } + ?.let { + logger.info("Rapportering med id ${rapportering.rapporteringsId} funnet") + val rapporteringsMelding = createMelding(it, rapportering) + rapporteringProducer.produceMessage(arbeidsoekerId, rapporteringsMelding) + + return@post call.respond(HttpStatusCode.OK) + } + + val metadata = kafkaStreams.queryMetadataForKey( + rapporteringStateStoreName, arbeidsoekerId, Serdes.Long().serializer() + ) + + if (metadata == null || metadata == KeyQueryMetadata.NOT_AVAILABLE) { + logger.info("Fant ikke metadata for arbeidsoeker, $metadata") + return@post call.respond(HttpStatusCode.NotFound) + } else { + val response = httpClient.post("http://${metadata.activeHost().host()}/api/v1/rapportering") { + call.request.headers["Authorization"]?.let { bearerAuth(it) } + setBody(rapportering) + } + return@post call.respond(response.status) + } + } + } + } + } +} + diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RequestScope.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RequestScope.kt new file mode 100644 index 00000000..15de4286 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/RequestScope.kt @@ -0,0 +1,48 @@ +package no.nav.paw.rapportering.api.routes + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.util.pipeline.PipelineContext +import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient +import no.nav.paw.rapportering.api.services.AutorisasjonService +import no.nav.paw.rapportering.api.services.NavAnsatt +import no.nav.paw.rapportering.api.utils.getClaims +import no.nav.paw.rapportering.api.utils.getNAVident +import no.nav.paw.rapportering.api.utils.getOid +import no.nav.paw.rapportering.api.utils.getPid +import no.nav.paw.rapportering.api.utils.isAzure +import no.nav.poao_tilgang.client.TilgangType + + +context(PipelineContext) +suspend fun requestScope( + requestIdentitetsnummer: String?, + autorisasjonService: AutorisasjonService, + kafkaKeyClient: KafkaKeysClient, + tilgangType: TilgangType +): Long { + val claims = call.getClaims() + val identitetsnummer = claims?.getPid() + ?: requestIdentitetsnummer + ?: throw IllegalArgumentException("Identitetsnummer mangler") + + if (claims?.isAzure == true) { + val oid = claims.getOid() + val navIdent = claims.getNAVident() + val navAnsatt = NavAnsatt( + azureId = oid, + navIdent= navIdent + ) + val tilgang = autorisasjonService.verifiserTilgangTilBruker(navAnsatt, identitetsnummer, tilgangType) + if (!tilgang) { + call.respond(HttpStatusCode.Forbidden) + throw IllegalArgumentException("NAV-ansatt har ikke tilgang til bruker") + } + } + + val arbeidsoekerId = kafkaKeyClient.getIdAndKey(identitetsnummer)?.id + ?: throw IllegalArgumentException("Fant ikke arbeidsoekerId for identitetsnummer") + + return arbeidsoekerId +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/SwaggerRoutes.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/SwaggerRoutes.kt new file mode 100644 index 00000000..66b15d0a --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/routes/SwaggerRoutes.kt @@ -0,0 +1,8 @@ +package no.nav.paw.rapportering.api.routes + +import io.ktor.server.plugins.swagger.swaggerUI +import io.ktor.server.routing.Route + +fun Route.swaggerRoutes() { + swaggerUI(path = "docs", swaggerFile = "openapi/rapporteringapi.yaml") +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/services/AutorisasjonService.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/services/AutorisasjonService.kt new file mode 100644 index 00000000..ff177374 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/services/AutorisasjonService.kt @@ -0,0 +1,37 @@ +package no.nav.paw.rapportering.api.services + +import no.nav.paw.rapportering.api.utils.auditLogMelding +import no.nav.paw.rapportering.api.utils.auditLogger +import no.nav.paw.rapportering.api.utils.logger +import no.nav.poao_tilgang.client.NavAnsattTilgangTilEksternBrukerPolicyInput +import no.nav.poao_tilgang.client.PoaoTilgangCachedClient +import no.nav.poao_tilgang.client.TilgangType +import java.util.* + +class AutorisasjonService( + private val poaoTilgangHttpClient: PoaoTilgangCachedClient +) { + fun verifiserTilgangTilBruker( + navAnsatt: NavAnsatt, + identitetsnummer: String, + tilgangType: TilgangType + ): Boolean { + val harNavAnsattTilgang = + poaoTilgangHttpClient.evaluatePolicy( + NavAnsattTilgangTilEksternBrukerPolicyInput( + navAnsattAzureId = navAnsatt.azureId, + tilgangType = tilgangType, + norskIdent = identitetsnummer + ) + ).getOrThrow().isPermit + + if (!harNavAnsattTilgang) { + logger.info("NAV-ansatt har ikke $tilgangType til bruker") + } else { + auditLogger.info(auditLogMelding(identitetsnummer, navAnsatt, tilgangType, "NAV ansatt har benyttet $tilgangType tilgang til informasjon om bruker")) + } + return harNavAnsattTilgang + } +} + +data class NavAnsatt(val azureId: UUID, val navIdent: String) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Claims.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Claims.kt new file mode 100644 index 00000000..15f29b2e --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Claims.kt @@ -0,0 +1,45 @@ +package no.nav.paw.rapportering.api.utils + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import no.nav.security.token.support.core.context.TokenValidationContext +import no.nav.security.token.support.v2.TokenValidationContextPrincipal +import java.util.* + +val claimsList = listOf( + "tokenx" to "pid", + "azure" to "NavIdent", + "azure" to "oid" +) + +data class ResolvedClaim( + val issuer: String, + val claim: Claim +) + +data class Claim( + val name: String, + val value: String +) + +typealias ResolvedClaims = List + +fun TokenValidationContext?.getResolvedClaims(): ResolvedClaims = claimsList.mapNotNull { (issuer, claimName) -> + this.resolveClaim(issuer, claimName)?.let { claimValue -> + ResolvedClaim(issuer, Claim(claimName, claimValue)) + } +} + +fun TokenValidationContext?.resolveClaim(issuer: String, claimName: String): String? = + this?.getClaims(issuer)?.getStringClaim(claimName) + +fun ApplicationCall.getClaims() = this.authentication.principal() + ?.context + ?.getResolvedClaims() + +val ResolvedClaims.isTokenx get(): Boolean = any { it.issuer == "tokenx" } +val ResolvedClaims.isAzure get(): Boolean = any { it.issuer == "azure" } + +fun ResolvedClaims.getPid(): String = find { it.issuer == "tokenx" && it.claim.name == "pid" }?.claim?.value ?: throw IllegalArgumentException("Fant ikke Pid i token") +fun ResolvedClaims.getOid(): UUID = find { it.issuer == "azure" && it.claim.name == "oid" }?.claim?.value?.let(UUID::fromString) ?: throw IllegalArgumentException("Fant ikke oid i token") +fun ResolvedClaims.getNAVident(): String = find { it.issuer == "azure" && it.claim.name == "NAVident" }?.claim?.value ?: throw IllegalArgumentException("Fant ikke NAVident i token") diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Logger.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Logger.kt new file mode 100644 index 00000000..19c89087 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/Logger.kt @@ -0,0 +1,32 @@ +package no.nav.paw.rapportering.api.utils + +import no.nav.paw.rapportering.api.services.NavAnsatt +import no.nav.common.audit_log.cef.CefMessage +import no.nav.common.audit_log.cef.CefMessageEvent +import no.nav.common.audit_log.cef.CefMessageSeverity +import no.nav.poao_tilgang.client.TilgangType + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline val T.logger: Logger get() = LoggerFactory.getLogger(T::class.java) + +inline val auditLogger get() = LoggerFactory.getLogger("AuditLogger") + +fun auditLogMelding( + identitetsnummer: String, + navAnsatt: NavAnsatt, + tilgangType: TilgangType, + melding: String, +): String = + CefMessage.builder() + .applicationName("paw-rapportering-api") // TODO: fra config + .event(if (tilgangType == TilgangType.LESE) CefMessageEvent.ACCESS else CefMessageEvent.UPDATE) + .name("Sporingslogg") + .severity(CefMessageSeverity.INFO) + .sourceUserId(navAnsatt.navIdent) + .destinationUserId(identitetsnummer) + .timeEnded(System.currentTimeMillis()) + .extension("msg", melding) + .build() + .toString() \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/ObjectMapper.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/ObjectMapper.kt new file mode 100644 index 00000000..15b846e1 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/rapportering/api/utils/ObjectMapper.kt @@ -0,0 +1,23 @@ +package no.nav.paw.rapportering.api.utils + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule + +object JsonUtil { + val objectMapper: ObjectMapper = ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .registerModules( + KotlinModule.Builder() + .withReflectionCacheSize(512) + .configure(KotlinFeature.NullToEmptyCollection, true) + .configure(KotlinFeature.NullToEmptyMap, true) + .configure(KotlinFeature.NullIsSameAsDefault, false) + .configure(KotlinFeature.SingletonSupport, false) + .configure(KotlinFeature.StrictNullChecks, false) + .build(), + JavaTimeModule() + ) +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/local/application_config.toml b/apps/bekreftelse-api/src/main/resources/local/application_config.toml new file mode 100644 index 00000000..79be8a99 --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/local/application_config.toml @@ -0,0 +1,30 @@ +applicationIdSuffix = "v1" +producerId = "paw-rapportering-api-v1" +rapporteringTopic = "paw.rapportering-v1" +rapporteringHendelseLoggTopic = "paw.rapportering-hendelse-logg-v1" +rapporteringStateStoreName = "RapporteringStateStore" +hostname = "${HOSTNAME}" + +[[authProviders]] +name = "tokenx" +discoveryUrl = "http://localhost:8081/default/.well-known/openid-configuration" +tokenEndpointUrl = "http://localhost:8081/default/token" +clientId = "paw-rapportering-api" + +[authProviders.claims] +map = [ "acr=Level4", "acr=idporten-loa-high" ] +combineWithOr = true + +[[authProviders]] +name = "azure" +discoveryUrl = "http://localhost:8081/default/.well-known/openid-configuration" +tokenEndpointUrl = "http://localhost:8081/default/token" +clientId = "paw-rapportering-api" + +[authProviders.claims] +map = [ "NAVident" ] +combineWithOr = false + +[poaoClientConfig] +url = "http://localhost:8090/poao-tilgang/" +scope = "api://test.test.poao-tilgang/.default" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/local/azure_m2m_key_config.toml b/apps/bekreftelse-api/src/main/resources/local/azure_m2m_key_config.toml new file mode 100644 index 00000000..3a45f041 --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/local/azure_m2m_key_config.toml @@ -0,0 +1,2 @@ +tokenEndpointUrl = "http://localhost:8081/default/token" +clientId = "paw-arbeidssoekerregisteret-utgang-pdl" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/local/kafka_configuration_schemareg.toml b/apps/bekreftelse-api/src/main/resources/local/kafka_configuration_schemareg.toml new file mode 100644 index 00000000..0b52f870 --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/local/kafka_configuration_schemareg.toml @@ -0,0 +1,3 @@ +brokers = "localhost:9092" +[schemaRegistry] +url = "http://localhost:8082" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/local/kafka_key_generator_client_config.toml b/apps/bekreftelse-api/src/main/resources/local/kafka_key_generator_client_config.toml new file mode 100644 index 00000000..caeec4fd --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/local/kafka_key_generator_client_config.toml @@ -0,0 +1,2 @@ +url = "MOCK" +scope = "api://../.default" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/local/kafka_streams_configuration_schemareg.toml b/apps/bekreftelse-api/src/main/resources/local/kafka_streams_configuration_schemareg.toml new file mode 100644 index 00000000..dc69514a --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/local/kafka_streams_configuration_schemareg.toml @@ -0,0 +1,4 @@ +brokers = "localhost:9092" +applicationIdPrefix = "paw-rapportering-v1" +[schemaRegistry] +url = "http://localhost:8082" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/logback.xml b/apps/bekreftelse-api/src/main/resources/logback.xml new file mode 100644 index 00000000..8c0b3bad --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/logback.xml @@ -0,0 +1,44 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %c{1}:%L - %m%n + + + + + + /secure-logs/secure.log + + /secure-logs/secure.log.%i + 1 + 1 + + + 50MB + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/bekreftelse-api/src/main/resources/nais/application_config.toml b/apps/bekreftelse-api/src/main/resources/nais/application_config.toml new file mode 100644 index 00000000..df4dc806 --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/nais/application_config.toml @@ -0,0 +1,33 @@ +applicationIdSuffix = "v1" +producerId = "paw-rapportering-api-v1" +rapporteringTopic = "paw.rapportering-v1" +rapporteringHendelseLoggTopic = "paw.rapportering-hendelse-logg-v1" +rapporteringStateStoreName = "RapporteringStateStore" +hostname = "${HOSTNAME}" + +[[authProviders]] +name = "tokenx" +discoveryUrl = "${TOKEN_X_WELL_KNOWN_URL}" +tokenEndpointUrl = "${TOKEN_X_TOKEN_ENDPOINT}" +clientId = "${TOKEN_X_CLIENT_ID}" + +[authProviders.claims] +map = ["acr=Level4", "acr=idporten-loa-high"] +combineWithOr = true + +[[authProviders]] +name = "azure" +discoveryUrl = "${AZURE_APP_WELL_KNOWN_URL}" +tokenEndpointUrl = "${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT}" +clientId = "${AZURE_APP_CLIENT_ID}" + +[authProviders.claims] +map = ["NAVident"] + +[kafkaKeyGeneratorClient] +url = "http://paw-kafka-key-generator/api/v2/hentEllerOpprett" +scope = "${KAFKA_KEYS_SCOPE}" + +[poaoClientConfig] +url = "http://poao-tilgang.poao.svc.cluster.local" +scope = "api://${NAIS_CLUSTER_NAME}.poao.poao-tilgang/.default" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/nais/azure_m2m_key_config.toml b/apps/bekreftelse-api/src/main/resources/nais/azure_m2m_key_config.toml new file mode 100644 index 00000000..b6a0195e --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/nais/azure_m2m_key_config.toml @@ -0,0 +1,2 @@ +tokenEndpointUrl = "${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT}" +clientId = "${AZURE_APP_CLIENT_ID}" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/nais/kafka_key_generator_client_config.toml b/apps/bekreftelse-api/src/main/resources/nais/kafka_key_generator_client_config.toml new file mode 100644 index 00000000..e83e543b --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/nais/kafka_key_generator_client_config.toml @@ -0,0 +1,2 @@ +url = "http://paw-kafka-key-generator/api/v2/hentEllerOpprett" +scope = "${KAFKA_KEYS_SCOPE}" \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/resources/openapi/rapporteringapi.yaml b/apps/bekreftelse-api/src/main/resources/openapi/rapporteringapi.yaml new file mode 100644 index 00000000..e69de29b diff --git a/apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json b/apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json new file mode 100644 index 00000000..5d2917de --- /dev/null +++ b/apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json @@ -0,0 +1,10 @@ +{ + "hendelseId": "cfc94678-b0ea-462f-8c40-1a6e22fc76fb", + "periodeId": "fe3080b3-063d-4d8c-8cad-bba10d943538", + "identitetsnummer": 12345678910, + "arbeidssoekerId": 2072234860133, + "rapporteringsId": "0415bed8-92e8-4474-9ded-56b9a477bdba", + "gjelderFra": "2024-05-27T10:40:05.056592103Z", + "gjelderTil": "2024-06-07T10:40:05.056592103Z", + "hendelseType": "rapportering.tilgjengelig" +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e9c82b63..4ffa40e2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,8 @@ include( "apps:hendelselogg-backup", "apps:utgang-pdl", "apps:kafka-key-generator", - "apps:bekreftelse-tjeneste" + "apps:bekreftelse-tjeneste", + "apps:bekreftelse-api", ) dependencyResolutionManagement {