-
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 #334 from navikt/tilgangsstyring_roller
Tilgangsstyring roller
- Loading branch information
Showing
3 changed files
with
374 additions
and
1 deletion.
There are no files selected for viewing
94 changes: 94 additions & 0 deletions
94
src/main/kotlin/no/nav/arbeidsgiver/min_side/tilgangsstyring/AltinnRollerClient.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,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>>() {} | ||
} | ||
} |
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
280 changes: 280 additions & 0 deletions
280
src/test/kotlin/no/nav/arbeidsgiver/min_side/tilgangsstyring/AltinnRollerClientTest.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,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 | ||
} | ||
] | ||
} | ||
] | ||
""" |