Skip to content

Commit

Permalink
Merge pull request #334 from navikt/tilgangsstyring_roller
Browse files Browse the repository at this point in the history
Tilgangsstyring roller
  • Loading branch information
peterbb authored Oct 13, 2023
2 parents d45c59e + cded034 commit 5c67cfa
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package no.nav.arbeidsgiver.min_side.tilgangsstyring

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import no.nav.arbeidsgiver.min_side.clients.retryInterceptor
import no.nav.arbeidsgiver.min_side.maskinporten.MaskinportenTokenService
import org.apache.http.NoHttpResponseException
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.web.client.RestTemplateBuilder
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod.GET
import org.springframework.stereotype.Component
import java.net.SocketException
import javax.net.ssl.SSLHandshakeException

@Component
class AltinnRollerClient(
restTemplateBuilder: RestTemplateBuilder,
@Value("\${altinn.apiBaseUrl}") altinnApiBaseUrl: String,
@Value("\${altinn.altinnHeader}") private val altinnApiKey: String,
private val maskinportenTokenService: MaskinportenTokenService,
) {
private val restTemplate = restTemplateBuilder
.rootUri(altinnApiBaseUrl)
.additionalInterceptors(
retryInterceptor(
3,
250L,
NoHttpResponseException::class.java,
SocketException::class.java,
SSLHandshakeException::class.java,
)
)
.build()

private val safeRoleName = Regex("^[A-ZÆØÅ]+$")
private val orgnrRegex = Regex("^[0-9]{9}$")

fun harAltinnRolle(
fnr: String,
orgnr: String,
altinnRoller: Set<String>,
externalRoller: Set<String>,
): Boolean {
require(orgnr.matches(orgnrRegex)) // user-controlled, so ensure only digits before injecting into query
require(altinnRoller.isNotEmpty() && externalRoller.isNotEmpty()) {
"skrevet under antagelse om at både altinnRoller og externalRoller er non-empty"
}

val headers = HttpHeaders().apply {
set("apikey", altinnApiKey)
setBearerAuth(maskinportenTokenService.currentAccessToken())
}

fun roleDefintionFilter(roller: Iterable<String>) =
roller.joinToString(separator = "+or+") {
require(it.matches(safeRoleName))
"RoleDefinitionCode+eq+'$it'"
}

val altinnRolleFilter = roleDefintionFilter(altinnRoller)
val eregRolleFilter = roleDefintionFilter(externalRoller)
val filter = "(RoleType+eq+'Altinn'+and+($altinnRolleFilter))+or+(RoleType+eq+'External'+and+($eregRolleFilter))"

val roller = restTemplate.exchange(
"/api/serviceowner/authorization/roles?subject={subject}&reportee={reportee}&${'$'}filter={filter}&ForceEIAuthentication",
GET,
HttpEntity<Nothing>(headers),
roleListType,
mapOf<String, Any>(
"subject" to fnr,
"reportee" to orgnr,
"filter" to filter,
)
).body ?: throw RuntimeException("serviceowner/authorization/roles missing body")

/* Kanskje litt paranoid, men da er vi korrekte uavhengig av om $filter er implementert
* som forventet hos altinn eller om vi gjør noe feil med filteret. */
return roller.any { it.roleType == "Altinn" && it.roleDefinitionCode in altinnRoller }
|| roller.any { it.roleType == "External" && it.roleDefinitionCode in externalRoller }
}

@JsonIgnoreProperties(ignoreUnknown = true)
private class RoleDTO(
@JsonProperty("RoleType") val roleType: String,
@JsonProperty("RoleDefinitionCode") val roleDefinitionCode: String,
)

companion object {
private val roleListType = object : ParameterizedTypeReference<List<RoleDTO>>() {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package no.nav.arbeidsgiver.min_side.tilgangsstyring

import no.nav.arbeidsgiver.min_side.maskinporten.MaskinportenTokenServiceStub
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
import org.springframework.http.MediaType
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
import java.lang.IllegalArgumentException


@RestClientTest(
AltinnRollerClient::class,
MaskinportenTokenServiceStub::class,
)
class AltinnRollerClientTest {
@Autowired
lateinit var altinnServer: MockRestServiceServer

@Autowired
lateinit var altinnRollerClient: AltinnRollerClient

/* Ved ukjent fnr svarer altinn:
* HTTP/1.1 400 012345678912 is not a valid organization number or social security number.
*
* Ved ukjent orgnr svarer altinn:
* HTTP/1.1 400 012345678 is not a valid organization number or social security number.
*/

@Test
fun `ingen tilgang ved ingen roller`() {
mockRoles("1234", "567812345", ingenRollerResponse)
assertFalse(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567812345",
altinnRoller = setOf("SIGNE"),
externalRoller = setOf("DAGL")
))
}

@Test
fun `tilgang hvis vi sjekker DAGL(ereg) og bruker er DAGL(ereg), ATTST(altinn)`() {
mockRoles("1234", "567812345", daglOgAttstRolleResponse)
assertTrue(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567812345",
altinnRoller = setOf("SIGN"),
externalRoller = setOf("DAGL"),
))
}

@Test
fun `tilgang hvis vi sjekker HADM(altinn) og bruker er DAGL(ereg), HADM(altinn)`() {
mockRoles("1234", "567812345", daglOgHadmRolleResponse)
assertTrue(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567812345",
altinnRoller = setOf("HADM"),
externalRoller = setOf("ANNENROLLE"),
))
}

@Test
fun `ikke tilgang hvis altinn- og ereg-roller byttes om`() {
mockRoles("1234", "567811223", daglOgHadmRolleResponse)
assertFalse(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567811223",
altinnRoller = setOf("DAGL"),
externalRoller = setOf("HADM"),
))
}

@Test
fun `har tilgang hvis man både har ereg- og altinn-rolle`() {
mockRoles("1234", "567811223", daglOgAttstRolleResponse)
assertTrue(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567811223",
altinnRoller = setOf("ATTST"),
externalRoller = setOf("DAGL")
))
}

@Test
fun `bruker trenger ikke å ha alle rollene vi spør om`() {
mockRoles("1234", "567811223", daglOgAttstRolleResponse)
assertTrue(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567811223",
altinnRoller = setOf("ATTST"),
externalRoller = setOf("DAGL", "ANNENROLLE"),
))
}

@Test
fun `ikke tilgang selv med flere roller og rolle-sjekker`() {
mockRoles("789", "567811223", daglOgAttstRolleResponse)
assertFalse(altinnRollerClient.harAltinnRolle(
fnr = "789",
orgnr = "567811223",
altinnRoller = setOf("IKKEROLLE"),
externalRoller = setOf("ANNENIKKEROLLE"),
))
}

@Test
fun `tolker ikke Local-roller som ereg-roller`() {
mockRoles("1234", "567811223", daglMenLocalRolleResponse)
assertFalse(altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567811223",
altinnRoller = setOf("ANNEN"),
externalRoller = setOf("DAGL"),
))
}


@Test
fun `exception hvis ingen roller oppgis`() {
mockRoles("1234", "567811223", daglOgAttstRolleResponse)
assertThrows<IllegalArgumentException> {
altinnRollerClient.harAltinnRolle(
fnr = "1234",
orgnr = "567811223",
altinnRoller = setOf(),
externalRoller = setOf()
)
}
}

private fun mockRoles(fnr: String, orgnr: String, response: String) =
altinnServer.expect {
assertEquals("/api/serviceowner/authorization/roles", it.uri.path)
assertNotNull(it.uri.query)
val queryParams = it.uri.query.removePrefix("?").split("&")
.map { it.split("=") }
.associate { it.get(0) to it.getOrNull(1) }
assertTrue("ForceEIAuthentication" in queryParams)
assertTrue("\$filter" in queryParams)
assertEquals(fnr, queryParams["subject"])
assertEquals(orgnr, queryParams["reportee"])

}.andRespond(withSuccess(response, MediaType.APPLICATION_JSON))
}

/* Har ikke fått til få tt02 til å returnere tom liste. Men høres ikke utenkelig ut at det er mulig. */
private val ingenRollerResponse = """
[]
"""

/* Hentet fra tt02.altinn.no */
private val daglOgAttstRolleResponse = """
[
{
"RoleType": "Altinn",
"RoleDefinitionId": 85,
"RoleName": "Auditor certifies validity of VAT compensation",
"RoleDescription": "Certification by auditor of RF-0009",
"RoleDefinitionCode": "ATTST",
"_links": [
{
"Rel": "roledefinition",
"Href": "https://tt02.altinn.no/api/serviceowner/roledefinitions/85",
"Title": null,
"FileNameWithExtension": null,
"MimeType": null,
"IsTemplated": false,
"Encrypted": false,
"SigningLocked": false,
"SignedByDefault": false,
"FileSize": 0
}
]
},
{
"RoleId": 45084,
"RoleType": "External",
"RoleDefinitionId": 195,
"RoleName": "General manager",
"RoleDescription": "External role (from The Central Coordinating Register for Legal Entities)",
"RoleDefinitionCode": "DAGL",
"_links": [
{
"Rel": "roledefinition",
"Href": "https://tt02.altinn.no/api/serviceowner/roledefinitions/195",
"Title": null,
"FileNameWithExtension": null,
"MimeType": null,
"IsTemplated": false,
"Encrypted": false,
"SigningLocked": false,
"SignedByDefault": false,
"FileSize": 0
}
]
}
]
""".trimIndent()

/* modifisert svar fra tt02.altinn.no */
private val daglOgHadmRolleResponse = """
[
{
"RoleType": "Altinn",
"RoleDefinitionId": 85,
"RoleName": "Auditor certifies validity of VAT compensation",
"RoleDescription": "Certification by auditor of RF-0009",
"RoleDefinitionCode": "HADM",
"_links": [
{
"Rel": "roledefinition",
"Href": "https://tt02.altinn.no/api/serviceowner/roledefinitions/85",
"Title": null,
"FileNameWithExtension": null,
"MimeType": null,
"IsTemplated": false,
"Encrypted": false,
"SigningLocked": false,
"SignedByDefault": false,
"FileSize": 0
}
]
},
{
"RoleId": 45084,
"RoleType": "External",
"RoleDefinitionId": 195,
"RoleName": "General manager",
"RoleDescription": "External role (from The Central Coordinating Register for Legal Entities)",
"RoleDefinitionCode": "DAGL",
"_links": [
{
"Rel": "roledefinition",
"Href": "https://tt02.altinn.no/api/serviceowner/roledefinitions/195",
"Title": null,
"FileNameWithExtension": null,
"MimeType": null,
"IsTemplated": false,
"Encrypted": false,
"SigningLocked": false,
"SignedByDefault": false,
"FileSize": 0
}
]
}
]
""".trimIndent()

/* Svar fra tt02.altinn.no */
private val daglMenLocalRolleResponse = """
[
{
"RoleType": "Local",
"RoleDefinitionId": 0,
"RoleName": "Single Rights",
"RoleDescription": "Collection of single rights",
"Delegator": "XXX",
"DelegatedTime": "2012-12-03T10:39:59.233",
"RoleDefinitionCode": "DAGL",
"_links": [
{
"Rel": "roledefinition",
"Href": "https://tt02.altinn.no/api/serviceowner/roledefinitions/0",
"Title": null,
"FileNameWithExtension": null,
"MimeType": null,
"IsTemplated": false,
"Encrypted": false,
"SigningLocked": false,
"SignedByDefault": false,
"FileSize": 0
}
]
}
]
"""

0 comments on commit 5c67cfa

Please sign in to comment.