-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #330 from navikt/TAG-2161-api-spec
Controller for kontaktinfo
- Loading branch information
Showing
8 changed files
with
340 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
src/main/kotlin/no/nav/arbeidsgiver/min_side/kontaktinfo/KontaktinfoController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
src/test/kotlin/no/nav/arbeidsgiver/min_side/kontaktinfo/KontaktinfoControllerAuthzTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
110 changes: 110 additions & 0 deletions
110
src/test/kotlin/no/nav/arbeidsgiver/min_side/kontaktinfo/KontaktinfoControllerSerdeTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters