Skip to content

Commit

Permalink
Merge pull request #330 from navikt/TAG-2161-api-spec
Browse files Browse the repository at this point in the history
Controller for kontaktinfo
  • Loading branch information
peterbb authored Oct 13, 2023
2 parents 5c67cfa + ec09ff7 commit 460a0ba
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 4 deletions.
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()

@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

0 comments on commit 460a0ba

Please sign in to comment.