diff --git a/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/MaskinPortenTokenProvider.kt b/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/MaskinPortenTokenProvider.kt new file mode 100644 index 0000000000..27c874d2c1 --- /dev/null +++ b/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/MaskinPortenTokenProvider.kt @@ -0,0 +1,100 @@ +package no.nav.mulighetsrommet.tokenprovider + +import com.nimbusds.jose.JWSSigner +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.oauth2.sdk.JWTBearerGrant +import com.nimbusds.oauth2.sdk.Scope +import com.nimbusds.oauth2.sdk.TokenRequest +import com.nimbusds.oauth2.sdk.TokenResponse +import no.nav.common.token_client.utils.TokenClientUtils +import org.slf4j.LoggerFactory +import java.net.URI +import java.util.* + +class MaskinPortenTokenProvider( + private val clientId: String, + private val issuer: String, + tokenEndpointUrl: String, + privateJwk: String, +) { + private val log = LoggerFactory.getLogger(javaClass) + private val tokenEndpoint: URI + private val privateJwkKeyId: String + private val assertionSigner: JWSSigner + + data class Config( + val clientId: String, + val issuer: String, + val tokenEndpointUrl: String, + val privateJwk: String, + ) + + init { + val rsaKey = RSAKey.parse(privateJwk) + + tokenEndpoint = URI.create(tokenEndpointUrl) + privateJwkKeyId = rsaKey.keyID + assertionSigner = RSASSASigner(rsaKey) + } + + private fun createToken(scope: String, targetAudience: String): String { + val signedJwt = TokenClientUtils.signedClientAssertion( + TokenClientUtils.clientAssertionHeader(privateJwkKeyId), + clientAssertionClaims(targetAudience = targetAudience, scope = scope), + assertionSigner, + ) + + val request = + TokenRequest( + tokenEndpoint, + JWTBearerGrant(signedJwt.clientAssertion), + Scope(*(scope.split(" ")).toTypedArray()), + ) + + val response = TokenResponse.parse(request.toHTTPRequest().send()) + + if (!response.indicatesSuccess()) { + val tokenErrorResponse = response.toErrorResponse() + log.error( + "Failed to fetch Maskinporten M2M token for scope={}. Error: {}", + scope, + tokenErrorResponse.toJSONObject().toString(), + ) + throw RuntimeException("Failed to fetch Maskinporten M2M token for scope=$scope") + } + + return response + .toSuccessResponse() + .tokens + .accessToken + .value + } + + fun withScope(scope: String, targetAudience: String): M2MTokenProvider { + return M2MTokenProvider exchange@{ accessType -> + createToken(scope, targetAudience) + } + } + + fun clientAssertionClaims( + targetAudience: String, + scope: String, + ): JWTClaimsSet { + val now = Date() + val expiration = Date(now.toInstant().plusSeconds(30).toEpochMilli()) + + return JWTClaimsSet.Builder() + .subject(clientId) + .issuer(clientId) + .audience(issuer) + .jwtID(UUID.randomUUID().toString()) + .issueTime(now) + .notBeforeTime(now) + .expirationTime(expiration) + .claim("resource", targetAudience) + .claim("scope", scope) + .build() + } +} diff --git a/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/TokenProvider.kt b/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/TokenProvider.kt index f858053a2d..512cd30cb2 100644 --- a/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/TokenProvider.kt +++ b/common/token-provider/src/main/kotlin/no/nav/mulighetsrommet/tokenprovider/TokenProvider.kt @@ -23,6 +23,10 @@ fun interface TokenProvider { suspend fun exchange(accessType: AccessType): String } +fun interface M2MTokenProvider { + suspend fun exchange(accessType: AccessType.M2M): String +} + /** * Denne wrapper kall til login.microsoft i `CoroutineScope(Dispatchers.IO)` * som gjør at man kan gjøre token exchanges i parallel (dvs. med coroutines uten @@ -111,6 +115,23 @@ private fun createM2mTokenClient(clientId: String, tokenEndpointUrl: String): Ma else -> AzureAdTokenClientBuilder.builder().withNaisDefaults().buildMachineToMachineTokenClient() } +fun createMaskinportenM2mTokenClient(clientId: String, tokenEndpointUrl: String, issuer: String): MaskinPortenTokenProvider? = + when (NaisEnv.current()) { + NaisEnv.Local -> MaskinPortenTokenProvider( + clientId = clientId, + tokenEndpointUrl = tokenEndpointUrl, + privateJwk = createMockRSAKey("maskinporten").toJSONString(), + issuer = issuer, + ) + NaisEnv.ProdGCP -> null // TODO: Remove when prod + else -> MaskinPortenTokenProvider( + clientId = clientId, + tokenEndpointUrl = tokenEndpointUrl, + privateJwk = System.getenv("MASKINPORTEN_CLIENT_JWK"), + issuer = issuer, + ) + } + private fun createMockRSAKey(keyID: String): RSAKey = KeyPairGenerator .getInstance("RSA").let { it.initialize(2048) diff --git a/frontend/arrangor-flate/app/auth/altinn.server.ts b/frontend/arrangor-flate/app/auth/altinn.server.ts deleted file mode 100644 index d2dbaf5fb5..0000000000 --- a/frontend/arrangor-flate/app/auth/altinn.server.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Miljø, hentMiljø } from "~/services/miljø"; -import { oboExchange, requirePersonIdent } from "./auth.server"; - -interface TilgangerResponse { - roller: TiltaksarrangorRoller[]; -} - -interface TiltaksarrangorRoller { - organisasjonsnummer: string; - roller: Roller[]; -} - -type Roller = "TILTAK_ARRANGOR_REFUSJON"; - -const mockRoller: TiltaksarrangorRoller[] = [ - { - organisasjonsnummer: "971808616", - roller: ["TILTAK_ARRANGOR_REFUSJON"], - }, -]; - -export async function getTilganger(request: Request): Promise { - if (hentMiljø() === Miljø.Lokalt) { - return { roller: mockRoller }; - } - - const [personident, token] = await Promise.all([ - requirePersonIdent(request), - oboExchange( - request, - `${process.env.NAIS_CLUSTER_NAME}:team-mulighetsrommet:mulighetsrommet-altinn-acl`, - ), - ]); - - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }; - - const payload = { - method: "POST", - body: JSON.stringify({ personident: personident }), - headers: { - ...headers, - }, - }; - - // TODO Sette opp openAPi-client med "hardkodet" openapi.yaml - const response = await fetch( - "http://mulighetsrommet-altinn-acl/api/v1/rolle/tiltaksarrangor", - payload, - ); - - if (!response.ok) { - // eslint-disable-next-line no-console - console.log( - `Klarte ikke hente tilganger for bruker. Status = ${response.status} - Error: ${response.statusText} - ${JSON.stringify(response, null, 2)}`, - ); - throw new Error("Klarte ikke hente tilganger for bruker"); - } - - const tilganger = await response.json(); - if (hentMiljø() === Miljø.DevGcp) { - return { - roller: [...tilganger.roller, ...mockRoller], - }; - } - - return tilganger; -} diff --git a/frontend/arrangor-flate/app/mocks/altinnMocks.ts b/frontend/arrangor-flate/app/mocks/altinnMocks.ts deleted file mode 100644 index ba8a568ee0..0000000000 --- a/frontend/arrangor-flate/app/mocks/altinnMocks.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { http, HttpResponse, PathParams } from "msw"; -import { Rolletilgang, RolleTilgangRequest, RolleType } from "../domene/domene"; - -export const mulighetsrommetAltinnAclHandlers = [ - http.post( - "*/mulighetsrommet-altinn-acl/api/v1/rolle/tiltaksarrangor", - () => - HttpResponse.json({ - roller: [ - { organisasjonsnummer: "123456789", roller: [RolleType.TILTAK_ARRANGOR_REFUSJON] }, - ], - }), - ), -]; diff --git a/frontend/arrangor-flate/app/mocks/handlers.ts b/frontend/arrangor-flate/app/mocks/handlers.ts index 38f9e217ea..7e08352c9c 100644 --- a/frontend/arrangor-flate/app/mocks/handlers.ts +++ b/frontend/arrangor-flate/app/mocks/handlers.ts @@ -1,4 +1,3 @@ -import { mulighetsrommetAltinnAclHandlers } from "./altinnMocks"; import { refusjonHandlers } from "./refusjonMocks"; -export const handlers = [...mulighetsrommetAltinnAclHandlers, ...refusjonHandlers]; +export const handlers = [...refusjonHandlers]; diff --git a/frontend/arrangor-flate/app/routes/_index.tsx b/frontend/arrangor-flate/app/routes/_index.tsx index 4ff811b06a..b71ade55d4 100644 --- a/frontend/arrangor-flate/app/routes/_index.tsx +++ b/frontend/arrangor-flate/app/routes/_index.tsx @@ -1,10 +1,9 @@ import { RefusjonKravAft, RefusjonskravService } from "@mr/api-client"; import { Heading, VStack } from "@navikt/ds-react"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; +import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { RefusjonskravTable } from "~/components/refusjonskrav/RefusjonskravTable"; -import { getTilganger } from "../auth/altinn.server"; import { setupOpenApi } from "../auth/auth.server"; import { PageHeader } from "../components/PageHeader"; @@ -16,20 +15,8 @@ export const meta: MetaFunction = () => { }; export async function loader({ request }: LoaderFunctionArgs) { - const tilganger = await getTilganger(request); - - if (tilganger.roller.length === 0) { - throw redirect("/ingen-tilgang"); - } - await setupOpenApi(request); - // TODO: Vi trenger en måte å velge orgrn på - // F. eks med bedriftsvelger (eller hva det heter) som min-side-arbeidsgiver bruker - const krav = await RefusjonskravService.getRefusjonskrav({ - requestBody: { - orgnr: tilganger.roller.map((rolle) => rolle.organisasjonsnummer), - }, - }); + const krav = await RefusjonskravService.getRefusjonskrav(); return json({ krav }); } diff --git a/mulighetsrommet-api/.nais/nais-dev.yaml b/mulighetsrommet-api/.nais/nais-dev.yaml index 80f33904f6..7e925e2924 100644 --- a/mulighetsrommet-api/.nais/nais-dev.yaml +++ b/mulighetsrommet-api/.nais/nais-dev.yaml @@ -138,7 +138,15 @@ spec: - host: axsys.dev-fss-pub.nais.io - host: pdl-api.dev-fss-pub.nais.io - host: api.utdanning.no - + - host: platform.tt02.altinn.no + - host: test.maskinporten.no envFrom: - secret: mulighetsrommet-api - secret: mr-admin-flate-unleash-api-token + - secret: altinn-api-key + maskinporten: + enabled: true + scopes: + consumes: + - name: "altinn:authorization/authorize" + - name: "altinn:accessmanagement/authorizedparties.resourceowner" diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/Config.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/Config.kt index c012de27bc..d24cf2609f 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/Config.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/Config.kt @@ -2,6 +2,7 @@ package no.nav.mulighetsrommet.api import io.ktor.client.engine.* import io.ktor.client.engine.cio.* +import no.nav.mulighetsrommet.api.clients.altinn.AltinnClient import no.nav.mulighetsrommet.api.clients.brreg.BrregClient import no.nav.mulighetsrommet.api.clients.sanity.SanityClient import no.nav.mulighetsrommet.api.clients.utdanning.UtdanningClient @@ -50,11 +51,13 @@ data class AppConfig( val pdl: ServiceClientConfig, val engine: HttpClientEngine = CIO.create(), val utdanning: UtdanningClient.Config, + val altinn: AltinnClient.Config, ) data class AuthConfig( val azure: AuthProvider, val tokenx: AuthProvider, + val maskinporten: AuthProvider, val roles: List, ) diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/altinn/AltinnClient.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/altinn/AltinnClient.kt new file mode 100644 index 0000000000..91f665e925 --- /dev/null +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/altinn/AltinnClient.kt @@ -0,0 +1,119 @@ +package no.nav.mulighetsrommet.api.clients.altinn + +import io.ktor.client.call.* +import io.ktor.client.engine.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.serialization.Serializable +import no.nav.mulighetsrommet.domain.dto.NorskIdent +import no.nav.mulighetsrommet.domain.dto.Organisasjonsnummer +import no.nav.mulighetsrommet.ktor.clients.httpJsonClient +import no.nav.mulighetsrommet.tokenprovider.AccessType +import no.nav.mulighetsrommet.tokenprovider.M2MTokenProvider +import org.slf4j.LoggerFactory + +const val PAGINERING_SIZE = 500 + +class AltinnClient( + private val baseUrl: String, + private val altinnApiKey: String, + private val tokenProvider: M2MTokenProvider, + clientEngine: HttpClientEngine = CIO.create(), +) { + private val log = LoggerFactory.getLogger(javaClass) + private val client = httpJsonClient(clientEngine).config { + install(HttpCache) + } + + data class Config( + val url: String, + val apiKey: String, + val scope: String, + ) + + suspend fun hentRoller(norskIdent: NorskIdent): List { + val roller: MutableList = mutableListOf() + var ferdig = false + while (!ferdig) { + log.info("Henter organisasjoner fra Altinn") + val authorizedParties = hentAuthorizedParties(norskIdent) + roller.addAll(findAltinnRoller(authorizedParties, listOf(AltinnRessurs.TILTAK_ARRANGOR_REFUSJON))) + ferdig = roller.size < PAGINERING_SIZE + } + return roller + } + + private fun findAltinnRoller( + parties: List, + ressurser: List, + roller: MutableList = mutableListOf(), + ): List { + for (party in parties) { + if (party.organizationNumber != null) { + roller.addAll( + ressurser + .filter { party.authorizedResources.contains(it.ressursId) } + .map { + AltinnRolle( + organisasjonsnummer = Organisasjonsnummer(party.organizationNumber), + ressurs = it, + ) + }, + ) + } + findAltinnRoller(party.subunits, ressurser, roller) + } + return roller + } + + private suspend fun hentAuthorizedParties(norskIdent: NorskIdent): List { + @Serializable + data class Request( + val type: String, + val value: String, + ) + val response = client.post("$baseUrl/accessmanagement/api/v1/resourceowner/authorizedparties") { + parameter("includeAltinn2", "true") + header("Ocp-Apim-Subscription-Key", altinnApiKey) + bearerAuth(tokenProvider.exchange(AccessType.M2M)) + header(HttpHeaders.ContentType, ContentType.Application.Json) + setBody( + Request( + type = "urn:altinn:person:identifier-no", + value = norskIdent.value, + ), + ) + } + + if (response.status != HttpStatusCode.OK) { + log.error("Klarte ikke hente organisasjoner for Altinn. response: ${response.status}, body=${response.bodyAsText()}") + throw RuntimeException("Klarte ikke å hente organisasjoner code=${response.status}") + } + + if (!response.headers["X-Warning-LimitReached"].isNullOrEmpty()) { + log.error("For mange tilganger. Klarte ikke hente tilganger for bruker. response: ${response.status}") + } + + return response.body() + } + + @Serializable + data class AuthorizedParty( + val organizationNumber: String? = null, + val type: String, + val authorizedResources: List, + val subunits: List, + ) +} + +enum class AltinnRessurs(val ressursId: String) { + TILTAK_ARRANGOR_REFUSJON("tiltak-arrangor-refusjon"), +} + +data class AltinnRolle( + val organisasjonsnummer: Organisasjonsnummer, + val ressurs: AltinnRessurs, +) diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/oppfolging/VeilarboppfolgingClient.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/oppfolging/VeilarboppfolgingClient.kt index a7ea06b33d..13d49d5ae6 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/oppfolging/VeilarboppfolgingClient.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/clients/oppfolging/VeilarboppfolgingClient.kt @@ -93,7 +93,7 @@ class VeilarboppfolgingClient( suspend fun erBrukerUnderOppfolging( fnr: NorskIdent, - accessType: AccessType, + accessType: AccessType.OBO, ): Either { return hentGjeldendePeriode(fnr, accessType) .map { (it.sluttDato == null).right() } @@ -108,7 +108,7 @@ class VeilarboppfolgingClient( private suspend fun hentGjeldendePeriode( fnr: NorskIdent, - accessType: AccessType, + accessType: AccessType.OBO, ): Either { gjeldendePeriodeCache.getIfPresent(fnr)?.let { return@hentGjeldendePeriode it.right() } @@ -125,7 +125,7 @@ class VeilarboppfolgingClient( } HttpStatusCode.Forbidden -> { - log.info("Manglet tilgang til å hente oppfølgingsstatus for bruker.") + log.info("Manglet tilgang til å hente gjeldende periode for bruker.") OppfolgingError.Forbidden.left() } @@ -134,7 +134,7 @@ class VeilarboppfolgingClient( } else -> if (!response.status.isSuccess()) { - log.warn("Klarte ikke hente oppfølgingsstatus for bruker. Status: ${response.status}") + log.warn("Klarte ikke hente gjeldende periode for bruker. Status: ${response.status}") OppfolgingError.Error.left() } else { response.body().right() diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonRoutes.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonRoutes.kt index cfd3f49f73..01074772c0 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonRoutes.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonRoutes.kt @@ -3,7 +3,6 @@ package no.nav.mulighetsrommet.api.okonomi.refusjon import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.* -import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.util.* @@ -13,8 +12,8 @@ import no.nav.mulighetsrommet.api.okonomi.models.DeltakelsePeriode import no.nav.mulighetsrommet.api.okonomi.models.RefusjonKravBeregningAft import no.nav.mulighetsrommet.api.okonomi.models.RefusjonskravDto import no.nav.mulighetsrommet.api.plugins.getPid +import no.nav.mulighetsrommet.api.services.ArrangorRolleService import no.nav.mulighetsrommet.domain.dto.NorskIdent -import no.nav.mulighetsrommet.domain.dto.Organisasjonsnummer import no.nav.mulighetsrommet.domain.serializers.LocalDateSerializer import no.nav.mulighetsrommet.domain.serializers.LocalDateTimeSerializer import no.nav.mulighetsrommet.domain.serializers.UUIDSerializer @@ -34,13 +33,16 @@ fun Route.refusjonRoutes() { Environment(), ) val service: RefusjonService by inject() + val arrangorRolleService: ArrangorRolleService by inject() route("/api/v1/intern/refusjon") { - post("/krav") { - val request = call.receive() - val norskIdent = getPid() + get("/krav") { + val roller = arrangorRolleService.getRoller(getPid()) + if (roller.isEmpty()) { + return@get call.respond(HttpStatusCode.Unauthorized) + } - val krav = service.getByOrgnr(request.orgnr) + val krav = service.getByArrangorIds(roller.map { it.arrangorId }) .map { // TODO egen listemodell som er generell på tvers av beregningstype? toRefusjonKravOppsummering(it) @@ -109,11 +111,6 @@ private fun toRefusjonKravOppsummering(krav: RefusjonskravDto) = when (val bereg } } -@Serializable -data class GetRefusjonskravRequest( - val orgnr: List, -) - @Serializable @SerialName("AFT") data class RefusjonKravAft( diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonService.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonService.kt index 0063982ac4..c08978ab33 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonService.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonService.kt @@ -10,7 +10,6 @@ import no.nav.mulighetsrommet.api.repositories.TiltaksgjennomforingRepository import no.nav.mulighetsrommet.database.Database import no.nav.mulighetsrommet.domain.Tiltakskode import no.nav.mulighetsrommet.domain.dto.DeltakerStatus -import no.nav.mulighetsrommet.domain.dto.Organisasjonsnummer import java.time.LocalDate import java.time.LocalDateTime import java.time.temporal.TemporalAdjusters @@ -22,8 +21,8 @@ class RefusjonService( private val refusjonskravRepository: RefusjonskravRepository, private val db: Database, ) { - fun getByOrgnr(orgnr: List): List { - return refusjonskravRepository.getByOrgnr(orgnr) + fun getByArrangorIds(ids: List): List { + return refusjonskravRepository.getByArrangorIds(ids) } fun getById(id: UUID): RefusjonskravDto? { diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonskravRepository.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonskravRepository.kt index 3ac106d3d6..a7d431e210 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonskravRepository.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonskravRepository.kt @@ -8,7 +8,6 @@ import no.nav.mulighetsrommet.api.okonomi.models.RefusjonKravBeregning import no.nav.mulighetsrommet.api.okonomi.models.RefusjonKravBeregningAft import no.nav.mulighetsrommet.api.okonomi.models.RefusjonskravDto import no.nav.mulighetsrommet.database.Database -import no.nav.mulighetsrommet.domain.dto.Organisasjonsnummer import org.intellij.lang.annotations.Language import java.util.* @@ -124,17 +123,17 @@ class RefusjonskravRepository(private val db: Database) { .runWithSession(tx) } - fun getByOrgnr(orgnr: List) = db.transaction { getByOrgnr(orgnr, it) } + fun getByArrangorIds(ids: List) = db.transaction { getByArrangorIds(ids, it) } - fun getByOrgnr(orgnr: List, tx: Session): List { + fun getByArrangorIds(ids: List, tx: Session): List { @Language("PostgreSQL") val query = """ select * from refusjonskrav_aft_view - where arrangor_organisasjonsnummer = any(:orgnr) + where arrangor_id = any(:ids) """.trimIndent() return tx.run( - queryOf(query, mapOf("orgnr" to db.createTextArray(orgnr.map { it.value }))) + queryOf(query, mapOf("ids" to db.createUuidArray(ids))) .map { it.toRefusjonsKravAft() } .asList, ) diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/plugins/DependencyInjection.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/plugins/DependencyInjection.kt index 42bb41c218..4c9914ed55 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/plugins/DependencyInjection.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/plugins/DependencyInjection.kt @@ -13,6 +13,7 @@ import no.nav.mulighetsrommet.api.SlackConfig import no.nav.mulighetsrommet.api.TaskConfig import no.nav.mulighetsrommet.api.avtaler.AvtaleValidator import no.nav.mulighetsrommet.api.avtaler.OpsjonLoggValidator +import no.nav.mulighetsrommet.api.clients.altinn.AltinnClient import no.nav.mulighetsrommet.api.clients.amtDeltaker.AmtDeltakerClient import no.nav.mulighetsrommet.api.clients.arenaadapter.ArenaAdapterClient import no.nav.mulighetsrommet.api.clients.brreg.BrregClient @@ -56,6 +57,8 @@ import no.nav.mulighetsrommet.slack.SlackNotifierImpl import no.nav.mulighetsrommet.tasks.DbSchedulerKotlinSerializer import no.nav.mulighetsrommet.tokenprovider.AccessType import no.nav.mulighetsrommet.tokenprovider.CachedTokenProvider +import no.nav.mulighetsrommet.tokenprovider.M2MTokenProvider +import no.nav.mulighetsrommet.tokenprovider.createMaskinportenM2mTokenClient import no.nav.mulighetsrommet.unleash.UnleashService import no.nav.mulighetsrommet.unleash.strategies.ByEnhetStrategy import no.nav.mulighetsrommet.unleash.strategies.ByNavIdentStrategy @@ -172,11 +175,17 @@ private fun repositories() = module { single { TilsagnRepository(get()) } single { RefusjonskravRepository(get()) } single { UtdanningRepository(get()) } + single { ArrangorAnsattRepository(get()) } } private fun services(appConfig: AppConfig) = module { val azure = appConfig.auth.azure val cachedTokenProvider = CachedTokenProvider.init(azure.audience, azure.tokenEndpointUrl) + val maskinportenTokenProvider = createMaskinportenM2mTokenClient( + appConfig.auth.maskinporten.audience, + appConfig.auth.maskinporten.tokenEndpointUrl, + appConfig.auth.maskinporten.issuer, + ) single { VeilarboppfolgingClient( @@ -261,6 +270,17 @@ private fun services(appConfig: AppConfig) = module { ) } single { UtdanningClient(config = appConfig.utdanning) } + single { + AltinnClient( + baseUrl = appConfig.altinn.url, + altinnApiKey = appConfig.altinn.apiKey, + clientEngine = appConfig.engine, + tokenProvider = maskinportenTokenProvider?.withScope( + scope = appConfig.altinn.scope, + targetAudience = appConfig.altinn.url, + ) ?: M2MTokenProvider { "dummy" }, // TODO: Remove when prod + ) + } single { EndringshistorikkService(get()) } single { ArenaAdapterService( @@ -336,6 +356,7 @@ private fun services(appConfig: AppConfig) = module { single { OpsjonLoggService(get(), get(), get(), get(), get()) } single { LagretFilterService(get()) } single { TilsagnService(get(), get(), get(), get()) } + single { ArrangorRolleService(get(), get(), get(), get()) } } private fun tasks(config: TaskConfig) = module { diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/repositories/ArrangorAnsattRepository.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/repositories/ArrangorAnsattRepository.kt new file mode 100644 index 0000000000..bb077d3f2e --- /dev/null +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/repositories/ArrangorAnsattRepository.kt @@ -0,0 +1,155 @@ +package no.nav.mulighetsrommet.api.repositories + +import kotliquery.Row +import kotliquery.Session +import kotliquery.queryOf +import no.nav.mulighetsrommet.api.services.ArrangorRolle +import no.nav.mulighetsrommet.api.services.ArrangorRolleType +import no.nav.mulighetsrommet.database.Database +import no.nav.mulighetsrommet.domain.dto.NorskIdent +import org.intellij.lang.annotations.Language +import java.util.* + +class ArrangorAnsattRepository(private val db: Database) { + fun upsertRoller(ansattId: UUID, roller: List) = + db.transaction { tx -> upsertRoller(ansattId, roller, tx) } + + fun upsertRoller(ansattId: UUID, roller: List, tx: Session) { + @Language("PostgreSQL") + val upsertRolle = """ + insert into arrangor_ansatt_rolle ( + arrangor_ansatt_id, + arrangor_id, + rolle, + expiry + ) values ( + :arrangor_ansatt_id::uuid, + :arrangor_id::uuid, + :rolle::arrangor_rolle, + :expiry + ) on conflict (arrangor_ansatt_id, arrangor_id) do update set + rolle = excluded.rolle, + expiry = excluded.expiry + """.trimIndent() + + @Language("PostgreSQL") + val deleteRoller = """ + delete from arrangor_ansatt_rolle + where arrangor_ansatt_id = ?::uuid and not (arrangor_id = any (?)) + """.trimIndent() + roller.forEach { + tx.run(queryOf(upsertRolle, it.toSqlParameters(ansattId)).asExecute) + } + + tx.run(queryOf(deleteRoller, ansattId, db.createUuidArray(roller.map { it.arrangorId })).asExecute) + } + + fun upsertAnsatt(ansatt: ArrangorAnsatt) = + db.transaction { tx -> upsertAnsatt(ansatt, tx) } + + fun upsertAnsatt(ansatt: ArrangorAnsatt, tx: Session) { + @Language("PostgreSQL") + val query = """ + insert into arrangor_ansatt ( + id, + norsk_ident + ) values ( + :id::uuid, + :norsk_ident + ) on conflict (id) do update set + norsk_ident = excluded.norsk_ident + returning * + """.trimIndent() + + tx.run( + queryOf(query, ansatt.toSqlParameters()).asExecute, + ) + } + + fun getAnsatt(norskIdent: NorskIdent, tx: Session): ArrangorAnsatt? { + @Language("PostgreSQL") + val query = """ + select + id, + norsk_ident + from arrangor_ansatt + where norsk_ident = ? + """.trimIndent() + + return queryOf(query, norskIdent.value) + .map { + ArrangorAnsatt( + id = it.uuid("id"), + norskIdent = NorskIdent(it.string("norsk_ident")), + ) + } + .asSingle + .let { tx.run(it) } + } + + fun getAnsatte(): List = + db.transaction { tx -> getAnsatte(tx) } + + fun getAnsatte(tx: Session): List { + @Language("PostgreSQL") + val query = """ + select + id, + norsk_ident + from arrangor_ansatt + """.trimIndent() + + return queryOf(query) + .map { + ArrangorAnsatt( + id = it.uuid("id"), + norskIdent = NorskIdent(it.string("norsk_ident")), + ) + } + .asList + .let { tx.run(it) } + } + + fun getRoller(norskIdent: NorskIdent): List { + @Language("PostgreSQL") + val query = """ + select + rolle, + expiry, + arrangor_id + from arrangor_ansatt_rolle + inner join arrangor_ansatt on arrangor_ansatt.id = arrangor_ansatt_rolle.arrangor_ansatt_id + where arrangor_ansatt.norsk_ident = ? + """.trimIndent() + + return queryOf(query, norskIdent.value) + .map { it.toRolle() } + .asList + .let { db.run(it) } + } + + private fun ArrangorRolle.toSqlParameters(ansattId: UUID) = mapOf( + "arrangor_ansatt_id" to ansattId, + "arrangor_id" to arrangorId, + "rolle" to rolle.name, + "expiry" to expiry, + ) + + private fun ArrangorAnsatt.toSqlParameters() = mapOf( + "id" to id, + "norsk_ident" to norskIdent.value, + ) + + fun Row.toRolle(): ArrangorRolle { + return ArrangorRolle( + arrangorId = uuid("arrangor_id"), + rolle = ArrangorRolleType.valueOf(string("rolle")), + expiry = localDateTime("expiry"), + ) + } +} + +data class ArrangorAnsatt( + val id: UUID, + val norskIdent: NorskIdent, +) diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/ArrangorRolleService.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/ArrangorRolleService.kt new file mode 100644 index 0000000000..3e42f6da98 --- /dev/null +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/ArrangorRolleService.kt @@ -0,0 +1,82 @@ +package no.nav.mulighetsrommet.api.services + +import arrow.core.getOrElse +import kotliquery.Session +import no.nav.mulighetsrommet.api.clients.altinn.AltinnClient +import no.nav.mulighetsrommet.api.clients.altinn.AltinnRessurs +import no.nav.mulighetsrommet.api.repositories.ArrangorAnsatt +import no.nav.mulighetsrommet.api.repositories.ArrangorAnsattRepository +import no.nav.mulighetsrommet.database.Database +import no.nav.mulighetsrommet.domain.dto.NorskIdent +import org.threeten.bp.Duration +import java.time.LocalDateTime +import java.util.* + +class ArrangorRolleService( + private val altinnClient: AltinnClient, + private val arrangorService: ArrangorService, + private val db: Database, + private val arrangorAnsattRepository: ArrangorAnsattRepository, +) { + private val rolleExpiryDuration = Duration.ofDays(1) + + suspend fun getRoller(norskIdent: NorskIdent): List { + val roller = arrangorAnsattRepository.getRoller(norskIdent) + return if (roller.isEmpty() || roller.any { it.expiry.isBefore(LocalDateTime.now()) }) { + syncRoller(norskIdent) + } else { + roller + } + } + + private suspend fun syncRoller(norskIdent: NorskIdent): List { + return db.transactionSuspend { tx -> + val ansatt = arrangorAnsattRepository.getAnsatt(norskIdent, tx) + ?: run { + val nyAnsatt = ArrangorAnsatt( + id = UUID.randomUUID(), + norskIdent = norskIdent, + ) + arrangorAnsattRepository.upsertAnsatt(nyAnsatt, tx) + nyAnsatt + } + + syncRoller(ansatt, tx) + } + } + + private suspend fun syncRoller(ansatt: ArrangorAnsatt, tx: Session): List { + return altinnClient.hentRoller(ansatt.norskIdent) + .map { altinnRolle -> + val arrangor = arrangorService.getOrSyncArrangorFromBrreg(altinnRolle.organisasjonsnummer.value) + .getOrElse { throw Exception("feil ved henting av org fra brreg") } + + ArrangorRolle( + arrangorId = arrangor.id, + expiry = LocalDateTime.now().plusSeconds(rolleExpiryDuration.seconds), + rolle = ArrangorRolleType.fromAltinnRessurs(altinnRolle.ressurs), + ) + } + .also { + arrangorAnsattRepository.upsertRoller(ansatt.id, it, tx) + } + } +} + +enum class ArrangorRolleType { + TILTAK_ARRANGOR_REFUSJON, + ; + + companion object { + fun fromAltinnRessurs(ressurs: AltinnRessurs) = + when (ressurs) { + AltinnRessurs.TILTAK_ARRANGOR_REFUSJON -> TILTAK_ARRANGOR_REFUSJON + } + } +} + +data class ArrangorRolle( + val arrangorId: UUID, + val rolle: ArrangorRolleType, + val expiry: LocalDateTime, +) diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/NavAnsattService.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/NavAnsattService.kt index f17e33aa9b..f24c5bf391 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/NavAnsattService.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/services/NavAnsattService.kt @@ -1,7 +1,5 @@ package no.nav.mulighetsrommet.api.services -import io.ktor.client.statement.* -import io.ktor.http.* import no.nav.mulighetsrommet.api.AdGruppeNavAnsattRolleMapping import no.nav.mulighetsrommet.api.domain.dbo.NavAnsattDbo import no.nav.mulighetsrommet.api.domain.dto.NavAnsattDto diff --git a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/tasks/SynchronizeNavAnsatte.kt b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/tasks/SynchronizeNavAnsatte.kt index 72cf2d60b7..4b5a6bbe90 100644 --- a/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/tasks/SynchronizeNavAnsatte.kt +++ b/mulighetsrommet-api/src/main/kotlin/no/nav/mulighetsrommet/api/tasks/SynchronizeNavAnsatte.kt @@ -21,7 +21,7 @@ import kotlin.jvm.optionals.getOrNull class SynchronizeNavAnsatte( config: Config, - private val navAnsattService: NavAnsattSyncService, + private val navAnsattSyncService: NavAnsattSyncService, slack: SlackNotifier, database: Database, ) { @@ -63,7 +63,7 @@ class SynchronizeNavAnsatte( runBlocking { val today = LocalDate.now() val deletionDate = today.plus(config.deleteNavAnsattGracePeriod) - navAnsattService.synchronizeNavAnsatte(today, deletionDate) + navAnsattSyncService.synchronizeNavAnsatte(today, deletionDate) } } diff --git a/mulighetsrommet-api/src/main/resources/application-dev.yaml b/mulighetsrommet-api/src/main/resources/application-dev.yaml index 3a520ecb28..78aa5bf3a4 100644 --- a/mulighetsrommet-api/src/main/resources/application-dev.yaml +++ b/mulighetsrommet-api/src/main/resources/application-dev.yaml @@ -84,6 +84,11 @@ app: jwksUri: ${TOKEN_X_JWKS_URI} audience: ${TOKEN_X_CLIENT_ID} tokenEndpointUrl: ${TOKEN_X_WELL_KNOWN_URL} + maskinporten: + issuer: ${MASKINPORTEN_ISSUER} + jwksUri: ${MASKINPORTEN_JWKS_URI} + audience: ${MASKINPORTEN_CLIENT_ID} + tokenEndpointUrl: ${MASKINPORTEN_TOKEN_ENDPOINT} sanity: dataset: ${SANITY_DATASET} @@ -140,6 +145,11 @@ app: brreg: baseUrl: https://data.brreg.no/enhetsregisteret/api + altinn: + url: https://platform.tt02.altinn.no + apiKey: ${ALTINN_API_KEY} + scope: ${MASKINPORTEN_SCOPES} + tasks: synchronizeNorgEnheter: delayOfMinutes: 360 # Hver 6. time diff --git a/mulighetsrommet-api/src/main/resources/application-local.yaml b/mulighetsrommet-api/src/main/resources/application-local.yaml index 2c2ace579a..b0fd0a69e4 100644 --- a/mulighetsrommet-api/src/main/resources/application-local.yaml +++ b/mulighetsrommet-api/src/main/resources/application-local.yaml @@ -89,6 +89,11 @@ app: jwksUri: http://localhost:8081/tokenx/jwks audience: mulighetsrommet-api tokenEndpointUrl: http://localhost:8081/tokenx/token + maskinporten: + issuer: http://localhost:8081/maskinporten + jwksUri: http://localhost:8081/maskinporten/jwks + audience: mulighetsrommet-api + tokenEndpointUrl: http://localhost:8081/maskinporten/token sanity: dataset: test @@ -147,6 +152,11 @@ app: brreg: baseUrl: https://data.brreg.no/enhetsregisteret/api + altinn: + url: http://localhost:8090/altinn + apiKey: dummy + scope: default + tasks: synchronizeNorgEnheter: disabled: true diff --git a/mulighetsrommet-api/src/main/resources/application-prod.yaml b/mulighetsrommet-api/src/main/resources/application-prod.yaml index e9fe0b3817..b675188def 100644 --- a/mulighetsrommet-api/src/main/resources/application-prod.yaml +++ b/mulighetsrommet-api/src/main/resources/application-prod.yaml @@ -81,6 +81,12 @@ app: jwksUri: ${TOKEN_X_JWKS_URI} audience: ${TOKEN_X_CLIENT_ID} tokenEndpointUrl: ${TOKEN_X_WELL_KNOWN_URL} + maskinporten: + issuer: ${MASKINPORTEN_ISSUER:dummyremovewhenprod} + jwksUri: ${MASKINPORTEN_X_JWKS_URI:dummyremovewhenprod} + audience: ${MASKINPORTEN_CLIENT_ID:dummyremovewhenprod} + tokenEndpointUrl: ${MASKINPORTEN_TOKEN_ENDPOINT:dummyremovewhenprod} + sanity: dataset: ${SANITY_DATASET} @@ -137,6 +143,11 @@ app: brreg: baseUrl: https://data.brreg.no/enhetsregisteret/api + altinn: + url: dummy + apiKey: ${ALTINN_API_KEY:dummy} + scope: ${MASKINPORTEN_SCOPES:dummy} + tasks: synchronizeNorgEnheter: delayOfMinutes: 360 # Hver 6. time diff --git a/mulighetsrommet-api/src/main/resources/db/migration/V190__arrangor_ansatt.sql b/mulighetsrommet-api/src/main/resources/db/migration/V190__arrangor_ansatt.sql new file mode 100644 index 0000000000..5fa9d59317 --- /dev/null +++ b/mulighetsrommet-api/src/main/resources/db/migration/V190__arrangor_ansatt.sql @@ -0,0 +1,17 @@ +create type arrangor_rolle as enum ('TILTAK_ARRANGOR_REFUSJON'); + +create table arrangor_ansatt ( + id uuid primary key, + norsk_ident text unique not null, + fornavn text, + etternavn text +); + +CREATE TABLE arrangor_ansatt_rolle +( + arrangor_ansatt_id uuid references arrangor_ansatt (id) NOT NULL, + arrangor_id uuid references arrangor (id) NOT NULL, + rolle arrangor_rolle NOT NULL, + expiry timestamp not null, + primary key (arrangor_id, arrangor_ansatt_id) +); diff --git a/mulighetsrommet-api/src/main/resources/web/openapi.yaml b/mulighetsrommet-api/src/main/resources/web/openapi.yaml index 54d18d4280..867bcc840c 100644 --- a/mulighetsrommet-api/src/main/resources/web/openapi.yaml +++ b/mulighetsrommet-api/src/main/resources/web/openapi.yaml @@ -1797,15 +1797,10 @@ paths: $ref: "#/components/schemas/AFTSats" /api/v1/intern/refusjon/krav: - post: + get: tags: - Refusjonskrav operationId: getRefusjonskrav - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/GetRefusjonskravRequest" responses: 200: description: Hent refusjonskrav til arrangør diff --git a/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/ApplicationTestConfig.kt b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/ApplicationTestConfig.kt index ef556604be..a5b26766eb 100644 --- a/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/ApplicationTestConfig.kt +++ b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/ApplicationTestConfig.kt @@ -2,6 +2,7 @@ package no.nav.mulighetsrommet.api import io.ktor.server.application.* import io.ktor.server.testing.* +import no.nav.mulighetsrommet.api.clients.altinn.AltinnClient import no.nav.mulighetsrommet.api.clients.brreg.BrregClient import no.nav.mulighetsrommet.api.clients.sanity.SanityClient import no.nav.mulighetsrommet.api.clients.utdanning.UtdanningClient @@ -106,6 +107,11 @@ fun createTestApplicationConfig() = AppConfig( utdanning = UtdanningClient.Config( baseurl = "", ), + altinn = AltinnClient.Config( + url = "altinn-acl", + scope = "default", + apiKey = "apiKey", + ), ) fun createKafkaConfig(): KafkaConfig = KafkaConfig( @@ -155,4 +161,10 @@ fun createAuthConfig( jwksUri = oauth?.jwksUrl(issuer)?.toUri()?.toString() ?: "http://localhost", tokenEndpointUrl = oauth?.tokenEndpointUrl(issuer)?.toString() ?: "http://localhost", ), + maskinporten = AuthProvider( + issuer = oauth?.issuerUrl(issuer)?.toString() ?: issuer, + audience = audience, + jwksUri = oauth?.jwksUrl(issuer)?.toUri()?.toString() ?: "http://localhost", + tokenEndpointUrl = oauth?.tokenEndpointUrl(issuer)?.toString() ?: "http://localhost", + ), ) diff --git a/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/clients/altinn/AltinnClientTest.kt b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/clients/altinn/AltinnClientTest.kt new file mode 100644 index 0000000000..9f82f95088 --- /dev/null +++ b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/clients/altinn/AltinnClientTest.kt @@ -0,0 +1,61 @@ +package no.nav.mulighetsrommet.api.clients.altinn + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldHaveSize +import no.nav.mulighetsrommet.domain.dto.NorskIdent +import no.nav.mulighetsrommet.ktor.createMockEngine +import no.nav.mulighetsrommet.ktor.respondJson + +class AltinnClientTest : FunSpec({ + val altinnResponse = """ + [ + { + "name": "LAGSPORT PLUTSELIG", + "organizationNumber": null, + "type": "Person", + "authorizedResources": [], + "subunits": [] + }, + { + "name": "NONFIGURATIV KOMFORTABEL HUND DA", + "type": "Organization", + "organizationNumber": "999987004", + "authorizedResources": [], + "subunits": [ + { + "name": "UEMOSJONELL KREATIV TIGER AS", + "type": "Organization", + "organizationNumber": "211267232", + "authorizedResources": ["tiltak-arrangor-refusjon"], + "subunits": [] + } + ] + }, + { + "name": "FRYKTLØS OPPSTEMT STRUTS LTD", + "type": "Organization", + "organizationNumber": "312899485", + "authorizedResources": ["tiltak-arrangor-refusjon"], + "subunits": [] + } + ] + """.trimIndent() + + test("hentAlleOrganisasjoner 1 tilgang - kun et kall til Altinn") { + val altinnClient = AltinnClient( + "https://altinn.no", + altinnApiKey = "api-key", + tokenProvider = { "token" }, + createMockEngine( + "/accessmanagement/api/v1/resourceowner/authorizedparties?includeAltinn2=true" to { + respondJson(altinnResponse) + }, + ), + ) + + val norskIdent = NorskIdent("12345678901") + val organisasjoner = altinnClient.hentRoller(norskIdent) + + organisasjoner shouldHaveSize 2 + } +}) diff --git a/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonServiceTest.kt b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonServiceTest.kt index b125562f6a..09bd9c486e 100644 --- a/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonServiceTest.kt +++ b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/okonomi/refusjon/RefusjonServiceTest.kt @@ -18,7 +18,6 @@ import no.nav.mulighetsrommet.api.repositories.TiltaksgjennomforingRepository import no.nav.mulighetsrommet.database.kotest.extensions.FlywayDatabaseTestListener import no.nav.mulighetsrommet.database.kotest.extensions.truncateAll import no.nav.mulighetsrommet.domain.dto.DeltakerStatus -import no.nav.mulighetsrommet.domain.dto.Organisasjonsnummer import java.time.LocalDate class RefusjonServiceTest : FunSpec({ @@ -48,8 +47,8 @@ class RefusjonServiceTest : FunSpec({ service.genererRefusjonskravForMonth(LocalDate.of(2024, 1, 1)) - val allKrav = service.getByOrgnr( - listOf(Organisasjonsnummer(ArrangorFixtures.underenhet1.organisasjonsnummer)), + val allKrav = service.getByArrangorIds( + listOf(ArrangorFixtures.underenhet1.id), ) allKrav.size shouldBe 1 @@ -109,7 +108,7 @@ class RefusjonServiceTest : FunSpec({ service.genererRefusjonskravForMonth(LocalDate.of(2024, 1, 1)) val krav = service - .getByOrgnr(listOf(Organisasjonsnummer(ArrangorFixtures.underenhet1.organisasjonsnummer))) + .getByArrangorIds(listOf(ArrangorFixtures.underenhet1.id)) .first() krav.beregning.input.shouldBeTypeOf().should { @@ -166,7 +165,7 @@ class RefusjonServiceTest : FunSpec({ service.genererRefusjonskravForMonth(LocalDate.of(2024, 1, 1)) val krav = service - .getByOrgnr(listOf(Organisasjonsnummer(ArrangorFixtures.underenhet1.organisasjonsnummer))) + .getByArrangorIds(listOf(ArrangorFixtures.underenhet1.id)) .first() krav.beregning.output.shouldBeTypeOf().should { diff --git a/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/repositories/ArrangorAnsattRepositoryTest.kt b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/repositories/ArrangorAnsattRepositoryTest.kt new file mode 100644 index 0000000000..8ddc2f3579 --- /dev/null +++ b/mulighetsrommet-api/src/test/kotlin/no/nav/mulighetsrommet/api/repositories/ArrangorAnsattRepositoryTest.kt @@ -0,0 +1,54 @@ +package no.nav.mulighetsrommet.api.repositories + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import no.nav.mulighetsrommet.api.createDatabaseTestConfig +import no.nav.mulighetsrommet.api.fixtures.ArrangorFixtures.underenhet1 +import no.nav.mulighetsrommet.api.fixtures.ArrangorFixtures.underenhet2 +import no.nav.mulighetsrommet.api.services.ArrangorRolle +import no.nav.mulighetsrommet.api.services.ArrangorRolleType +import no.nav.mulighetsrommet.database.kotest.extensions.FlywayDatabaseTestListener +import no.nav.mulighetsrommet.domain.dto.NorskIdent +import java.time.LocalDateTime +import java.util.* + +class ArrangorAnsattRepositoryTest : FunSpec({ + val database = extension(FlywayDatabaseTestListener(createDatabaseTestConfig())) + + val ansatt1 = ArrangorAnsatt( + id = UUID.randomUUID(), + norskIdent = NorskIdent("12345678901"), + ) + val ansatt2 = ArrangorAnsatt( + id = UUID.randomUUID(), + norskIdent = NorskIdent("22345010203"), + ) + val rolle1 = ArrangorRolle( + arrangorId = underenhet1.id, + rolle = ArrangorRolleType.TILTAK_ARRANGOR_REFUSJON, + expiry = LocalDateTime.of(2024, 1, 1, 0, 0), + ) + val rolle2 = ArrangorRolle( + arrangorId = underenhet2.id, + rolle = ArrangorRolleType.TILTAK_ARRANGOR_REFUSJON, + expiry = LocalDateTime.of(2024, 1, 1, 0, 0), + ) + + test("CRUD") { + val arrangorRepository = ArrangorRepository(database.db) + val ansatteRepository = ArrangorAnsattRepository(database.db) + arrangorRepository.upsert(underenhet1) + arrangorRepository.upsert(underenhet2) + + ansatteRepository.upsertAnsatt(ansatt1) + ansatteRepository.upsertAnsatt(ansatt2) + + ansatteRepository.upsertRoller(ansatt1.id, listOf(rolle1)) + ansatteRepository.upsertRoller(ansatt2.id, listOf(rolle2)) + + ansatteRepository.getRoller(ansatt1.norskIdent) shouldBe listOf(rolle1) + ansatteRepository.getRoller(ansatt2.norskIdent) shouldBe listOf(rolle2) + ansatteRepository.getAnsatte().map { it.id } shouldContainExactlyInAnyOrder listOf(ansatt1.id, ansatt2.id) + } +}) diff --git a/mulighetsrommet-api/wiremock/mappings/altinn.json b/mulighetsrommet-api/wiremock/mappings/altinn.json new file mode 100644 index 0000000000..987cfc9053 --- /dev/null +++ b/mulighetsrommet-api/wiremock/mappings/altinn.json @@ -0,0 +1,26 @@ +{ + "request": { + "method": "POST", + "urlPath": "/altinn/accessmanagement/api/v1/resourceowner/authorizedparties", + "queryParameters": { + "includeAltinn2": { + "equalTo": "true" + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "name": "FRYKTLØS OPPSTEMT STRUTS LTD", + "type": "Organization", + "organizationNumber": "971808616", + "authorizedResources": ["tiltak-arrangor-refusjon"], + "subunits": [] + } + ], + "headers": { + "Content-Type": "application/json" + } + } +}