Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Legger til autorisering av rekrutteringstreff-endepunkt #10

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/rekrutteringstreff-api/.nais/dev-gcp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ gcp_disk_autoresize: false
postgres: POSTGRES_15
adGruppeUtvikler: "a1749d9a-52e0-4116-bb9f-935c38f6c74a"
openai_azure_host: "arbeidsmarked-dev.openai.azure.com"

adGruppeArbeidsgiverrettet: "52bc2af7-38d1-468b-b68d-0f3a4de45af2"
5 changes: 5 additions & 0 deletions apps/rekrutteringstreff-api/.nais/nais.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ spec:
cascadingDelete: true
envFrom:
- secret: openai-toi-rekrutteringsbistand-stilling
env:
- name: REKRUTTERINGSBISTAND_ARBEIDSGIVERRETTET
value: {{adGruppeArbeidsgiverrettet}}
- name: REKRUTTERINGSBISTAND_UTVIKLER
value: {{adGruppeUtvikler}}
2 changes: 2 additions & 0 deletions apps/rekrutteringstreff-api/.nais/prod-gcp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ gcp_db_tier: db-custom-1-3840
gcp_disk_autoresize: true
postgres: POSTGRES_15
openai_azure_host: "todo"
adGruppeArbeidsgiverrettet: "46c8e3b2-0469-4740-983f-d8cd2b6e4fee"
adGruppeUtvikler: "41080368-439f-4128-858a-afbef876431e"
11 changes: 8 additions & 3 deletions apps/rekrutteringstreff-api/src/main/kotlin/no/nav/toi/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ import java.time.Instant
import java.time.ZoneId.of
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit.MILLIS
import java.util.UUID
import javax.sql.DataSource

class App(
private val port: Int,
private val authConfigs: List<AuthenticationConfiguration>,
private val dataSource: DataSource
private val dataSource: DataSource,
private val arbeidsgiverrettet: UUID,
private val utvikler: UUID
) {
private lateinit var javalin: Javalin

Expand All @@ -39,7 +42,7 @@ class App(
log.info("Javalin opprettet")
}
javalin.handleHealth()
javalin.leggTilAutensieringPåRekrutteringstreffEndepunkt(authConfigs)
javalin.leggTilAutensieringPåRekrutteringstreffEndepunkt(authConfigs, RolleUuidSpesifikasjon(arbeidsgiverrettet, utvikler))
javalin.handleRekrutteringstreff(RekrutteringstreffRepository(dataSource))
javalin.start(port)
}
Expand Down Expand Up @@ -76,7 +79,9 @@ fun main() {
else
null
),
dataSource
dataSource,
System.getenv("REKRUTTERINGSBISTAND_ARBEIDSGIVERRETTET").let(UUID::fromString),
System.getenv("REKRUTTERINGSBISTAND_UTVIKLER").let(UUID::fromString)
).start()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import com.auth0.jwt.interfaces.DecodedJWT
import com.auth0.jwt.interfaces.RSAKeyProvider
import io.javalin.Javalin
import io.javalin.http.Context
import io.javalin.http.ForbiddenResponse
import io.javalin.http.InternalServerErrorResponse
import io.javalin.http.UnauthorizedResponse
import org.eclipse.jetty.http.HttpHeader
import java.net.URI
import java.security.interfaces.RSAPublicKey
import java.util.*
import java.util.concurrent.TimeUnit

private const val NAV_IDENT_CLAIM = "NAVident"
Expand All @@ -33,22 +36,35 @@ class AuthenticationConfiguration(
}

class AuthenticatedUser private constructor(
private val navIdent: String
private val navIdent: String,
private val roller: Set<Rolle>,
private val jwt: String
) {
companion object {
fun fromJwt(jwt: DecodedJWT): AuthenticatedUser {
val navIdent = jwt.getClaim(NAV_IDENT_CLAIM).asString()
?: throw UnauthorizedResponse("Missing claim: $NAV_IDENT_CLAIM")
return AuthenticatedUser(navIdent)
fun verifiserAutorisasjon(vararg gyldigeRoller: Rolle) {
if(!erEnAvRollene(*gyldigeRoller)) {
throw ForbiddenResponse()
}
}

fun erEnAvRollene(vararg gyldigeRoller: Rolle) = roller.any { it in (gyldigeRoller.toList() + Rolle.UTVIKLER) }

companion object {
fun fromJwt(jwt: DecodedJWT, rolleUuidSpesifikasjon: RolleUuidSpesifikasjon) =
AuthenticatedUser(
navIdent = jwt.getClaim(NAV_IDENT_CLAIM).asString(),
roller = jwt.getClaim("groups")
.asList(UUID::class.java)
.let(rolleUuidSpesifikasjon::rollerForUuider),
jwt = jwt.token,
)
fun Context.extractNavIdent(): String =
attribute<AuthenticatedUser>("authenticatedUser")?.navIdent ?: throw UnauthorizedResponse("Not authenticated")
}
}


fun Javalin.leggTilAutensieringPåRekrutteringstreffEndepunkt(authConfigs: List<AuthenticationConfiguration>): Javalin {
fun Javalin.leggTilAutensieringPåRekrutteringstreffEndepunkt(authConfigs: List<AuthenticationConfiguration>,
rolleUuidSpesifikasjon: RolleUuidSpesifikasjon): Javalin {
log.info("Starter autentiseringoppsett")
val verifiers = authConfigs.map { it.jwtVerifier() }
before { ctx ->
Expand All @@ -57,7 +73,7 @@ fun Javalin.leggTilAutensieringPåRekrutteringstreffEndepunkt(authConfigs: List<
?.removePrefix("Bearer ")
?.trim() ?: throw UnauthorizedResponse("Missing token")
val decoded = verifyJwt(verifiers, token)
ctx.attribute("authenticatedUser", AuthenticatedUser.fromJwt(decoded))
ctx.attribute("authenticatedUser", AuthenticatedUser.fromJwt(decoded, rolleUuidSpesifikasjon))
}
}
return this
Expand Down Expand Up @@ -91,3 +107,9 @@ private fun algorithm(jwksUri: String): Algorithm {
override fun getPrivateKeyId() = throw UnsupportedOperationException()
})
}

fun Context.authenticatedUser() = attribute<AuthenticatedUser>("authenticatedUser")
?: run {
log.error("No authenticated user found!")
throw InternalServerErrorResponse()
}
26 changes: 26 additions & 0 deletions apps/rekrutteringstreff-api/src/main/kotlin/no/nav/toi/Rolle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package no.nav.toi

import java.util.*

enum class Rolle {
ARBEIDSGIVER_RETTET,
UTVIKLER
}

/*
Holder på UUID-ene som brukes for å identifisere roller i Azure AD.
Det er ulik spesifikasjon for dev og prod.
*/
data class RolleUuidSpesifikasjon(
private val arbeidsgiverrettet: UUID,
private val utvikler: UUID,
) {
private fun rolleForUuid(uuid: UUID) = when (uuid) {
arbeidsgiverrettet -> Rolle.ARBEIDSGIVER_RETTET
utvikler -> Rolle.UTVIKLER
else -> { log.warn("Ukjent rolle-UUID: $uuid"); null }
}

fun rollerForUuider(uuider: Collection<UUID>) = uuider.mapNotNull(::rolleForUuid).toSet()
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import io.javalin.http.NotFoundResponse
import io.javalin.http.bodyAsClass
import io.javalin.openapi.*
import no.nav.toi.AuthenticatedUser.Companion.extractNavIdent
import no.nav.toi.Rolle
import no.nav.toi.authenticatedUser
import no.nav.toi.noClassLogger
import no.nav.toi.rekrutteringstreff.eier.handleEiere
import no.nav.toi.rekrutteringstreff.rekrutteringstreff.OpenAiClient
Expand Down Expand Up @@ -38,6 +40,7 @@ const val endepunktRekrutteringstreff = "/api/rekrutteringstreff"
methods = [HttpMethod.POST]
)
private fun opprettRekrutteringstreffHandler(repo: RekrutteringstreffRepository): (Context) -> Unit = { ctx ->
ctx.authenticatedUser().verifiserAutorisasjon(Rolle.ARBEIDSGIVER_RETTET)
val inputDto = ctx.bodyAsClass<OpprettRekrutteringstreffDto>()
val internalDto = OpprettRekrutteringstreffInternalDto(
tittel = "Nytt rekrutteringstreff",
Expand Down Expand Up @@ -77,6 +80,7 @@ private fun opprettRekrutteringstreffHandler(repo: RekrutteringstreffRepository)
methods = [HttpMethod.GET]
)
private fun hentAlleRekrutteringstreffHandler(repo: RekrutteringstreffRepository): (Context) -> Unit = { ctx ->
ctx.authenticatedUser().verifiserAutorisasjon(Rolle.ARBEIDSGIVER_RETTET)
ctx.status(200).json(repo.hentAlle().map { it.tilRekrutteringstreffDTO() })
}

Expand Down Expand Up @@ -107,6 +111,7 @@ private fun hentAlleRekrutteringstreffHandler(repo: RekrutteringstreffRepository
methods = [HttpMethod.GET]
)
private fun hentRekrutteringstreffHandler(repo: RekrutteringstreffRepository): (Context) -> Unit = { ctx ->
ctx.authenticatedUser().verifiserAutorisasjon(Rolle.ARBEIDSGIVER_RETTET)
val id = TreffId(ctx.pathParam("id"))
val treff = repo.hent(id) ?: throw NotFoundResponse("Rekrutteringstreff ikke funnet")
ctx.status(200).json(treff.tilRekrutteringstreffDTO())
Expand Down Expand Up @@ -151,6 +156,7 @@ private fun hentRekrutteringstreffHandler(repo: RekrutteringstreffRepository): (
methods = [HttpMethod.PUT]
)
private fun oppdaterRekrutteringstreffHandler(repo: RekrutteringstreffRepository): (Context) -> Unit = { ctx ->
ctx.authenticatedUser().verifiserAutorisasjon(Rolle.ARBEIDSGIVER_RETTET)
val id = TreffId(ctx.pathParam("id"))
val dto = ctx.bodyAsClass<OppdaterRekrutteringstreffDto>()
val navIdent = ctx.extractNavIdent()
Expand All @@ -175,6 +181,7 @@ private fun oppdaterRekrutteringstreffHandler(repo: RekrutteringstreffRepository
methods = [HttpMethod.DELETE]
)
private fun slettRekrutteringstreffHandler(repo: RekrutteringstreffRepository): (Context) -> Unit = { ctx ->
ctx.authenticatedUser().verifiserAutorisasjon(Rolle.ARBEIDSGIVER_RETTET)
val id = TreffId(ctx.pathParam("id"))
repo.slett(id)
ctx.status(200).result("Rekrutteringstreff slettet")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.util.*

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class KubernetesHealthTest {
private val appPort = ubruktPortnr()
private val app = App(port = appPort, listOf(), TestDatabase().dataSource)
private val app = App(port = appPort, listOf(), TestDatabase().dataSource, arbeidsgiverrettet, utvikler)

@BeforeAll
fun setUp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.util.*

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SwaggerEndpointsTest {

private val appPort = ubruktPortnr()
private val app = App(port = appPort, listOf(), TestDatabase().dataSource)
private val app = App(port = appPort, listOf(), TestDatabase().dataSource, arbeidsgiverrettet, utvikler)

@BeforeAll
fun setUp() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package no.nav.toi.rekrutteringstreff.no.nav.toi.rekrutteringstreff

import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.Method
import com.github.kittinunf.fuel.core.Request
import no.nav.security.mock.oauth2.MockOAuth2Server
import no.nav.toi.*
import no.nav.toi.rekrutteringstreff.*
import no.nav.toi.ubruktPortnrFra10000.ubruktPortnr
import org.junit.jupiter.api.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import java.net.HttpURLConnection.*
import java.time.ZonedDateTime
import java.util.*

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
private class AutorisasjonsTest {

companion object {
private val appPort = ubruktPortnr()
private lateinit var gyldigRekrutteringstreff: TreffId
}

private val authServer = MockOAuth2Server()
private val authPort = 18012
private val database = TestDatabase()
private val repo = RekrutteringstreffRepository(database.dataSource)

private val app = App(
port = appPort,
authConfigs = listOf(
AuthenticationConfiguration(
issuer = "http://localhost:$authPort/default",
jwksUri = "http://localhost:$authPort/default/jwks",
audience = "rekrutteringstreff-audience"
)
),
database.dataSource,
arbeidsgiverrettet,
utvikler
)

@BeforeAll
fun setUp() {
authServer.start(port = authPort)
app.start()
}

@AfterAll
fun tearDown() {
authServer.shutdown()
app.close()
}

@BeforeEach
fun setup() {
repo.opprett(OpprettRekrutteringstreffInternalDto("Tittel","A213456", "Kontor", ZonedDateTime.now()))
gyldigRekrutteringstreff = database.hentAlleRekrutteringstreff()[0].id
}

@AfterEach
fun reset() {
database.slettAlt()
}

enum class Endepunkt(val url: () -> String, val metode: Method, val requestBygger: Request.() -> Request = { this }){
OpprettRekrutteringstreff({ "http://localhost:$appPort/api/rekrutteringstreff" },Method.POST,{
body(
"""
{
"opprettetAvNavkontorEnhetId": "NAV-kontor"
}
""".trimIndent()
)
}),
HentAlleRekrutteringstreff({ "http://localhost:$appPort/api/rekrutteringstreff" }, Method.GET),
HentRekrutteringstreff({ "http://localhost:$appPort/api/rekrutteringstreff/${gyldigRekrutteringstreff.somString}" }, Method.GET),
OppdaterRekrutteringstreff({ "http://localhost:$appPort/api/rekrutteringstreff/${gyldigRekrutteringstreff.somString}" }, Method.PUT,{
body(mapper.writeValueAsString(
OppdaterRekrutteringstreffDto(
tittel = "Oppdatert Tittel",
beskrivelse = "Oppdatert beskrivelse",
fraTid = nowOslo().minusHours(2),
tilTid = nowOslo().plusHours(3),
sted = "Oppdatert Sted"
)
))
}),
SlettRekrutteringstreff({ "http://localhost:$appPort/api/rekrutteringstreff/${gyldigRekrutteringstreff.somString}" }, Method.DELETE)
}
enum class Gruppe(val somStringListe: List<UUID>) {
ModiaGenerell(listOf(modiaGenerell)),
Arbeidsgiverrettet(listOf(arbeidsgiverrettet)),
Utvikler(listOf(utvikler))
}

private fun autorisasjonsCases() = listOf(
Arguments.of(Endepunkt.OpprettRekrutteringstreff, Gruppe.Utvikler, HTTP_CREATED),
Arguments.of(Endepunkt.OpprettRekrutteringstreff, Gruppe.Arbeidsgiverrettet, HTTP_CREATED),
Arguments.of(Endepunkt.OpprettRekrutteringstreff, Gruppe.ModiaGenerell, HTTP_FORBIDDEN),
Arguments.of(Endepunkt.HentAlleRekrutteringstreff, Gruppe.Utvikler, HTTP_OK),
Arguments.of(Endepunkt.HentAlleRekrutteringstreff, Gruppe.Arbeidsgiverrettet, HTTP_OK),
Arguments.of(Endepunkt.HentAlleRekrutteringstreff, Gruppe.ModiaGenerell, HTTP_FORBIDDEN),
Arguments.of(Endepunkt.HentRekrutteringstreff, Gruppe.Utvikler, HTTP_OK),
Arguments.of(Endepunkt.HentRekrutteringstreff, Gruppe.Arbeidsgiverrettet, HTTP_OK),
Arguments.of(Endepunkt.HentRekrutteringstreff, Gruppe.ModiaGenerell, HTTP_FORBIDDEN),
Arguments.of(Endepunkt.OppdaterRekrutteringstreff, Gruppe.Utvikler, HTTP_OK),
Arguments.of(Endepunkt.OppdaterRekrutteringstreff, Gruppe.Arbeidsgiverrettet, HTTP_OK),
Arguments.of(Endepunkt.OppdaterRekrutteringstreff, Gruppe.ModiaGenerell, HTTP_FORBIDDEN),
Arguments.of(Endepunkt.SlettRekrutteringstreff, Gruppe.Utvikler, HTTP_OK),
Arguments.of(Endepunkt.SlettRekrutteringstreff, Gruppe.Arbeidsgiverrettet, HTTP_OK),
Arguments.of(Endepunkt.SlettRekrutteringstreff, Gruppe.ModiaGenerell, HTTP_FORBIDDEN),
).stream()

@ParameterizedTest
@MethodSource("autorisasjonsCases")
fun testEndepunkt(endepunkt: Endepunkt, gruppetilhørighet: Gruppe, expectedStatus: Int) {
val byggRequest = endepunkt.requestBygger
val (_, response, result) = Fuel.request(endepunkt.metode, endepunkt.url())
.byggRequest()
.header("Authorization", "Bearer ${authServer.lagToken(authPort, groups = gruppetilhørighet.somStringListe).serialize()}")
.responseString()
assertStatuscodeEquals(expectedStatus, response, result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ class RekrutteringstreffTest {
audience = "rekrutteringstreff-audience"
)
),
database.dataSource
database.dataSource,
arbeidsgiverrettet,
utvikler
)

@BeforeAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class RekrutteringstreffEierTest {
audience = "rekrutteringstreff-audience"
)
),
database.dataSource
database.dataSource,
arbeidsgiverrettet,
utvikler
)
}

Expand Down
Loading