Skip to content

Commit

Permalink
HAI-1873 Add API for identifying a user
Browse files Browse the repository at this point in the history
Add an endpoint that takes a user token in the request body and creates
a permission for the user to the hanke the token is associated with.

The token is deleted from DB after use, since it can't be used again
anyway.

The motivation is to activate the permissions that have been created for
a contact person. When creating a contact person we don't know which
user the contact person is, so we create a user token, that's emailed to
the contact. The contact can then use this endpoint (through UI) to
activate the given permission, i.e. change the token placeholder to an
actual permission.

As an unreleated change, configure springdoc to use application/json as
the default for all API descriptions. This way, we don't have to specify
the media type for every single response, including all the error
responses.

Also, configure springdoc to use a reference for the HankeError enum, so
the definition isn't copied for each error response in the OpenAPI JSON.
This cut down the size of the JSON file to about half.
  • Loading branch information
corvidian committed Aug 31, 2023
1 parent bf9f250 commit bff8d50
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import fi.hel.haitaton.hanke.factory.HankeKayttajaFactory
import fi.hel.haitaton.hanke.hankeError
import fi.hel.haitaton.hanke.hasSameElementsAs
import fi.hel.haitaton.hanke.logging.DisclosureLogService
import fi.hel.haitaton.hanke.permissions.HankeKayttajaController.Tunnistautuminen
import fi.hel.haitaton.hanke.permissions.PermissionCode.VIEW
import io.mockk.Called
import io.mockk.checkUnnecessaryStub
Expand Down Expand Up @@ -334,6 +335,74 @@ class HankeKayttajaControllerITest(@Autowired override val mockMvc: MockMvc) : C
return Pair(hanke, updates)
}
}

@Nested
inner class IdentifyUser {
private val url = "/kayttajat"
private val tunniste = "r5cmC0BmJaSX5Q6WA981ow8j"
private val tunnisteId = UUID.fromString("827fe492-2add-4d87-9564-049f963c1d86")
private val kayttajaId = UUID.fromString("ee239f7a-c5bb-4462-b9a5-3695eb410086")
private val permissionId = 156

@Test
fun `Returns 204 on success`() {
justRun { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) }

post(url, Tunnistautuminen(tunniste))
.andExpect(status().isNoContent)
.andExpect(content().string(""))

verify { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) }
}

@Test
fun `Returns 404 when tunniste not found`() {
every { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) } throws
TunnisteNotFoundException(USERNAME, tunniste)

post(url, Tunnistautuminen(tunniste))
.andExpect(status().isNotFound)
.andExpect(hankeError(HankeError.HAI4004))

verify { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) }
}

@Test
fun `Returns 500 when tunniste is orphaned`() {
every { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) } throws
OrphanedTunnisteException(USERNAME, tunnisteId)

post(url, Tunnistautuminen(tunniste))
.andExpect(status().isInternalServerError)
.andExpect(hankeError(HankeError.HAI4001))

verify { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) }
}

@Test
fun `Returns 409 when user already has a permission`() {
every { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) } throws
UserAlreadyHasPermissionException(USERNAME, tunnisteId, permissionId)

post(url, Tunnistautuminen(tunniste))
.andExpect(status().isConflict)
.andExpect(hankeError(HankeError.HAI4003))

verify { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) }
}

@Test
fun `Returns 409 when other user already has a permission for the hanke kayttaja`() {
every { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) } throws
PermissionAlreadyExistsException(USERNAME, "Other user", kayttajaId, permissionId)

post(url, Tunnistautuminen(tunniste))
.andExpect(status().isConflict)
.andExpect(hankeError(HankeError.HAI4003))

verify { hankeKayttajaService.createPermissionFromToken(USERNAME, tunniste) }
}
}
}

@WebMvcTest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.matches
import assertk.assertions.messageContains
import com.fasterxml.jackson.databind.util.ClassUtil.hasClass
import fi.hel.haitaton.hanke.DatabaseTest
import fi.hel.haitaton.hanke.factory.AlluDataFactory
import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.withContacts
Expand Down Expand Up @@ -633,6 +634,121 @@ class HankeKayttajaServiceITest : DatabaseTest() {
}
}

@Nested
inner class CreatePermissionFromToken {
private val tunniste = "Itf4UuErPqBHkhJF7CUAsu69"
private val newUserId = "newUser"

@Test
fun `throws exception if tunniste doesn't exist`() {
assertFailure { hankeKayttajaService.createPermissionFromToken(newUserId, "fake") }
.all {
hasClass(TunnisteNotFoundException::class)
messageContains(newUserId)
messageContains("fake")
}
}

@Test
fun `throws exception if there's no kayttaja with the tunniste`() {
val tunniste = saveToken()

assertFailure { hankeKayttajaService.createPermissionFromToken(newUserId, "existing") }
.all {
hasClass(OrphanedTunnisteException::class)
messageContains(newUserId)
messageContains(tunniste.id.toString())
}
}

@Test
fun `throws an exception if the user already has a permission for the hanke kayttaja`() {
val tunniste = saveToken()
val hanke = hankeFactory.save()
val kayttaja =
saveUserAndPermission(
hanke.id!!,
kayttajaTunniste = tunniste,
userId = newUserId,
)

assertFailure { hankeKayttajaService.createPermissionFromToken(newUserId, "existing") }
.all {
hasClass(UserAlreadyHasPermissionException::class)
messageContains(newUserId)
messageContains(tunniste.id.toString())
messageContains(kayttaja.permission!!.id.toString())
}
}

@Test
fun `throws an exception if the user has a permission for the hanke from elsewhere`() {
val hanke = hankeFactory.save()
val kayttaja = saveUserAndToken(hanke.id!!, tunniste = tunniste)
val permission =
permissionRepository.save(
PermissionEntity(
userId = newUserId,
hankeId = hanke.id!!,
role = roleRepository.findOneByRole(Role.KATSELUOIKEUS)
)
)

assertFailure { hankeKayttajaService.createPermissionFromToken(newUserId, tunniste) }
.all {
hasClass(UserAlreadyHasPermissionException::class)
messageContains(newUserId)
messageContains(kayttaja.kayttajaTunniste!!.id.toString())
messageContains(permission.id.toString())
}
}

@Test
fun `throws an exception if another user already has a permission for the hanke kayttaja`() {
val tunniste = saveToken()
val hanke = hankeFactory.save()
val kayttaja =
saveUserAndPermission(
hanke.id!!,
kayttajaTunniste = tunniste,
userId = "Other user",
)

assertFailure { hankeKayttajaService.createPermissionFromToken(newUserId, "existing") }
.all {
hasClass(PermissionAlreadyExistsException::class)
messageContains(newUserId)
messageContains("Other user")
messageContains(kayttaja.id.toString())
messageContains(kayttaja.permission!!.id.toString())
}
}

@Test
fun `Creates a permission`() {
val hanke = hankeFactory.save()
saveUserAndToken(hanke.id!!, tunniste = tunniste)

hankeKayttajaService.createPermissionFromToken(newUserId, tunniste)

val permission = permissionRepository.findOneByHankeIdAndUserId(hanke.id!!, newUserId)
assertThat(permission)
.isNotNull()
.transform { it.role.role }
.isEqualTo(Role.KATSELUOIKEUS)
}

@Test
fun `Removes the user token`() {
val hanke = hankeFactory.save()
saveUserAndToken(hanke.id!!, tunniste = tunniste)

hankeKayttajaService.createPermissionFromToken(newUserId, tunniste)

assertThat(kayttajaTunnisteRepository.findAll()).isEmpty()
}
}

private fun Assert<List<KayttajaTunnisteEntity>>.areValid() = each { t ->
t.transform { it.id }.isNotNull()
t.transform { it.role }.isEqualTo(Role.KATSELUOIKEUS)
Expand Down Expand Up @@ -688,16 +804,7 @@ class HankeKayttajaServiceITest : DatabaseTest() {
role: Role = Role.KATSELUOIKEUS,
tunniste: String = "existing",
): HankeKayttajaEntity {
val kayttajaTunnisteEntity =
kayttajaTunnisteRepository.save(
KayttajaTunnisteEntity(
tunniste = tunniste,
createdAt = OffsetDateTime.parse("2023-03-31T15:41:21Z"),
sentAt = null,
role = role,
hankeKayttaja = null,
)
)
val kayttajaTunnisteEntity = saveToken(tunniste, role)
return saveUser(hankeId, nimi, sahkoposti, null, kayttajaTunnisteEntity)
}

Expand Down Expand Up @@ -732,6 +839,20 @@ class HankeKayttajaServiceITest : DatabaseTest() {
)
}

private fun saveToken(
tunniste: String = "existing",
role: Role = Role.KATSELUOIKEUS,
) =
kayttajaTunnisteRepository.save(
KayttajaTunnisteEntity(
tunniste = tunniste,
createdAt = OffsetDateTime.parse("2023-03-31T15:41:21Z"),
sentAt = null,
role = role,
hankeKayttaja = null,
)
)

private fun savePermission(hankeId: Int, userId: String, role: Role) =
permissionRepository.save(
PermissionEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package fi.hel.haitaton.hanke

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonUnwrapped
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.ConstraintViolation

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
@Schema(enumAsRef = true)
enum class HankeError(val errorMessage: String) {
HAI0001("Access denied"),
HAI0002("Internal error"),
Expand Down Expand Up @@ -44,6 +46,7 @@ enum class HankeError(val errorMessage: String) {
HAI4001("HankeKayttaja not found"),
HAI4002("Trying to change own permission"),
HAI4003("Permission data conflict"),
HAI4004("Kayttajatunniste not found"),
;

val errorCode: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ class HankeKayttajaEntity(
val sahkoposti: String,
@OneToOne
@JoinColumn(name = "permission_id", updatable = true, nullable = true)
val permission: PermissionEntity?,
var permission: PermissionEntity?,
@OneToOne
@JoinColumn(name = "tunniste_id", updatable = true, nullable = true)
val kayttajaTunniste: KayttajaTunnisteEntity?,
var kayttajaTunniste: KayttajaTunnisteEntity?,
) {
fun toDto(): HankeKayttajaDto =
HankeKayttajaDto(
Expand Down
Loading

0 comments on commit bff8d50

Please sign in to comment.