From 3a565a1febb623deb1aa0488a0f4b0aaf70c4ad7 Mon Sep 17 00:00:00 2001 From: Topias Heinonen Date: Tue, 22 Aug 2023 09:44:05 +0300 Subject: [PATCH] HAI-1527 Audit logging for permission updates --- .../permissions/HankeKayttajaServiceITest.kt | 65 +++++++++++++++++++ .../haitaton/hanke/logging/AuditLogEntry.kt | 8 ++- .../logging/HankeKayttajaLoggingService.kt | 56 ++++++++++++++++ .../hanke/permissions/HankeKayttajaService.kt | 9 ++- .../hanke/permissions/KayttajaTunniste.kt | 15 +++++ .../haitaton/hanke/permissions/Permissions.kt | 15 ++++- 6 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/HankeKayttajaLoggingService.kt diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt index ed130904d..c381df7fe 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt @@ -7,6 +7,7 @@ import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.each +import assertk.assertions.first import assertk.assertions.hasClass import assertk.assertions.hasSize import assertk.assertions.isEmpty @@ -24,7 +25,12 @@ import fi.hel.haitaton.hanke.factory.HankeFactory import fi.hel.haitaton.hanke.factory.HankeFactory.Companion.withGeneratedOmistaja import fi.hel.haitaton.hanke.factory.HankeFactory.Companion.withYhteystiedot import fi.hel.haitaton.hanke.factory.HankeYhteystietoFactory +import fi.hel.haitaton.hanke.logging.AuditLogRepository +import fi.hel.haitaton.hanke.logging.ObjectType +import fi.hel.haitaton.hanke.logging.Operation +import fi.hel.haitaton.hanke.logging.UserRole import fi.hel.haitaton.hanke.test.Asserts.isRecent +import fi.hel.haitaton.hanke.toChangeLogJsonString import java.time.OffsetDateTime import java.util.UUID import org.junit.jupiter.api.Nested @@ -52,6 +58,7 @@ class HankeKayttajaServiceITest : DatabaseTest() { @Autowired private lateinit var hankeKayttajaRepository: HankeKayttajaRepository @Autowired private lateinit var permissionRepository: PermissionRepository @Autowired private lateinit var roleRepository: RoleRepository + @Autowired private lateinit var auditLogRepository: AuditLogRepository @Autowired private lateinit var permissionService: PermissionService @@ -378,6 +385,36 @@ class HankeKayttajaServiceITest : DatabaseTest() { } } + @Test + fun `Writes permission update to audit log`() { + val hanke = createHankeWithAdminHankeKayttaja() + val kayttaja = saveUserAndPermission(hanke.id!!) + val updates = mapOf(kayttaja.id to Role.HANKEMUOKKAUS) + auditLogRepository.deleteAll() + + hankeKayttajaService.updatePermissions(hanke, updates, false, USERNAME) + + val logs = auditLogRepository.findAll() + assertThat(logs).hasSize(1) + assertThat(logs) + .first() + .transform { it.message.auditEvent } + .all { + transform { it.operation }.isEqualTo(Operation.UPDATE) + transform { it.actor.role }.isEqualTo(UserRole.USER) + transform { it.actor.userId }.isEqualTo(USERNAME) + transform { it.target.id }.isEqualTo(kayttaja.permission?.id.toString()) + transform { it.target.type }.isEqualTo(ObjectType.PERMISSION) + val permission = kayttaja.permission!!.toDomain() + transform { it.target.objectBefore } + .isEqualTo(permission.toChangeLogJsonString()) + transform { it.target.objectAfter } + .isEqualTo( + permission.copy(role = Role.HANKEMUOKKAUS).toChangeLogJsonString() + ) + } + } + @Test fun `Updates role to tunniste if permission doesn't exist`() { val hanke = createHankeWithAdminHankeKayttaja() @@ -393,6 +430,34 @@ class HankeKayttajaServiceITest : DatabaseTest() { } } + @Test + fun `Writes tunniste update to audit log`() { + val hanke = createHankeWithAdminHankeKayttaja() + val kayttaja = saveUserAndToken(hanke.id!!, "Toinen Tohelo", "urho@kekkonen.test") + val updates = mapOf(kayttaja.id to Role.HANKEMUOKKAUS) + auditLogRepository.deleteAll() + + hankeKayttajaService.updatePermissions(hanke, updates, false, USERNAME) + + val logs = auditLogRepository.findAll() + assertThat(logs).hasSize(1) + assertThat(logs) + .first() + .transform { it.message.auditEvent } + .all { + transform { it.operation }.isEqualTo(Operation.UPDATE) + transform { it.actor.role }.isEqualTo(UserRole.USER) + transform { it.actor.userId }.isEqualTo(USERNAME) + transform { it.target.id }.isEqualTo(kayttaja.kayttajaTunniste?.id.toString()) + transform { it.target.type }.isEqualTo(ObjectType.KAYTTAJA_TUNNISTE) + val tunniste = + kayttaja.kayttajaTunniste!!.toDomain().copy(hankeKayttajaId = kayttaja.id) + transform { it.target.objectBefore }.isEqualTo(tunniste.toChangeLogJsonString()) + transform { it.target.objectAfter } + .isEqualTo(tunniste.copy(role = Role.HANKEMUOKKAUS).toChangeLogJsonString()) + } + } + @Test fun `Updates role to only permission if both permission and tunniste exist`() { val hanke = createHankeWithAdminHankeKayttaja() diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/AuditLogEntry.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/AuditLogEntry.kt index 4788019dd..ec8661b13 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/AuditLogEntry.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/AuditLogEntry.kt @@ -43,13 +43,15 @@ enum class Status { } enum class ObjectType { - YHTEYSTIETO, - ALLU_CUSTOMER, + APPLICATION, ALLU_CONTACT, + ALLU_CUSTOMER, GDPR_RESPONSE, HANKE, HANKE_KAYTTAJA, - APPLICATION, + KAYTTAJA_TUNNISTE, + PERMISSION, + YHTEYSTIETO, } enum class UserRole { diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/HankeKayttajaLoggingService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/HankeKayttajaLoggingService.kt new file mode 100644 index 000000000..e04c33a9c --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/logging/HankeKayttajaLoggingService.kt @@ -0,0 +1,56 @@ +package fi.hel.haitaton.hanke.logging + +import fi.hel.haitaton.hanke.application.Application +import fi.hel.haitaton.hanke.permissions.KayttajaTunniste +import fi.hel.haitaton.hanke.permissions.PermissionEntity +import fi.hel.haitaton.hanke.permissions.Role +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional + +@Service +class HankeKayttajaLoggingService(private val auditLogService: AuditLogService) { + + @Transactional(propagation = Propagation.MANDATORY) + fun logCreate(savedApplication: Application, userId: String) { + auditLogService.create( + AuditLogService.createEntry(userId, ObjectType.APPLICATION, savedApplication) + ) + } + + @Transactional(propagation = Propagation.MANDATORY) + fun logUpdate(roleBefore: Role, permissionEntityAfter: PermissionEntity, userId: String) { + val permissionAfter = permissionEntityAfter.toDomain() + val permissionBefore = permissionAfter.copy(role = roleBefore) + + AuditLogService.updateEntry( + userId, + ObjectType.PERMISSION, + permissionBefore, + permissionAfter, + ) + ?.let { auditLogService.create(it) } + } + + @Transactional(propagation = Propagation.MANDATORY) + fun logUpdate( + kayttajaTunnisteBefore: KayttajaTunniste, + kayttajaTunnisteAfter: KayttajaTunniste, + userId: String + ) { + AuditLogService.updateEntry( + userId, + ObjectType.KAYTTAJA_TUNNISTE, + kayttajaTunnisteBefore, + kayttajaTunnisteAfter, + ) + ?.let { auditLogService.create(it) } + } + + @Transactional(propagation = Propagation.MANDATORY) + fun logDelete(applicationBefore: Application, userId: String) { + auditLogService.create( + AuditLogService.deleteEntry(userId, ObjectType.APPLICATION, applicationBefore) + ) + } +} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt index 7ae74942e..de486fc81 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt @@ -5,6 +5,7 @@ import fi.hel.haitaton.hanke.application.ApplicationEntity import fi.hel.haitaton.hanke.configuration.Feature import fi.hel.haitaton.hanke.configuration.FeatureFlags import fi.hel.haitaton.hanke.domain.Hanke +import fi.hel.haitaton.hanke.logging.HankeKayttajaLoggingService import java.util.UUID import mu.KotlinLogging import org.springframework.stereotype.Service @@ -19,6 +20,7 @@ class HankeKayttajaService( private val roleRepository: RoleRepository, private val permissionService: PermissionService, private val featureFlags: FeatureFlags, + private val logService: HankeKayttajaLoggingService, ) { @Transactional(readOnly = true) @@ -82,9 +84,14 @@ class HankeKayttajaService( kayttajat.forEach { kayttaja -> if (kayttaja.permission != null) { + val roleBefore = kayttaja.permission.role.role kayttaja.permission.role = roleRepository.findOneByRole(updates[kayttaja.id]!!) + logService.logUpdate(roleBefore, kayttaja.permission, userId) } else { - kayttaja.kayttajaTunniste!!.role = updates[kayttaja.id]!! + val kayttajaTunnisteBefore = kayttaja.kayttajaTunniste!!.toDomain() + kayttaja.kayttajaTunniste.role = updates[kayttaja.id]!! + val kayttajaTunnisteAfter = kayttaja.kayttajaTunniste.toDomain() + logService.logUpdate(kayttajaTunnisteBefore, kayttajaTunnisteAfter, userId) } } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/KayttajaTunniste.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/KayttajaTunniste.kt index e0fa75522..9378d5707 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/KayttajaTunniste.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/KayttajaTunniste.kt @@ -1,5 +1,8 @@ package fi.hel.haitaton.hanke.permissions +import com.fasterxml.jackson.annotation.JsonView +import fi.hel.haitaton.hanke.ChangeLogView +import fi.hel.haitaton.hanke.domain.HasId import fi.hel.haitaton.hanke.getCurrentTimeUTC import jakarta.persistence.Column import jakarta.persistence.Entity @@ -15,6 +18,16 @@ import kotlin.streams.asSequence import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository +@JsonView(ChangeLogView::class) +data class KayttajaTunniste( + override val id: UUID, + val tunniste: String, + val createdAt: OffsetDateTime, + val sentAt: OffsetDateTime?, + var role: Role, + val hankeKayttajaId: UUID? +) : HasId + @Entity @Table(name = "kayttaja_tunniste") class KayttajaTunnisteEntity( @@ -26,6 +39,8 @@ class KayttajaTunnisteEntity( @OneToOne(mappedBy = "kayttajaTunniste") val hankeKayttaja: HankeKayttajaEntity? ) { + fun toDomain() = KayttajaTunniste(id, tunniste, createdAt, sentAt, role, hankeKayttaja?.id) + companion object { private const val tokenLength: Int = 24 private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/Permissions.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/Permissions.kt index d7b9bac81..a14e2a728 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/Permissions.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/Permissions.kt @@ -1,5 +1,8 @@ package fi.hel.haitaton.hanke.permissions +import com.fasterxml.jackson.annotation.JsonView +import fi.hel.haitaton.hanke.ChangeLogView +import fi.hel.haitaton.hanke.domain.HasId import jakarta.persistence.Entity import jakarta.persistence.FetchType import jakarta.persistence.GeneratedValue @@ -52,4 +55,14 @@ class PermissionEntity( @ManyToOne(optional = false, fetch = FetchType.EAGER) @JoinColumn(name = "roleid") var role: RoleEntity, -) +) { + fun toDomain() = Permission(id, userId, hankeId, role.role) +} + +@JsonView(ChangeLogView::class) +data class Permission( + override val id: Int, + val userId: String, + val hankeId: Int, + var role: Role, +) : HasId