Skip to content

Commit 1dd2509

Browse files
authored
Merge pull request #493 from navikt/aktor-bytte
Resttjeneste for aktørbytte
2 parents b7879b2 + 3abf6ee commit 1dd2509

15 files changed

+406
-8
lines changed

nais/dev-gcp.json

+15-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121
"diskAutoresize": "false",
2222
"highAvailability": "false"
2323
},
24+
"azure": {
25+
"replyURLs": [
26+
"https://sif-innsyn-api.intern.dev.nav.no/swagger-ui/oauth2-redirect.html"
27+
],
28+
"groups": [
29+
{
30+
"name": "0000-GA-k9-drift",
31+
"objectId": "0bc9661c-975c-4adb-86d1-a97172490662"
32+
}
33+
]
34+
},
2435
"azureTenant": "trygdeetaten.no",
2536
"kafkaPool": "nav-dev",
2637
"env": {
@@ -39,7 +50,10 @@
3950
"SAF_AZURE_SCOPE": "api://dev-fss.teamdokumenthandtering.saf-q1/.default",
4051
"K9_SAK_INNSYN_API_TOKEN_X_AUDIENCE": "dev-gcp:dusseldorf:k9-sak-innsyn-api",
4152
"K9_SELVBETJENING_OPPSLAG_TOKEN_X_AUDIENCE": "dev-gcp:dusseldorf:k9-selvbetjening-oppslag",
42-
"SWAGGER_ENABLED": "true"
53+
"SWAGGER_ENABLED": "true",
54+
"AZURE_LOGIN_URL": "https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0",
55+
"K9_DRIFT_GRUPPE_ID": "0bc9661c-975c-4adb-86d1-a97172490662"
56+
4357
},
4458
"slack-channel": "sif-alerts-dev",
4559
"slack-notify-type": "<!here> | sif-innsyn-api | "

nais/naiserator.yaml

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ spec:
1313
application:
1414
enabled: true
1515
tenant: {{azureTenant}}
16+
claims:
17+
extra:
18+
- "NAVident"
19+
groups:
20+
{{#each azure.groups as |group|}}
21+
- id: {{group.objectId}}
22+
{{/each}}
23+
replyURLs:
24+
{{#each azure.replyURLs as |url|}}
25+
- {{url}}
26+
{{/each}}
1627
tokenx:
1728
enabled: true
1829
accessPolicy:

nais/prod-gcp.json

+14-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121
"diskAutoresize": "true",
2222
"highAvailability": "true"
2323
},
24+
"azure": {
25+
"replyURLs": [
26+
"https://sif-innsyn-api.intern.nav.no/swagger-ui/oauth2-redirect.html"
27+
],
28+
"groups": [
29+
{
30+
"name": "0000-GA-k9-drift",
31+
"objectId": "1509dc91-a955-4e72-b64c-2f049e37c0c6"
32+
}
33+
]
34+
},
2435
"azureTenant": "nav.no",
2536
"kafkaPool": "nav-prod",
2637
"env": {
@@ -38,7 +49,9 @@
3849
"SAF_AZURE_SCOPE": "api://prod-fss.teamdokumenthandtering.saf/.default",
3950
"SAFSELVBETJENING_TOKEN_X_AUDIENCE": "prod-fss:teamdokumenthandtering:safselvbetjening",
4051
"K9_SAK_INNSYN_API_TOKEN_X_AUDIENCE": "prod-gcp:dusseldorf:k9-sak-innsyn-api",
41-
"K9_SELVBETJENING_OPPSLAG_TOKEN_X_AUDIENCE": "prod-gcp:dusseldorf:k9-selvbetjening-oppslag"
52+
"K9_SELVBETJENING_OPPSLAG_TOKEN_X_AUDIENCE": "prod-gcp:dusseldorf:k9-selvbetjening-oppslag",
53+
"AZURE_LOGIN_URL": "https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0",
54+
"K9_DRIFT_GRUPPE_ID": "1509dc91-a955-4e72-b64c-2f049e37c0c6"
4255
},
4356
"slack-channel": "sif-alerts",
4457
"slack-notify-type": "<!here> | sif-innsyn-api | "

src/main/kotlin/no/nav/sifinnsynapi/config/SecurityConfiguration.kt

+1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ internal class SecurityConfiguration
99

1010
object Issuers {
1111
const val TOKEN_X = "tokenx"
12+
const val AZURE = "azure"
1213
}

src/main/kotlin/no/nav/sifinnsynapi/config/SwaggerConfiguration.kt

+44-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
package no.nav.sifinnsynapi.config
22

3+
import io.swagger.v3.oas.models.Components
34
import io.swagger.v3.oas.models.ExternalDocumentation
45
import io.swagger.v3.oas.models.OpenAPI
56
import io.swagger.v3.oas.models.info.Info
7+
import io.swagger.v3.oas.models.security.*
68
import io.swagger.v3.oas.models.servers.Server
9+
import org.springframework.beans.factory.annotation.Value
10+
import org.springframework.context.EnvironmentAware
711
import org.springframework.context.annotation.Bean
812
import org.springframework.context.annotation.Configuration
9-
import org.springframework.context.annotation.Profile
13+
import org.springframework.core.env.Environment
1014

1115
@Configuration
12-
@Profile("local", "dev-gcp")
13-
class SwaggerConfiguration {
16+
class SwaggerConfiguration(
17+
@Value("\${springdoc.oAuthFlow.authorizationUrl}") val authorizationUrl: String,
18+
@Value("\${springdoc.oAuthFlow.tokenUrl}") val tokenUrl: String,
19+
@Value("\${springdoc.oAuthFlow.apiScope}") val apiScope: String
20+
) : EnvironmentAware {
21+
22+
private var env: Environment? = null
23+
1424

1525
@Bean
1626
fun openAPI(): OpenAPI {
@@ -28,6 +38,37 @@ class SwaggerConfiguration {
2838
ExternalDocumentation()
2939
.description("Sif Innsyn Api GitHub repository")
3040
.url("https://github.com/navikt/sif-innsyn-api")
41+
).components(
42+
Components()
43+
.addSecuritySchemes("oauth2", azureLogin())
44+
)
45+
.addSecurityItem(
46+
SecurityRequirement()
47+
.addList("oauth2", listOf("read", "write"))
48+
.addList("Authorization")
3149
)
3250
}
51+
52+
53+
private fun azureLogin(): SecurityScheme {
54+
return SecurityScheme()
55+
.name("oauth2")
56+
.type(SecurityScheme.Type.OAUTH2)
57+
.scheme("oauth2")
58+
.`in`(SecurityScheme.In.HEADER)
59+
.flows(
60+
OAuthFlows()
61+
.authorizationCode(
62+
OAuthFlow().authorizationUrl(authorizationUrl)
63+
.tokenUrl(tokenUrl)
64+
.scopes(Scopes().addString(apiScope, "read,write"))
65+
)
66+
)
67+
}
68+
69+
override fun setEnvironment(environment: Environment) {
70+
this.env = environment;
71+
}
72+
73+
3374
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package no.nav.sifinnsynapi.forvaltning
2+
3+
import no.nav.security.token.support.core.api.ProtectedWithClaims
4+
import no.nav.security.token.support.core.api.RequiredIssuers
5+
import no.nav.sifinnsynapi.common.AktørId
6+
import no.nav.sifinnsynapi.config.Issuers
7+
import no.nav.sifinnsynapi.sikkerhet.AuthorizationService
8+
import no.nav.sifinnsynapi.sikkerhet.ContextHolder
9+
import no.nav.sifinnsynapi.soknad.SøknadService
10+
import org.springframework.http.HttpStatus
11+
import org.springframework.http.MediaType
12+
import org.springframework.http.ProblemDetail
13+
import org.springframework.http.ResponseEntity
14+
import org.springframework.web.ErrorResponseException
15+
import org.springframework.web.bind.annotation.PostMapping
16+
import org.springframework.web.bind.annotation.RequestBody
17+
import org.springframework.web.bind.annotation.RestController
18+
19+
@RestController
20+
@RequiredIssuers(
21+
ProtectedWithClaims(issuer = Issuers.AZURE)
22+
)
23+
class AktørBytteController(
24+
private valknadService: SøknadService,
25+
private val authorizationService: AuthorizationService
26+
) {
27+
28+
@PostMapping(
29+
"/forvaltning/oppdaterAktoerId",
30+
consumes = [MediaType.APPLICATION_JSON_VALUE],
31+
produces = [MediaType.APPLICATION_JSON_VALUE]
32+
)
33+
@ProtectedWithClaims(issuer = ContextHolder.AZURE_AD, claimMap = ["NAVident=*"])
34+
fun oppdaterAktoerId(@RequestBody aktørBytteRequest: AktørBytteRequest): ResponseEntity<AktørBytteRespons> {
35+
if (!authorizationService.harTilgangTilDriftRolle()) {
36+
val problemDetail = ProblemDetail.forStatus(HttpStatus.FORBIDDEN)
37+
problemDetail.detail = "Mangler driftsrolle"
38+
throw ErrorResponseException(HttpStatus.FORBIDDEN, problemDetail, null)
39+
}
40+
val antallOppdaterte = søknadService.oppdaterAktørId(
41+
AktørId(aktørBytteRequest.gyldigAktør),
42+
AktørId(aktørBytteRequest.utgåttAktør)
43+
)
44+
return ResponseEntity.ok(AktørBytteRespons(antallOppdaterte))
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package no.nav.sifinnsynapi.forvaltning
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import jakarta.validation.constraints.Pattern
5+
import jakarta.validation.constraints.Size
6+
import org.jetbrains.annotations.NotNull
7+
8+
data class AktørBytteRequest(
9+
@JsonProperty
10+
@NotNull
11+
@Size(max = 20)
12+
@Pattern(regexp = "^\\d+$", message = "AktørId [\${validatedValue}] matcher ikke tillatt pattern [{regexp}]")
13+
val utgåttAktør: String,
14+
@JsonProperty
15+
@NotNull
16+
@Size(max = 20)
17+
@Pattern(regexp = "^\\d+$", message = "AktørId [\${validatedValue}] matcher ikke tillatt pattern [{regexp}]")
18+
val gyldigAktør: String
19+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package no.nav.sifinnsynapi.forvaltning
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import jakarta.validation.constraints.Pattern
5+
import jakarta.validation.constraints.Size
6+
import org.jetbrains.annotations.NotNull
7+
8+
data class AktørBytteRespons(
9+
@JsonProperty
10+
@NotNull
11+
val antallOppdaterteRader: Int,
12+
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package no.nav.sifinnsynapi.sikkerhet
2+
3+
import org.springframework.beans.factory.annotation.Value
4+
import org.springframework.context.annotation.Bean
5+
import org.springframework.context.annotation.Configuration
6+
7+
class AuthorizationService(
8+
private val contextHolder: ContextHolder,
9+
private val k9DriftGruppeId: String,
10+
) {
11+
fun harTilgangTilDriftRolle(): Boolean {
12+
return contextHolder.requestKontekst()?.jwtToken?.containsClaim("groups", k9DriftGruppeId) ?: false
13+
}
14+
}
15+
16+
@Configuration
17+
class AuthorizationConfig(
18+
@Value("\${no.nav.security.k9-drift-gruppe}") private val k9DriftGruppeId: String,
19+
) {
20+
@Bean
21+
fun authorizationService() = AuthorizationService(ContextHolder.INSTANCE, k9DriftGruppeId)
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package no.nav.sifinnsynapi.sikkerhet
2+
3+
import no.nav.security.token.support.core.jwt.JwtToken
4+
import no.nav.security.token.support.spring.SpringTokenValidationContextHolder
5+
import org.springframework.web.context.request.RequestContextHolder
6+
7+
class ContextHolder private constructor(private val context: SpringTokenValidationContextHolder) {
8+
9+
companion object {
10+
const val AZURE_AD = "azure"
11+
private var instans: ContextHolder? = null
12+
val INSTANCE: ContextHolder
13+
get() {
14+
if (instans == null) {
15+
instans = ContextHolder(SpringTokenValidationContextHolder())
16+
}
17+
return instans!!
18+
}
19+
20+
}
21+
22+
fun requestKontekst(): RequestKontekst? {
23+
if (RequestContextHolder.getRequestAttributes() == null)
24+
return null
25+
26+
val tokenContext = context.getTokenValidationContext()
27+
val reqIssuerShortNames = tokenContext.issuers //alle issuers på alle validerte tokens i context
28+
if (reqIssuerShortNames.contains(AZURE_AD)) {
29+
val jwtToken: JwtToken? = tokenContext.getJwtToken(AZURE_AD)
30+
return jwtToken?.let { RequestKontekst(it, AZURE_AD) }
31+
}
32+
return null
33+
}
34+
35+
@JvmRecord
36+
data class RequestKontekst(val jwtToken: JwtToken, val issuerShortname: String)
37+
38+
39+
}

src/main/kotlin/no/nav/sifinnsynapi/soknad/SøknadDAO.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import java.util.*
2222
@Entity(name = "søknad")
2323
data class SøknadDAO(
2424
@Column(name = "id") @Id @JdbcTypeCode(SqlTypes.UUID) val id: UUID = UUID.randomUUID(),
25-
@Column(name = "aktør_id") @Embedded val aktørId: AktørId,
25+
@Column(name = "aktør_id") @Embedded internal val aktørId: AktørId,
2626
@Column(name = "fødselsnummer") @Embedded valdselsnummer: Fødselsnummer,
2727
@Column(name = "søknadstype") @Enumerated(STRING) valknadstype: Søknadstype,
2828
@Column(name = "status") @Enumerated(STRING) val status: SøknadsStatus,

src/main/kotlin/no/nav/sifinnsynapi/soknad/SøknadService.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package no.nav.sifinnsynapi.soknad
33
import com.fasterxml.jackson.core.type.TypeReference
44
import com.fasterxml.jackson.databind.ObjectMapper
55
import no.nav.sifinnsynapi.common.AktørId
6+
import no.nav.sifinnsynapi.common.Fødselsnummer
67
import no.nav.sifinnsynapi.common.Søknadstype
78
import no.nav.sifinnsynapi.dokument.DokumentDTO
89
import no.nav.sifinnsynapi.dokument.DokumentService
@@ -53,8 +54,9 @@ class SøknadService(
5354

5455
return søknadDAOs
5556
.map { søknadDAO ->
56-
val relevanteDokumenter = dokumentOversikt.filter { it.journalpostId == søknadDAO.journalpostId }
57-
søknadDAO.tilSøknadDTO(relevanteDokumenter) }
57+
val relevanteDokumenter = dokumentOversikt.filter { it.journalpostId == søknadDAO.journalpostId }
58+
søknadDAO.tilSøknadDTO(relevanteDokumenter)
59+
}
5860
}
5961

6062
fun hentSøknad(søknadId: UUID): SøknadDTO {
@@ -121,6 +123,13 @@ class SøknadService(
121123
fun finnUnikeSøknaderUtenMikrofrontendSisteSeksMåneder(søknadstype: Søknadstype, limit: Int): List<SøknadDAO> {
122124
return repo.finnUnikeSøknaderUtenMikrofrontendSisteSeksMåneder(søknadstype.name, limit)
123125
}
126+
127+
fun oppdaterAktørId(gyldigAktørId: AktørId, utgåttAktørId: AktørId): Int {
128+
val søknader = repo.findAllByAktørId(utgåttAktørId);
129+
søknader.map { it.copy( aktørId = gyldigAktørId ) }
130+
.forEach { repo.save(it) }
131+
return søknader.size;
132+
}
124133
}
125134

126135
class SøknadNotFoundException(søknadId: String) :

src/main/resources/application.yml

+12
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ no.nav:
2929
saf-selvbetjening-base-url: # Settes i nais/<cluster>.json
3030

3131
security:
32+
k9-drift-gruppe: ${K9_DRIFT_GRUPPE_ID}
3233
cors:
3334
allowed-origins: # Settes i nais/<cluster>.json
3435
jwt:
3536
issuer:
37+
azure:
38+
discoveryUrl: ${AZURE_APP_WELL_KNOWN_URL}
39+
accepted_audience: ${AZURE_APP_CLIENT_ID}
3640
tokenx:
3741
discoveryUrl: ${TOKEN_X_WELL_KNOWN_URL}
3842
accepted_audience: ${TOKEN_X_CLIENT_ID}
@@ -187,3 +191,11 @@ springdoc:
187191
enabled: ${SWAGGER_ENABLED:false}
188192
disable-swagger-default-url: true
189193
path: swagger-ui.html
194+
oauth:
195+
use-pkce-with-authorization-code-grant: true
196+
client-id: ${AZURE_APP_CLIENT_ID}
197+
scope-separator: ","
198+
oAuthFlow:
199+
authorizationUrl: ${AZURE_LOGIN_URL:http://localhost:8080}/authorize
200+
tokenUrl: ${AZURE_LOGIN_URL:http://localhost:8080}/token
201+
apiScope: api://${AZURE_APP_CLIENT_ID:abc123}/.default

0 commit comments

Comments
 (0)