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

Controller for kontaktinfo #330

Merged
merged 8 commits into from
Oct 13, 2023
Merged
1 change: 1 addition & 0 deletions nais/dev-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ spec:
- application: altinn-rettigheter-proxy
namespace: arbeidsgiver
external:
- host: tt02.altinn.no
- host: api-gw-q1.oera.no
- host: ereg-services.dev-fss-pub.nais.io
1 change: 1 addition & 0 deletions nais/prod-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ spec:
- application: altinn-rettigheter-proxy
namespace: arbeidsgiver
external:
- host: www.altinn.no
- host: api-gw.oera.no
- host: ereg-services.prod-fss-pub.nais.io
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package no.nav.arbeidsgiver.min_side.kontaktinfo

import no.nav.arbeidsgiver.min_side.controller.AuthenticatedUserHolder
import no.nav.arbeidsgiver.min_side.services.ereg.EregService
import no.nav.arbeidsgiver.min_side.tilgangsstyring.AltinnRollerClient
import org.springframework.web.bind.annotation.*

@RestController
class KontaktinfoController(
private val authenticatedUserHolder: AuthenticatedUserHolder,
private val altinnRollerClient: AltinnRollerClient,
private val eregService: EregService,
private val kontaktinfoClient: KontaktinfoClient,
) {
@PostMapping("/api/kontaktinfo/v1")
fun getKontaktinfo(@RequestBody requestBody: KontaktinfoRequest): KontaktinfoResponse {
val orgnrUnderenhet = requestBody.virksomhetsnummer
val orgnrHovedenhet = eregService.hentUnderenhet(orgnrUnderenhet)
?.parentOrganizationNumber
?: return KontaktinfoResponse(null, null)

return KontaktinfoResponse(
underenhet = tilgangsstyrOgHentKontaktinfo(orgnrUnderenhet),
hovedenhet = tilgangsstyrOgHentKontaktinfo(orgnrHovedenhet),
)
}

private fun tilgangsstyrOgHentKontaktinfo(orgnr: String): Kontaktinfo? {
val tilgangHovedenhet = altinnRollerClient.harAltinnRolle(
fnr = authenticatedUserHolder.fnr,
orgnr = orgnr,
altinnRoller = ALTINN_ROLLER,
externalRoller = EXTERNAL_ROLLER,
)

return if (tilgangHovedenhet) {
kontaktinfoClient.hentKontaktinfo(orgnr)?.let {
Kontaktinfo(
eposter = it.eposter.toList(),
telefonnummer = it.telefonnumre.toList(),
)
}
} else {
null
}
}

class KontaktinfoRequest(
val virksomhetsnummer: String,
) {
init {
require(virksomhetsnummer.matches(orgnrRegex))
}

companion object {
private val orgnrRegex = Regex("^[0-9]{9}$")
}
}

@Suppress("unused") // DTO
class Kontaktinfo(
val eposter: List<String>,
val telefonnummer: List<String>,
)

@Suppress("unused") // DTO
class KontaktinfoResponse(
/* null hvis ingen tilgang */
val hovedenhet: Kontaktinfo?,

/* null hvis ingen tilgang */
val underenhet: Kontaktinfo?,
)

companion object {
private val ALTINN_ROLLER = setOf(
"HADM", // Hovedadministrator
"SIGNE", // Signerer av Samordnet registermelding
)
private val EXTERNAL_ROLLER = setOf(
"DAGL", // daglig leder
"LEDE", // styreleder
"NEST", // nestleder
"MEDL", // styremedlem
"INNH", // innehaver
"BOBE", // bobestyrer
"BEST", // bestyrende reder
"REPR", // norsk representant for utenlandske selskap
"FFØR", // forretningsfører
"KONT", // kontaktperson
)
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class EregService(
.build()

@Cacheable(EREG_CACHE_NAME)
fun hentUnderenhet(virksomhetsnummer: String?): Organisasjon? {
fun hentUnderenhet(virksomhetsnummer: String): Organisasjon? {
return try {
val json = restTemplate.getForEntity(
"/v1/organisasjon/{virksomhetsnummer}?inkluderHierarki=true",
Expand All @@ -51,7 +51,7 @@ class EregService(
}

@Cacheable(EREG_CACHE_NAME)
fun hentOverenhet(orgnummer: String?): Organisasjon? {
fun hentOverenhet(orgnummer: String): Organisasjon? {
return try {
val json = restTemplate.getForEntity(
"/v1/organisasjon/{orgnummer}",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package no.nav.arbeidsgiver.min_side.kontaktinfo

import no.nav.arbeidsgiver.min_side.kotlinAny
import no.nav.arbeidsgiver.min_side.controller.AuthenticatedUserHolder
import no.nav.arbeidsgiver.min_side.kontaktinfo.KontaktinfoController.KontaktinfoRequest
import no.nav.arbeidsgiver.min_side.models.Organisasjon
import no.nav.arbeidsgiver.min_side.services.ereg.EregService
import no.nav.arbeidsgiver.min_side.tilgangsstyring.AltinnRollerClient
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean

@SpringBootTest(classes = [KontaktinfoController::class])
@MockBean(AuthenticatedUserHolder::class)
@MockBean(AltinnRollerClient::class)
@MockBean(EregService::class)
@MockBean(KontaktinfoClient::class)
class KontaktinfoControllerAuthzTest {
@Autowired
lateinit var authenticatedUserHolder: AuthenticatedUserHolder
@Autowired
lateinit var altinnRollerClient: AltinnRollerClient
@Autowired
lateinit var eregService: EregService
@Autowired
lateinit var kontaktinfoClient: KontaktinfoClient
@Autowired
lateinit var kontaktinfoController: KontaktinfoController

private val fnr = "012345678"
private val orgnrUnderenhet = "0".repeat(9)
private val orgnrHovedenhet= "1".repeat(9)
private val orgnrAnnet = "2".repeat(9)

@Test
fun `tilgang til både underenhet og hovedenhet`() {
mockTilganger(underenhet = true, hovedenhet = true)

val kontakinfo = kontaktinfoController.getKontaktinfo(KontaktinfoRequest(orgnrUnderenhet))
assertNotNull(kontakinfo.hovedenhet)
assertNotNull(kontakinfo.underenhet)
}

@Test
fun `tilgang til kun underenhet`() {
mockTilganger(underenhet = true, hovedenhet = false)

val kontakinfo = kontaktinfoController.getKontaktinfo(KontaktinfoRequest(orgnrUnderenhet))
assertNull(kontakinfo.hovedenhet)
assertNotNull(kontakinfo.underenhet)
}

@Test
fun `tilgang til kun hovedenhet`() {
mockTilganger(underenhet = false, hovedenhet = true)

val kontakinfo = kontaktinfoController.getKontaktinfo(KontaktinfoRequest(orgnrUnderenhet))
assertNotNull(kontakinfo.hovedenhet)
assertNull(kontakinfo.underenhet)
}

@Test
fun `ikke tilgang til hverken hovedenhet eller underenhet `() {
mockTilganger(underenhet = false, hovedenhet = false)

val kontakinfo = kontaktinfoController.getKontaktinfo(KontaktinfoRequest(orgnrUnderenhet))
assertNull(kontakinfo.hovedenhet)
assertNull(kontakinfo.underenhet)
}

@BeforeEach
fun beforeEach() {
/* også kall med andre orgnr er vellykkede, for å unngå early return i controlleren, som kunne ha
* skjult manglende tilgangssjekker. */
`when`(eregService.hentUnderenhet(kotlinAny())).thenAnswer {
if (it.arguments[0] == orgnrUnderenhet) {
Organisasjon(
parentOrganizationNumber = orgnrHovedenhet,
organizationNumber = orgnrUnderenhet,
)
} else {
Organisasjon(
parentOrganizationNumber = orgnrAnnet,
organizationNumber = it.arguments[0] as String,
)
}
}

/* Returner alltid kontaktinfo, uavhengig av orgnr, så vi ikke skjuler feil. */
`when`(kontaktinfoClient.hentKontaktinfo(kotlinAny())).thenReturn(
KontaktinfoClient.Kontaktinfo(setOf("x"), setOf("y"))
)

`when`(authenticatedUserHolder.fnr).thenReturn(fnr)
}

/* Mock alle andre tilgangssjekker som true, for å provosere fram lekkasje. */
private fun mockTilganger(underenhet: Boolean, hovedenhet: Boolean) {
`when`(altinnRollerClient.harAltinnRolle(kotlinAny(), kotlinAny(), kotlinAny(), kotlinAny())).thenAnswer {
when (it.arguments[0]) {
fnr -> when (it.arguments[1]) {
orgnrUnderenhet -> underenhet
orgnrHovedenhet -> hovedenhet
else -> true
}
else -> true
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package no.nav.arbeidsgiver.min_side.kontaktinfo

import no.nav.arbeidsgiver.min_side.config.SecurityConfig
import no.nav.arbeidsgiver.min_side.controller.AuthenticatedUserHolder
import no.nav.arbeidsgiver.min_side.controller.SecurityMockMvcUtil.Companion.jwtWithPid
import no.nav.arbeidsgiver.min_side.services.ereg.EregService
import no.nav.arbeidsgiver.min_side.tilgangsstyring.AltinnRollerClient
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActionsDsl
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.request.RequestPostProcessor

@MockBean(JwtDecoder::class)
@MockBean(AuthenticatedUserHolder::class)
@MockBean(AltinnRollerClient::class)
@MockBean(EregService::class)
@MockBean(KontaktinfoClient::class)
@WebMvcTest(
value = [
KontaktinfoController::class,
SecurityConfig::class,
AuthenticatedUserHolder::class,
],
properties = [
"server.servlet.context-path=/"
]
)
class KontaktinfoControllerSerdeTest {
@Autowired
lateinit var mockMvc: MockMvc

@Test
fun protocolFormat() {
mockMvc.kontaktinfo(
content = """{ "virksomhetsnummer": "123456789" }"""
).andExpect {
status { isOk() }
content {
contentType(APPLICATION_JSON)
json("""
{
"hovedenhet": null,
"underenhet": null
}
""".trimIndent())
}
}
}

@Test
fun virksomhetsnummerAsNumberFails() {
/* spring's objectmapper konverterer numbers til strings. */
mockMvc.kontaktinfo(
content = """{ "virksomhetsnummer": 123456789 }"""
).andExpect {
status { isOk() }
}
}


@Test
fun wrongJsonInRequest() {
mockMvc.kontaktinfo(
content = """{ }"""
).andExpect {
status { isBadRequest() }
}
}


@Test
fun superflousJsonFields() {
/* spring's objectmapper godtar ekstra felter. */
mockMvc.kontaktinfo(
content = """{ "virksomhetsnummer": "123412341", "garbage": 2 }"""
).andExpect {
status { isOk() }
}
}

@Test
fun disallowAcceptXML() {
mockMvc.kontaktinfo(
content = """{ "virksomhetsnummer": "123412341" }""",
accept = MediaType.APPLICATION_XML
).andExpect {
status { is4xxClientError() }
}
}

private fun MockMvc.kontaktinfo(
contentType: MediaType? = APPLICATION_JSON,
content: String,
auth: RequestPostProcessor = jwtWithPid("42"),
accept: MediaType? = APPLICATION_JSON,
): ResultActionsDsl =
post("/api/kontaktinfo/v1") {
this.contentType = contentType
this.content = content
this.accept = accept
with(auth)
}
}
13 changes: 13 additions & 0 deletions src/test/kotlin/no/nav/arbeidsgiver/min_side/mockito.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package no.nav.arbeidsgiver.min_side

import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers

/* Based on https://github.com/mockito/mockito-kotlin */

inline fun<reified T> kotlinAny(): T = ArgumentMatchers.any<T>() ?: castNull()

fun <T >ArgumentCaptor<T>.kotlinCapture() = capture() ?: castNull()
peterbb marked this conversation as resolved.
Show resolved Hide resolved

@Suppress("UNCHECKED_CAST")
fun <T> castNull(): T = null as T
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package no.nav.arbeidsgiver.min_side.services.digisyfo

import io.micrometer.core.instrument.MeterRegistry
import no.nav.arbeidsgiver.min_side.kotlinCapture
import no.nav.arbeidsgiver.min_side.models.Organisasjon
import no.nav.arbeidsgiver.min_side.services.digisyfo.DigisyfoService.VirksomhetOgAntallSykmeldte
import no.nav.arbeidsgiver.min_side.services.ereg.EregService
Expand Down Expand Up @@ -43,13 +44,13 @@ class DigisyfoServiceTest {
@BeforeEach
fun setUp() {
Mockito.`when`(
eregService.hentOverenhet(orgnrCaptor.capture())
eregService.hentOverenhet(orgnrCaptor.kotlinCapture())
).thenAnswer {
enhetsregisteret[orgnrCaptor.value]
}

Mockito.`when`(
eregService.hentUnderenhet(orgnrCaptor.capture())
eregService.hentUnderenhet(orgnrCaptor.kotlinCapture())
).thenAnswer {
enhetsregisteret[orgnrCaptor.value]
}
Expand Down