From 83d6f5c08f1f4e0c60c2b808fe4e6ec280ab7270 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Mon, 9 Dec 2024 11:32:07 +0200 Subject: [PATCH] Add employee sfi login endpoint --- apigw/src/shared/service-client.ts | 19 +++++++++ .../src/main/kotlin/fi/espoo/evaka/Audit.kt | 2 + .../fi/espoo/evaka/pis/EmployeeQueries.kt | 23 ++++++++++ .../fi/espoo/evaka/pis/SystemController.kt | 42 +++++++++++++++++++ .../db/migration/V472__employee_ssn.sql | 1 + service/src/main/resources/migrations.txt | 1 + 6 files changed, 88 insertions(+) create mode 100644 service/src/main/resources/db/migration/V472__employee_ssn.sql diff --git a/apigw/src/shared/service-client.ts b/apigw/src/shared/service-client.ts index 950334b3648..a83342a8401 100644 --- a/apigw/src/shared/service-client.ts +++ b/apigw/src/shared/service-client.ts @@ -65,6 +65,12 @@ export interface EmployeeLoginRequest { employeeNumber?: string } +export interface EmployeeSuomiFiLoginRequest { + ssn: string + firstName: string + lastName: string +} + export interface EmployeeUser { id: string firstName: string @@ -107,6 +113,19 @@ export async function employeeLogin( return data } +export async function employeeSuomiFiLogin( + employee: EmployeeSuomiFiLoginRequest +): Promise { + const { data } = await client.post( + `/system/employee-sfi-login`, + employee, + { + headers: createServiceRequestHeaders(undefined, systemUserHeader) + } + ) + return data +} + export async function getEmployeeDetails( req: express.Request, employeeId: string diff --git a/service/src/main/kotlin/fi/espoo/evaka/Audit.kt b/service/src/main/kotlin/fi/espoo/evaka/Audit.kt index adb633785f7..e0121eab0c8 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/Audit.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/Audit.kt @@ -256,6 +256,8 @@ enum class Audit( EmployeeDelete(securityEvent = true, securityLevel = "high"), EmployeeDeleteDaycareRoles(securityEvent = true, securityLevel = "high"), EmployeeLogin(securityEvent = true, securityLevel = "high"), + EmployeeSfiLoginAttempt(securityEvent = true, securityLevel = "high"), + EmployeeSfiLogin(securityEvent = true, securityLevel = "high"), EmployeeRead(securityEvent = true), EmployeeUpdateDaycareRoles(securityEvent = true, securityLevel = "high"), EmployeeUpdateGlobalRoles(securityEvent = true, securityLevel = "high"), diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt index 230729f7cec..71c80cbc091 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt @@ -4,6 +4,7 @@ package fi.espoo.evaka.pis +import fi.espoo.evaka.Sensitive import fi.espoo.evaka.identity.ExternalId import fi.espoo.evaka.pairing.MobileDevice import fi.espoo.evaka.pairing.deleteDevice @@ -123,6 +124,28 @@ RETURNING id, preferred_first_name, first_name, last_name, email, external_id, c .exactlyOne() } +data class EmployeeSuomiFiLoginRequest( + val firstName: String, + val lastName: String, + val ssn: Sensitive, +) + +fun Database.Transaction.loginEmployeeWithSuomiFi( + now: HelsinkiDateTime, + request: EmployeeSuomiFiLoginRequest, +): Employee = + createUpdate { + sql( + """ +UPDATE employee +SET last_login = ${bind(now)}, first_name = ${bind(request.firstName)}, last_name = ${bind(request.lastName)}, active = TRUE +WHERE social_security_number = ${bind(request.ssn.value)} +""" + ) + } + .executeAndReturnGeneratedKeys() + .exactlyOne() + fun Database.Read.getEmployeeRoles(id: EmployeeId): EmployeeRoles = createQuery { sql( diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/SystemController.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/SystemController.kt index 94a823437cc..5e360571eca 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/SystemController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/SystemController.kt @@ -195,6 +195,48 @@ class SystemController( } } + @PostMapping("/system/employee-sfi-login") + fun employeeSuomiFiLogin( + db: Database, + user: AuthenticatedUser.SystemInternalUser, + clock: EvakaClock, + @RequestBody request: EmployeeSuomiFiLoginRequest, + ): EmployeeUser { + Audit.EmployeeSfiLoginAttempt.log( + targetId = AuditId(request.ssn.value), + meta = mapOf("lastName" to request.lastName, "firstName" to request.firstName), + ) + return db.connect { dbc -> + dbc.transaction { tx -> + val employee = tx.loginEmployeeWithSuomiFi(clock.now(), request) + val roles = tx.getEmployeeRoles(employee.id) + val employeeUser = + EmployeeUser( + id = employee.id, + firstName = employee.preferredFirstName ?: employee.firstName, + lastName = employee.lastName, + globalRoles = roles.globalRoles, + allScopedRoles = roles.allScopedRoles, + active = employee.active, + ) + tx.upsertEmployeeUser(employee.id) + employeeUser + } + } + .also { + Audit.EmployeeSfiLogin.log( + targetId = AuditId(request.ssn.value), + objectId = AuditId(it.id), + meta = + mapOf( + "lastName" to request.lastName, + "firstName" to request.firstName, + "globalRoles" to it.globalRoles, + ), + ) + } + } + @GetMapping("/system/employee/{id}") fun employeeUser( db: Database, diff --git a/service/src/main/resources/db/migration/V472__employee_ssn.sql b/service/src/main/resources/db/migration/V472__employee_ssn.sql new file mode 100644 index 00000000000..ac0e7a08348 --- /dev/null +++ b/service/src/main/resources/db/migration/V472__employee_ssn.sql @@ -0,0 +1 @@ +ALTER TABLE employee ADD COLUMN social_security_number text CONSTRAINT uniq$employee_ssn UNIQUE; diff --git a/service/src/main/resources/migrations.txt b/service/src/main/resources/migrations.txt index ff89aea65fc..8b7d638a4e8 100644 --- a/service/src/main/resources/migrations.txt +++ b/service/src/main/resources/migrations.txt @@ -467,3 +467,4 @@ V468__citizen_user.sql V469__income_statement_status.sql V470__cascade_citizen_user_delete.sql V471__application_confidentiality.sql +V472__employee_ssn.sql