Skip to content

Commit

Permalink
1415: Revoke existing cards when user entitlements are revoked or exp…
Browse files Browse the repository at this point in the history
…ired
  • Loading branch information
seluianova committed Oct 14, 2024
1 parent db41432 commit 4fd7777
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class CardMutationService {
)
val userHash = Argon2IdHasher.hashKoblenzUserData(user)

val userEntitlements = transaction { UserEntitlementsRepository.findUserEntitlements(userHash.toByteArray()) }
val userEntitlements = transaction { UserEntitlementsRepository.findByUserHash(userHash.toByteArray()) }
if (userEntitlements == null || userEntitlements.revoked || userEntitlements.endDate.isBefore(LocalDate.now())) {
throw InvalidUserEntitlementsException()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
package app.ehrenamtskarte.backend.userdata.database

import org.jetbrains.exposed.sql.booleanLiteral
import org.jetbrains.exposed.sql.intLiteral
import org.jetbrains.exposed.sql.javatime.dateLiteral
import org.jetbrains.exposed.sql.javatime.timestampLiteral
import org.jetbrains.exposed.sql.upsert
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.update
import java.time.Instant
import java.time.LocalDate

object UserEntitlementsRepository {

fun insertOrUpdateUserData(userHash: ByteArray, startDate: LocalDate, endDate: LocalDate, revoked: Boolean, regionId: Int) {
UserEntitlements.upsert(
UserEntitlements.userHash,
onUpdate = listOf(
UserEntitlements.startDate to dateLiteral(startDate),
UserEntitlements.endDate to dateLiteral(endDate),
UserEntitlements.revoked to booleanLiteral(revoked),
UserEntitlements.regionId to intLiteral(regionId),
UserEntitlements.lastUpdated to timestampLiteral(Instant.now())
)
) {
fun insert(userHash: ByteArray, startDate: LocalDate, endDate: LocalDate, revoked: Boolean, regionId: Int) {
UserEntitlements.insert {
it[UserEntitlements.userHash] = userHash
it[UserEntitlements.startDate] = startDate
it[UserEntitlements.endDate] = endDate
it[UserEntitlements.revoked] = revoked
it[UserEntitlements.regionId] = regionId
it[lastUpdated] = Instant.now()
}
}

fun findUserEntitlements(userHash: ByteArray): UserEntitlementsEntity? {
fun update(userHash: ByteArray, startDate: LocalDate, endDate: LocalDate, revoked: Boolean, regionId: Int) {
UserEntitlements.update({ UserEntitlements.userHash eq userHash }) {
it[UserEntitlements.startDate] = startDate
it[UserEntitlements.endDate] = endDate
it[UserEntitlements.revoked] = revoked
it[UserEntitlements.regionId] = regionId
it[lastUpdated] = Instant.now()
}
}

fun findByUserHash(userHash: ByteArray): UserEntitlementsEntity? {
return UserEntitlementsEntity.find { UserEntitlements.userHash eq userHash }.firstOrNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.ehrenamtskarte.backend.auth.database.ApiTokenEntity
import app.ehrenamtskarte.backend.auth.database.PasswordCrypto
import app.ehrenamtskarte.backend.auth.database.repos.ApiTokensRepository
import app.ehrenamtskarte.backend.cards.Argon2IdHasher
import app.ehrenamtskarte.backend.cards.database.repos.CardRepository
import app.ehrenamtskarte.backend.config.BackendConfiguration
import app.ehrenamtskarte.backend.exception.service.ForbiddenException
import app.ehrenamtskarte.backend.exception.service.ProjectNotFoundException
Expand Down Expand Up @@ -124,13 +125,16 @@ class UserImportHandler(
val revoked = entry.get("revoked").toBooleanStrictOrNull()
?: throw UserImportException(entry.recordNumber, "Revoked must be a boolean value")

UserEntitlementsRepository.insertOrUpdateUserData(
userHash.toByteArray(),
startDate,
endDate,
revoked,
regionId
)
val userEntitlement = UserEntitlementsRepository.findByUserHash(userHash.toByteArray())

if (userEntitlement == null) {
UserEntitlementsRepository.insert(userHash.toByteArray(), startDate, endDate, revoked, regionId)
} else {
UserEntitlementsRepository.update(userHash.toByteArray(), startDate, endDate, revoked, regionId)
if (revoked || endDate <= LocalDate.now()) {
CardRepository.revokeByEntitlementId(userEntitlement.id.value)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import app.ehrenamtskarte.backend.helper.CardInfoTestSample
import app.ehrenamtskarte.backend.helper.ExampleCardInfo
import app.ehrenamtskarte.backend.helper.TestData
import app.ehrenamtskarte.backend.userdata.database.UserEntitlements
import app.ehrenamtskarte.backend.userdata.database.UserEntitlementsEntity
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.javalin.testtools.JavalinTest
import io.ktor.util.decodeBase64Bytes
Expand Down Expand Up @@ -150,12 +151,12 @@ internal class CreateCardFromSelfServiceTest : GraphqlApiTest() {
@Test
fun `POST returns a successful response when cards are created`() = JavalinTest.test(app) { _, client ->
val userRegionId = 95
val userEntitlements = TestData.createUserEntitlements(
val userEntitlementId = TestData.createUserEntitlements(
userHash = "\$argon2id\$v=19\$m=19456,t=2,p=1\$cr3lP9IMUKNz4BLfPGlAOHq1z98G5/2tTbhDIko35tY",
regionId = userRegionId
)
val oldDynamicCard = TestData.createDynamicCard(regionId = userRegionId, entitlementId = userEntitlements.id.value)
val oldStaticCard = TestData.createStaticCard(regionId = userRegionId, entitlementId = userEntitlements.id.value)
val oldDynamicCardId = TestData.createDynamicCard(regionId = userRegionId, entitlementId = userEntitlementId)
val oldStaticCardId = TestData.createStaticCard(regionId = userRegionId, entitlementId = userEntitlementId)

val encodedCardInfo = ExampleCardInfo.getEncoded(CardInfoTestSample.KoblenzPass)
val mutation = createMutation(encodedCardInfo = encodedCardInfo)
Expand All @@ -175,36 +176,33 @@ internal class CreateCardFromSelfServiceTest : GraphqlApiTest() {
transaction {
assertEquals(4, Cards.selectAll().count())

CardEntity.find { Cards.cardInfoHash eq oldDynamicCard.cardInfoHash }.single().let {
assertTrue(it.revoked)
}
assertTrue(CardEntity.find { Cards.id eq oldDynamicCardId }.single().revoked)
assertTrue(CardEntity.find { Cards.id eq oldStaticCardId }.single().revoked)

CardEntity.find { Cards.cardInfoHash eq oldStaticCard.cardInfoHash }.single().let {
assertTrue(it.revoked)
}
val userEntitlement = UserEntitlementsEntity.find { UserEntitlements.id eq userEntitlementId }.single()

CardEntity.find { Cards.cardInfoHash eq newDynamicActivationCode.decodeBase64Bytes() }.single().let {
assertNotNull(it.activationSecretHash)
assertNull(it.totpSecret)
assertEquals(userEntitlements.endDate.toEpochDay(), it.expirationDay)
assertEquals(userEntitlement.endDate.toEpochDay(), it.expirationDay)
assertFalse(it.revoked)
assertEquals(userRegionId, it.regionId.value)
assertNull(it.issuerId)
assertNull(it.firstActivationDate)
assertEquals(userEntitlements.startDate.toEpochDay(), it.startDay)
assertEquals(userEntitlements.id, it.entitlementId)
assertEquals(userEntitlement.startDate.toEpochDay(), it.startDay)
assertEquals(userEntitlement.id, it.entitlementId)
}

CardEntity.find { Cards.cardInfoHash eq newStaticVerificationCode.decodeBase64Bytes() }.single().let {
assertNull(it.activationSecretHash)
assertNull(it.totpSecret)
assertEquals(userEntitlements.endDate.toEpochDay(), it.expirationDay)
assertEquals(userEntitlement.endDate.toEpochDay(), it.expirationDay)
assertFalse(it.revoked)
assertEquals(userRegionId, it.regionId.value)
assertNull(it.issuerId)
assertNull(it.firstActivationDate)
assertEquals(userEntitlements.startDate.toEpochDay(), it.startDay)
assertEquals(userEntitlements.id, it.entitlementId)
assertEquals(userEntitlement.startDate.toEpochDay(), it.startDay)
assertEquals(userEntitlement.id, it.entitlementId)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package app.ehrenamtskarte.backend.helper
import app.ehrenamtskarte.backend.auth.database.AdministratorEntity
import app.ehrenamtskarte.backend.auth.database.ApiTokens
import app.ehrenamtskarte.backend.auth.database.PasswordCrypto
import app.ehrenamtskarte.backend.cards.database.CardEntity
import app.ehrenamtskarte.backend.cards.database.Cards
import app.ehrenamtskarte.backend.cards.database.CodeType
import app.ehrenamtskarte.backend.stores.database.AcceptingStoreEntity
Expand All @@ -12,7 +11,6 @@ import app.ehrenamtskarte.backend.stores.database.Addresses
import app.ehrenamtskarte.backend.stores.database.Contacts
import app.ehrenamtskarte.backend.stores.database.PhysicalStores
import app.ehrenamtskarte.backend.userdata.database.UserEntitlements
import app.ehrenamtskarte.backend.userdata.database.UserEntitlementsEntity
import net.postgis.jdbc.geometry.Point
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
Expand Down Expand Up @@ -92,16 +90,15 @@ object TestData {
endDate: LocalDate = LocalDate.now().plusYears(1L),
revoked: Boolean = false,
regionId: Int
): UserEntitlementsEntity {
): Int {
return transaction {
val result = UserEntitlements.insert {
UserEntitlements.insertAndGetId {
it[UserEntitlements.userHash] = userHash.toByteArray()
it[UserEntitlements.startDate] = startDate
it[UserEntitlements.endDate] = endDate
it[UserEntitlements.revoked] = revoked
it[UserEntitlements.regionId] = regionId
}.resultedValues!!.first()
UserEntitlementsEntity.wrapRow(result)
}.value
}
}

Expand All @@ -115,7 +112,7 @@ object TestData {
firstActivationDate: Instant? = null,
entitlementId: Int? = null,
startDay: Long? = null
): CardEntity {
): Int {
val fakeActivationSecretHash = Random.nextBytes(20)
return createCard(
fakeActivationSecretHash,
Expand All @@ -141,7 +138,7 @@ object TestData {
firstActivationDate: Instant? = null,
entitlementId: Int? = null,
startDay: Long? = null
): CardEntity {
): Int {
return createCard(
activationSecretHash = null,
totpSecret = null,
Expand Down Expand Up @@ -169,10 +166,10 @@ object TestData {
firstActivationDate: Instant? = null,
entitlementId: Int? = null,
startDay: Long? = null
): CardEntity {
): Int {
val fakeCardInfoHash = Random.nextBytes(20)
return transaction {
val result = Cards.insert {
Cards.insertAndGetId {
it[Cards.activationSecretHash] = activationSecretHash
it[Cards.totpSecret] = totpSecret
it[Cards.expirationDay] = expirationDay
Expand All @@ -185,8 +182,7 @@ object TestData {
it[Cards.firstActivationDate] = firstActivationDate
it[Cards.entitlementId] = entitlementId
it[Cards.startDay] = startDay
}.resultedValues!!.first()
CardEntity.wrapRow(result)
}.value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package app.ehrenamtskarte.backend.userdata

import app.ehrenamtskarte.backend.IntegrationTest
import app.ehrenamtskarte.backend.auth.database.ApiTokens
import app.ehrenamtskarte.backend.cards.database.CardEntity
import app.ehrenamtskarte.backend.cards.database.Cards
import app.ehrenamtskarte.backend.helper.TestAdministrators
import app.ehrenamtskarte.backend.helper.TestData
import app.ehrenamtskarte.backend.userdata.database.UserEntitlements
Expand All @@ -26,6 +28,7 @@ import java.io.File
import java.time.LocalDate
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

private const val USER_IMPORT_PATH = "/users/import"
private const val TEST_CSV_FILE_PATH = "build/tmp/test.csv"
Expand All @@ -43,6 +46,7 @@ internal class UserImportTest : IntegrationTest() {
@AfterEach
fun cleanUp() {
transaction {
Cards.deleteAll()
ApiTokens.deleteAll()
UserEntitlements.deleteAll()
}
Expand Down Expand Up @@ -318,7 +322,7 @@ internal class UserImportTest : IntegrationTest() {
}

@Test
fun `POST returns a successful response and new user entitlements are saved in db`() = JavalinTest.test(app) { _, client ->
fun `POST returns a successful response when new user entitlements are saved in db`() = JavalinTest.test(app) { _, client ->
TestData.createApiToken(creatorId = admin.id)

val csvFile = generateCsvFile(
Expand Down Expand Up @@ -381,6 +385,76 @@ internal class UserImportTest : IntegrationTest() {
}
}

@Test
fun `POST returns a successful response and existing cards are revoked when the user entitlement has been revoked`() = JavalinTest.test(app) { _, client ->
TestData.createApiToken(creatorId = admin.id)
val entitlementId = TestData.createUserEntitlements(
userHash = TEST_USER_HASH,
regionId = 1
)
val dynamicCardId = TestData.createDynamicCard(
regionId = 1,
entitlementId = entitlementId
)
val staticCardId = TestData.createStaticCard(
regionId = 1,
entitlementId = entitlementId
)

val csvFile = generateCsvFile(
TEST_CSV_FILE_PATH,
listOf("regionKey", "userHash", "startDate", "endDate", "revoked"),
listOf("07111", "\"$TEST_USER_HASH\"", "01.02.2024", "01.02.2025", "true")
)
val response = importUsers(client, csvFile)

assertEquals(200, response.code)

val jsonResponse = jacksonObjectMapper().readTree(response.body?.string())

assertEquals("Import successfully completed", jsonResponse["message"].asText())

transaction {
assertTrue(CardEntity.find { Cards.id eq dynamicCardId }.single().revoked)
assertTrue(CardEntity.find { Cards.id eq staticCardId }.single().revoked)
}
}

@Test
fun `POST returns a successful response and existing cards are revoked when the user entitlement has been expired`() = JavalinTest.test(app) { _, client ->
TestData.createApiToken(creatorId = admin.id)
val entitlementId = TestData.createUserEntitlements(
userHash = TEST_USER_HASH,
regionId = 1
)
val dynamicCardId = TestData.createDynamicCard(
regionId = 1,
entitlementId = entitlementId
)
val staticCardId = TestData.createStaticCard(
regionId = 1,
entitlementId = entitlementId
)

val csvFile = generateCsvFile(
TEST_CSV_FILE_PATH,
listOf("regionKey", "userHash", "startDate", "endDate", "revoked"),
listOf("07111", "\"$TEST_USER_HASH\"", "01.02.2024", "01.07.2024", "false")
)
val response = importUsers(client, csvFile)

assertEquals(200, response.code)

val jsonResponse = jacksonObjectMapper().readTree(response.body?.string())

assertEquals("Import successfully completed", jsonResponse["message"].asText())

transaction {
assertTrue(CardEntity.find { Cards.id eq dynamicCardId }.single().revoked)
assertTrue(CardEntity.find { Cards.id eq staticCardId }.single().revoked)
}
}

private fun importUsers(client: HttpClient, csvFile: File?, token: String? = "dummy"): Response {
val requestBuilder = Request.Builder().url(client.origin + USER_IMPORT_PATH)
if (token != null) {
Expand Down

0 comments on commit 4fd7777

Please sign in to comment.