diff --git a/infra/lib/service-stack.ts b/infra/lib/service-stack.ts index eda4c36f..7d3fd14e 100644 --- a/infra/lib/service-stack.ts +++ b/infra/lib/service-stack.ts @@ -58,6 +58,13 @@ export class ServiceStack extends Stack { "kielitesti-token", ), ), + OPPIJANUMERO_PASSWORD: aws_ecs.Secret.fromSecretsManager( + aws_secretsmanager.Secret.fromSecretNameV2( + this, + "OppijanumeroPassword", + "oppijanumero-password", + ), + ), }, }, cpu: 1024, diff --git a/scripts/test_oppijanumerorekisteri.sh b/scripts/test_oppijanumerorekisteri.sh new file mode 100755 index 00000000..7200f268 --- /dev/null +++ b/scripts/test_oppijanumerorekisteri.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +curl -s -i \ + -X POST "https://virkailija.testiopintopolku.fi/cas/v1/tickets" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$1&password=$2" diff --git a/server/src/main/kotlin/fi/oph/kitu/generated/api/OppijanumeroControllerApi.kt b/server/src/main/kotlin/fi/oph/kitu/generated/api/OppijanumeroControllerApi.kt new file mode 100644 index 00000000..1b6c7e1f --- /dev/null +++ b/server/src/main/kotlin/fi/oph/kitu/generated/api/OppijanumeroControllerApi.kt @@ -0,0 +1,19 @@ +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.8.0). + * https://openapi-generator.tech + * Do not edit the class manually. +*/ +package fi.oph.kitu.generated.api + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +interface OppijanumeroControllerApi { + @RequestMapping( + method = [RequestMethod.GET], + value = ["/api/oppijanumero"], + produces = ["text/plain"], + ) + fun getOppijanumero(): ResponseEntity +} diff --git a/server/src/main/kotlin/fi/oph/kitu/oppijanumero/CasAuthenticatedService.kt b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/CasAuthenticatedService.kt new file mode 100644 index 00000000..fc0a7935 --- /dev/null +++ b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/CasAuthenticatedService.kt @@ -0,0 +1,56 @@ +package fi.oph.kitu.oppijanumero + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +@Service +class CasAuthenticatedService( + private val httpClient: HttpClient, + private val casService: CasService, +) { + @Value("\${kitu.oppijanumero.callerid}") + private lateinit var callerId: String + + fun authenticateToCas() { + val grantingTicket = casService.getGrantingTicket() + val serviceTicket = casService.getServiceTicket(grantingTicket) + + casService.sendAuthenticationRequest(serviceTicket) + } + + fun sendRequest(requestBuilder: HttpRequest.Builder): HttpResponse { + println("Sending CAS authenticated request") + requestBuilder + .header("Caller-Id", callerId) + .header("CSRF", "CSRF") + .header("Cookie", "CSRF=CSRF") + val response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) + + if (isLoginToCas(response)) { + // Oppijanumerorekisteri ohjaa CAS kirjautumissivulle, jos autentikaatiota + // ei ole tehty. Luodaan uusi CAS ticket ja yritetään uudelleen. + println("Was redirected to CAS login") + authenticateToCas() // gets JSESSIONID Cookie and it will be used in the next request below + return httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) + } else if (response.statusCode() == 401) { + // Oppijanumerorekisteri vastaa HTTP 401 kun sessio on vanhentunut. + // HUOM! Oppijanumerorekisteri vastaa HTTP 401 myös jos käyttöoikeudet eivät riitä. + println("Received HTTP 401 response") + authenticateToCas() // gets JSESSIONID Cookie and it will be used in the next request below + return httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) + } else { + return response + } + } + + private fun isLoginToCas(response: HttpResponse<*>): Boolean { + if (response.statusCode() == 302) { + val header = response.headers().firstValue("Location") + return header.map { location: String -> location.contains("/cas/login") }.orElse(false) + } + return false + } +} diff --git a/server/src/main/kotlin/fi/oph/kitu/oppijanumero/CasService.kt b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/CasService.kt new file mode 100644 index 00000000..1191ac03 --- /dev/null +++ b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/CasService.kt @@ -0,0 +1,85 @@ +package fi.oph.kitu.oppijanumero + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.net.URI +import java.net.URLEncoder +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +@Service +class CasService( + private val httpClient: HttpClient, +) { + @Value("\${kitu.oppijanumero.username}") + private lateinit var onrUsername: String + + @Value("\${kitu.oppijanumero.password}") + private lateinit var onrPassword: String + + @Value("\${kitu.oppijanumero.casUrl}") + private lateinit var casUrl: String + + @Value("\${kitu.oppijanumero.serviceUrl}") + private lateinit var serviceUrl: String + + fun sendAuthenticationRequest(serviceTicket: String) { + val authRequest = + HttpRequest + .newBuilder(URI.create("$serviceUrl/j_spring_cas_security_check?ticket=$serviceTicket")) + .method("GET", HttpRequest.BodyPublishers.noBody()) + .build() + val authResponse = httpClient.send(authRequest, HttpResponse.BodyHandlers.ofString()) + println("Auth reset response: $authResponse") + } + + fun getServiceTicket(ticketGrantingTicket: String): String { + val request = + HttpRequest + .newBuilder(URI.create("$casUrl/v1/tickets/$ticketGrantingTicket")) + .POST( + HttpRequest.BodyPublishers.ofString( + "service=$serviceUrl/j_spring_cas_security_check", + ), + ).header("Content-Type", "application/x-www-form-urlencoded") + .build() + + val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() != 200) { + throw RuntimeException("Unexpected status code: ${response.statusCode()} and message: ${response.body()}") + } + + val ticket = response.body() + println("Successfully got service ticket $ticket") + return ticket + } + + fun getGrantingTicket(): String { + // Step 2 - form a request + val username = URLEncoder.encode(onrUsername, "UTF-8") + val password = URLEncoder.encode(onrPassword, "UTF-8") + val request = + HttpRequest + .newBuilder(URI.create("$casUrl/v1/tickets")) + .POST(HttpRequest.BodyPublishers.ofString("username=$username&password=$password")) + .header("Content-Type", "application/x-www-form-urlencoded") + .build() + + // Step 3 - Get the response + val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) + val statusCode = response.statusCode() + val body = response.body() + + if (statusCode != 201) { + throw RuntimeException("Ticket granting service responded with status code $statusCode and message $body") + } + + val location = response.headers().firstValue("Location").get() + val ticket = location.substring(location.lastIndexOf("/") + 1) + println("Successfully fetched TGT (Ticket Granting Ticket): $ticket") + + return ticket + } +} diff --git a/server/src/main/kotlin/fi/oph/kitu/oppijanumero/HttpClientConfig.kt b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/HttpClientConfig.kt new file mode 100644 index 00000000..ad69e14c --- /dev/null +++ b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/HttpClientConfig.kt @@ -0,0 +1,18 @@ +package fi.oph.kitu.oppijanumero + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.net.CookieManager +import java.net.http.HttpClient +import java.time.Duration + +@Configuration +class HttpClientConfig { + @Bean + fun HttpClient(): HttpClient = + HttpClient + .newBuilder() + .cookieHandler(CookieManager()) // sends JSESSIONID Cookie between the requests + .connectTimeout(Duration.ofSeconds(10)) + .build() +} diff --git a/server/src/main/kotlin/fi/oph/kitu/oppijanumero/OppijanumeroController.kt b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/OppijanumeroController.kt new file mode 100644 index 00000000..2d784a34 --- /dev/null +++ b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/OppijanumeroController.kt @@ -0,0 +1,36 @@ +package fi.oph.kitu.oppijanumero + +import fi.oph.kitu.generated.api.OppijanumeroControllerApi +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.RestController + +@RestController +class OppijanumeroController( + private val oppijanumeroService: OppijanumeroService, +) : OppijanumeroControllerApi { + override fun getOppijanumero(): ResponseEntity { + try { + val response = + oppijanumeroService.yleistunnisteHae( + YleistunnisteHaeRequest( + "Magdalena Testi", + "010866-9260", + "Magdalena", + "Sallinen-Testi", + ), + ) + return ResponseEntity( + response.body(), + if (response.statusCode() == 200) HttpStatus.OK else HttpStatus.INTERNAL_SERVER_ERROR, + ) + } catch (e: Exception) { + println("ERROR: ${e.message}") + + return ResponseEntity( + "An unexpected error has occurred:${e.localizedMessage}", + HttpStatus.INTERNAL_SERVER_ERROR, + ) + } + } +} diff --git a/server/src/main/kotlin/fi/oph/kitu/oppijanumero/OppijanumeroService.kt b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/OppijanumeroService.kt new file mode 100644 index 00000000..978f2779 --- /dev/null +++ b/server/src/main/kotlin/fi/oph/kitu/oppijanumero/OppijanumeroService.kt @@ -0,0 +1,43 @@ +package fi.oph.kitu.oppijanumero + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.net.URI +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +@Service +class OppijanumeroService( + private val casAuthenticatedService: CasAuthenticatedService, +) { + @Value("\${kitu.oppijanumero.serviceUrl}") + private lateinit var serviceUrl: String + + fun yleistunnisteHae(request: YleistunnisteHaeRequest): HttpResponse { + val httpRequest = + HttpRequest + .newBuilder(URI.create("$serviceUrl/yleistunniste/hae")) + .POST(toBodyPublisher(request)) + .header("Content-Type", "application/json") + + val response = casAuthenticatedService.sendRequest(httpRequest) + return response + } + + private fun toBodyPublisher(request: YleistunnisteHaeRequest): HttpRequest.BodyPublisher = + HttpRequest.BodyPublishers.ofString( + """{ + "etunimet": "${request.etunimet}", + "hetu": "${request.hetu}", + "kutsumanimi": "${request.kutsumanimi}", + "sukunimi": "${request.sukunimi}" + }""".trim(), + ) +} + +data class YleistunnisteHaeRequest( + val etunimet: String, + val hetu: String, + val kutsumanimi: String, + val sukunimi: String, +) diff --git a/server/src/main/resources/application-e2e.properties b/server/src/main/resources/application-e2e.properties index 67d6728c..e890de4b 100644 --- a/server/src/main/resources/application-e2e.properties +++ b/server/src/main/resources/application-e2e.properties @@ -7,3 +7,9 @@ kitu.opintopolkuHostname=virkailija.untuvaopintopolku.fi kitu.kielitesti.wstoken= kitu.kielitesti.baseurl= + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password= +kitu.oppijanumero.callerid=1.2.246.562.24.85478397072 +kitu.oppijanumero.casUrl=https://virkailija.untuvaopintopolku.fi/cas +kitu.oppijanumero.serviceUrl=https://virkailija.untuvaopintopolku.fi/oppijanumerorekisteri-service diff --git a/server/src/main/resources/application-local.properties b/server/src/main/resources/application-local.properties index f6b07c45..530e0612 100644 --- a/server/src/main/resources/application-local.properties +++ b/server/src/main/resources/application-local.properties @@ -8,3 +8,9 @@ kitu.appUrl=http://localhost:8080 kitu.opintopolkuHostname=virkailija.untuvaopintopolku.fi kitu.kielitesti.baseurl=https://kielitesti.mmg.fi + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password=${OPPIJANUMERO_PASSWORD} +kitu.oppijanumero.callerid=1.2.246.562.24.85478397072 +kitu.oppijanumero.casUrl=https://virkailija.untuvaopintopolku.fi/cas +kitu.oppijanumero.serviceUrl=https://virkailija.untuvaopintopolku.fi/oppijanumerorekisteri-service diff --git a/server/src/main/resources/application-prod.properties b/server/src/main/resources/application-prod.properties index 1cb77fa4..d9853f40 100644 --- a/server/src/main/resources/application-prod.properties +++ b/server/src/main/resources/application-prod.properties @@ -6,3 +6,9 @@ spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} kitu.kielitesti.baseurl=https://kielitesti.oph.fi + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password=${OPPIJANUMERO_PASSWORD} +kitu.oppijanumero.callerid= +kitu.oppijanumero.casUrl=https://virkailija.opintopolku.fi/cas +kitu.oppijanumero.serviceUrl=https://virkailija.opintopolku.fi/oppijanumerorekisteri-service diff --git a/server/src/main/resources/application-qa.properties b/server/src/main/resources/application-qa.properties index 36af1d34..9a59e0d0 100644 --- a/server/src/main/resources/application-qa.properties +++ b/server/src/main/resources/application-qa.properties @@ -6,3 +6,9 @@ spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} kitu.kielitesti.baseurl=https://kielitesti.mmg.fi + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password=${OPPIJANUMERO_PASSWORD} +kitu.oppijanumero.callerid=1.2.246.562.24.77101904300 +kitu.oppijanumero.casUrl=https://virkailija.testiopintopolku.fi/cas +kitu.oppijanumero.serviceUrl=https://virkailija.testiopintopolku.fi/oppijanumerorekisteri-service diff --git a/server/src/main/resources/application-untuva.properties b/server/src/main/resources/application-untuva.properties index 9b9b1775..642dc185 100644 --- a/server/src/main/resources/application-untuva.properties +++ b/server/src/main/resources/application-untuva.properties @@ -6,3 +6,9 @@ spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} kitu.kielitesti.baseurl=https://kielitesti.mmg.fi + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password=${OPPIJANUMERO_PASSWORD} +kitu.oppijanumero.callerid=1.2.246.562.24.85478397072 +kitu.oppijanumero.casUrl=https://virkailija.untuvaopintopolku.fi/cas +kitu.oppijanumero.serviceUrl=https://virkailija.untuvaopintopolku.fi/oppijanumerorekisteri-service diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 2d725cd1..96e92c52 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -7,3 +7,9 @@ springdoc.swagger-ui.csrf.enabled=true springdoc.api-docs.enabled=false kitu.kielitesti.wstoken=${KIELITESTI_TOKEN} + +kitu.oppijanumero.username= +kitu.oppijanumero.password= +kitu.oppijanumero.callerid= +kitu.oppijanumero.casUrl= +kitu.oppijanumero.serviceUrl= diff --git a/server/src/main/resources/example-local.properties b/server/src/main/resources/example-local.properties index b92a7972..550e598e 100644 --- a/server/src/main/resources/example-local.properties +++ b/server/src/main/resources/example-local.properties @@ -1,2 +1,8 @@ # Kielitesti Moodle web service token kitu.kielitesti.wstoken= + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password= +kitu.oppijanumero.callerid= +kitu.oppijanumero.casUrl= +kitu.oppijanumero.serviceUrl= diff --git a/server/src/main/resources/static/open-api.yaml b/server/src/main/resources/static/open-api.yaml index 40a4885c..87c2c5d6 100644 --- a/server/src/main/resources/static/open-api.yaml +++ b/server/src/main/resources/static/open-api.yaml @@ -59,6 +59,25 @@ paths: type: array items: $ref: "#/components/schemas/Oppija" + + /api/oppijanumero: + get: + tags: + - "oppijanumero-controller" + summary: "Hakee yhden käyttäjän oppijanumerorekisteristä." + operationId: "getOppijanumero" + responses: + 200: + description: "Oppija löytyi" + content: + "text/plain": + schema: + type: string + example: "oid" + 400: + description: "Virheellinen pyyntö" + 500: + description: "Virheellinen pyyntö" components: schemas: Oppija: diff --git a/server/src/test/resources/application.properties b/server/src/test/resources/application.properties index 25f6bc58..46b35c4a 100644 --- a/server/src/test/resources/application.properties +++ b/server/src/test/resources/application.properties @@ -14,3 +14,9 @@ springdoc.api-docs.enabled=false kitu.kielitesti.wstoken= kitu.kielitesti.baseurl= + +kitu.oppijanumero.username=koto-rekisteri +kitu.oppijanumero.password= +kitu.oppijanumero.callerid= +kitu.oppijanumero.casUrl= +kitu.oppijanumero.serviceUrl=