diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/app/spring/security/SpringAdamCreation.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/app/spring/security/SpringAdamCreation.kt deleted file mode 100644 index f56cbe7..0000000 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/app/spring/security/SpringAdamCreation.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ru.vityaman.lms.botalka.app.spring.security - -import kotlinx.coroutines.runBlocking -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import ru.vityaman.lms.botalka.app.spring.storage.MainR2dbcConfig -import ru.vityaman.lms.botalka.app.spring.storage.SpringMigration -import ru.vityaman.lms.botalka.core.logging.Slf4jLog -import ru.vityaman.lms.botalka.core.logic.UserService -import ru.vityaman.lms.botalka.core.model.User -import ru.vityaman.lms.botalka.core.security.auth.AccessToken -import ru.vityaman.lms.botalka.core.security.auth.TokenService -import ru.vityaman.lms.botalka.core.storage.ConfigStorage -import ru.vityaman.lms.botalka.core.tx.TxEnv - -open class SpringAdam(val user: User, val token: AccessToken?) - -@Configuration -class SpringAdamCreation( - private val migration: SpringMigration, - private val users: UserService, - private val tokens: TokenService, - private val config: ConfigStorage, - - @Qualifier(MainR2dbcConfig.BeanName.TX_ENV) - private val txEnv: TxEnv, -) { - private val log = Slf4jLog("SpringAdamCreation") - - @Bean - fun adam(): SpringAdam = runBlocking { - migration.let { } - txEnv.transactional { - val user = users.getByAlias(alias)!! - log.info("Got $alias as $user") - - val token = if (config.isInitialized()) { - buildString { - append("Service is already initialized, ") - append("don't issue access token") - }.let { log.info(it) } - - null - } else { - log.info("Creating the world of LMS...") - tokens.issue(AccessToken.Payload(user.id)) - .also { log.warn("Adam token: '${it.text}'") } - } - - SpringAdam(user, token) - } - } - - companion object { - private val alias = User.Alias("adam") - } -} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/app/spring/security/SpringAdamSource.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/app/spring/security/SpringAdamSource.kt new file mode 100644 index 0000000..f0007c9 --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/app/spring/security/SpringAdamSource.kt @@ -0,0 +1,42 @@ +package ru.vityaman.lms.botalka.app.spring.security + +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import ru.vityaman.lms.botalka.app.spring.storage.MainR2dbcConfig +import ru.vityaman.lms.botalka.app.spring.storage.SpringMigration +import ru.vityaman.lms.botalka.core.logging.Slf4jLog +import ru.vityaman.lms.botalka.core.security.Adam +import ru.vityaman.lms.botalka.core.security.AdamSource +import ru.vityaman.lms.botalka.core.security.auth.TokenService +import ru.vityaman.lms.botalka.core.storage.ConfigStorage +import ru.vityaman.lms.botalka.core.storage.UserStorage +import ru.vityaman.lms.botalka.core.tx.TxEnv + +@Configuration +class SpringAdamSource( + migration: SpringMigration, + + users: UserStorage, + tokens: TokenService, + config: ConfigStorage, + + @Qualifier(MainR2dbcConfig.BeanName.TX_ENV) + txEnv: TxEnv, +) { + private val source = AdamSource( + users = users, + tokens = tokens, + config = config, + txEnv = txEnv, + log = Slf4jLog("SpringAdamCreation"), + ) + + init { + migration.let { } + } + + @Bean + fun adam(): Adam = runBlocking { source.adam() } +} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/event/domain/HomeworkEvents.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/event/domain/HomeworkEvents.kt index aa414b2..800c64d 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/event/domain/HomeworkEvents.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/event/domain/HomeworkEvents.kt @@ -3,6 +3,7 @@ package ru.vityaman.lms.botalka.core.event.domain import kotlinx.coroutines.flow.Flow import ru.vityaman.lms.botalka.core.event.Events import ru.vityaman.lms.botalka.core.model.Homework +import ru.vityaman.lms.botalka.core.storage.FetchPolicy import ru.vityaman.lms.botalka.core.storage.HomeworkStorage import ru.vityaman.lms.botalka.core.tx.TxEnv import java.time.Clock @@ -23,5 +24,5 @@ class HomeworkEvents( event.isPublished override suspend fun acquireById(id: Homework.Id): Homework = - storage.acquireById(id) + storage.getById(id, FetchPolicy.WRITE_LOCKED)!! } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/security/Adam.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/security/Adam.kt new file mode 100644 index 0000000..a862f91 --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/security/Adam.kt @@ -0,0 +1,45 @@ +package ru.vityaman.lms.botalka.core.security + +import ru.vityaman.lms.botalka.core.logging.Log +import ru.vityaman.lms.botalka.core.model.User +import ru.vityaman.lms.botalka.core.security.auth.AccessToken +import ru.vityaman.lms.botalka.core.security.auth.TokenService +import ru.vityaman.lms.botalka.core.storage.ConfigStorage +import ru.vityaman.lms.botalka.core.storage.FetchPolicy +import ru.vityaman.lms.botalka.core.storage.UserStorage +import ru.vityaman.lms.botalka.core.tx.TxEnv + +data class Adam(val user: User, val token: AccessToken?) + +class AdamSource( + private val users: UserStorage, + private val tokens: TokenService, + private val config: ConfigStorage, + private val txEnv: TxEnv, + private val log: Log, +) { + suspend fun adam(): Adam = txEnv.transactional { + val user = users.getByAlias(alias, FetchPolicy.WRITE_LOCKED)!! + log.info("Got $alias as $user") + + val token = if (config.isInitialized()) { + buildString { + append("Service is already initialized, ") + append("don't issue access token") + }.let { log.info(it) } + + null + } else { + log.info("Creating the world of LMS...") + tokens.issue(AccessToken.Payload(user.id)) + .also { config.markInitialized() } + .also { log.warn("Adam token: '${it.text}'") } + } + + Adam(user, token) + } + + companion object { + private val alias = User.Alias("adam") + } +} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/FetchPolicy.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/FetchPolicy.kt new file mode 100644 index 0000000..3fca843 --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/FetchPolicy.kt @@ -0,0 +1,6 @@ +package ru.vityaman.lms.botalka.core.storage + +enum class FetchPolicy { + SNAPSHOT, + WRITE_LOCKED, +} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/HomeworkStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/HomeworkStorage.kt index 09c039f..c1e0955 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/HomeworkStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/HomeworkStorage.kt @@ -6,9 +6,12 @@ import java.time.OffsetDateTime interface HomeworkStorage { suspend fun create(homework: Homework.Draft): Homework - suspend fun getById(id: Homework.Id): Homework? - suspend fun acquireById(id: Homework.Id): Homework + suspend fun getById( + id: Homework.Id, + policy: FetchPolicy = FetchPolicy.SNAPSHOT, + ): Homework? + fun publishableAt(moment: OffsetDateTime): Flow suspend fun markPublished(id: Homework.Id) } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/UserStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/UserStorage.kt index 22fe00e..6cb2ad2 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/UserStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/core/storage/UserStorage.kt @@ -7,7 +7,12 @@ import ru.vityaman.lms.botalka.core.model.User interface UserStorage { suspend fun getById(id: User.Id): User? - suspend fun getByAlias(alias: User.Alias): User? + + suspend fun getByAlias( + alias: User.Alias, + policy: FetchPolicy = FetchPolicy.SNAPSHOT, + ): User? + suspend fun create(user: User.Draft): User suspend fun create(teacher: Teacher) suspend fun create(student: Student) diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqFetchPolicy.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqFetchPolicy.kt new file mode 100644 index 0000000..c2ad21f --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqFetchPolicy.kt @@ -0,0 +1,12 @@ +package ru.vityaman.lms.botalka.storage.jooq + +import org.jooq.Record +import org.jooq.SelectConditionStep +import ru.vityaman.lms.botalka.core.storage.FetchPolicy + +internal fun SelectConditionStep.withPolicy( + policy: FetchPolicy, +) = when (policy) { + FetchPolicy.SNAPSHOT -> this + FetchPolicy.WRITE_LOCKED -> this.forUpdate() +} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqHomeworkStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqHomeworkStorage.kt index 12fe7ef..6968fac 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqHomeworkStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqHomeworkStorage.kt @@ -3,6 +3,7 @@ package ru.vityaman.lms.botalka.storage.jooq import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import ru.vityaman.lms.botalka.core.model.Homework +import ru.vityaman.lms.botalka.core.storage.FetchPolicy import ru.vityaman.lms.botalka.core.storage.HomeworkStorage import ru.vityaman.lms.botalka.storage.jooq.entity.toModel import ru.vityaman.lms.botalka.storage.jooq.tables.references.HOMEWORK @@ -32,19 +33,16 @@ class JooqHomeworkStorage( .coerce(HOMEWORK) }.toModel() - override suspend fun getById(id: Homework.Id): Homework? = + override suspend fun getById( + id: Homework.Id, + policy: FetchPolicy, + ): Homework? = database.maybe { selectFrom(HOMEWORK) .where(HOMEWORK.ID.eq(id.number)) + .withPolicy(policy) }?.toModel() - override suspend fun acquireById(id: Homework.Id): Homework = - database.only { - selectFrom(HOMEWORK) - .where(HOMEWORK.ID.eq(id.number)) - .forUpdate() - }.toModel() - override fun publishableAt( moment: OffsetDateTime, ): Flow = diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt index a0d48f3..eca45c3 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt @@ -6,6 +6,7 @@ import ru.vityaman.lms.botalka.core.model.Admin import ru.vityaman.lms.botalka.core.model.Student import ru.vityaman.lms.botalka.core.model.Teacher import ru.vityaman.lms.botalka.core.model.User +import ru.vityaman.lms.botalka.core.storage.FetchPolicy import ru.vityaman.lms.botalka.core.storage.UserStorage import ru.vityaman.lms.botalka.storage.jooq.entity.toModel import ru.vityaman.lms.botalka.storage.jooq.exception.UniqueViolationException @@ -23,10 +24,14 @@ class JooqUserStorage( .where(USER.ID.equal(id.number)) }?.toModel()?.equipped() - override suspend fun getByAlias(alias: User.Alias): User? = + override suspend fun getByAlias( + alias: User.Alias, + policy: FetchPolicy, + ): User? = database.maybe { selectFrom(USER) .where(USER.ALIAS.equal(alias.text)) + .withPolicy(policy) }?.toModel()?.equipped() override suspend fun create(user: User.Draft): User = try { diff --git a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/BotalkaTestSuite.kt b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/BotalkaTestSuite.kt index d56814a..0094716 100644 --- a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/BotalkaTestSuite.kt +++ b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/BotalkaTestSuite.kt @@ -11,11 +11,11 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import ru.vityaman.lms.botalka.app.spring.api.http.client.Api -import ru.vityaman.lms.botalka.app.spring.security.SpringAdam import ru.vityaman.lms.botalka.app.spring.storage.BrokerContainerInitializer import ru.vityaman.lms.botalka.app.spring.storage.DatabaseContainerInitializer import ru.vityaman.lms.botalka.app.spring.storage.MainR2dbcConfig import ru.vityaman.lms.botalka.app.spring.storage.SpringMigration +import ru.vityaman.lms.botalka.core.security.Adam import ru.vityaman.lms.botalka.storage.jooq.JooqDatabase import ru.vityaman.lms.botalka.storage.jooq.Lms.Companion.LMS as MainLMS @@ -40,7 +40,7 @@ abstract class BotalkaTestSuite { private lateinit var migration: SpringMigration @Autowired - private lateinit var adam: SpringAdam + private lateinit var adam: Adam protected lateinit var admin: Api diff --git a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/api/http/client/Api.kt b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/api/http/client/Api.kt index a53efba..24026ac 100644 --- a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/api/http/client/Api.kt +++ b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/api/http/client/Api.kt @@ -8,7 +8,7 @@ import ru.vityaman.lms.botalka.app.spring.api.http.client.apis.HomeworkApi import ru.vityaman.lms.botalka.app.spring.api.http.client.apis.MonitoringApi import ru.vityaman.lms.botalka.app.spring.api.http.client.apis.RatingApi import ru.vityaman.lms.botalka.app.spring.api.http.client.apis.UserApi -import ru.vityaman.lms.botalka.app.spring.security.SpringAdam +import ru.vityaman.lms.botalka.core.security.Adam class Api private constructor( private val data: Data? = null, @@ -43,7 +43,7 @@ class Api private constructor( fun ofNewbie() = Api() - fun ofAdam(adam: SpringAdam) = + fun ofAdam(adam: Adam) = Api(Data(adam.user.id.number, adam.token!!.text)) private suspend fun ofRegisteredAs(message: PostUserRequestMessage) = diff --git a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/security/AdamTest.kt b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/security/AdamTest.kt new file mode 100644 index 0000000..00860b5 --- /dev/null +++ b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/app/spring/security/AdamTest.kt @@ -0,0 +1,33 @@ +package ru.vityaman.lms.botalka.app.spring.security + +import io.kotest.common.runBlocking +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import ru.vityaman.lms.botalka.app.spring.BotalkaTestSuite +import java.util.concurrent.atomic.AtomicInteger + +class AdamTest( + @Autowired private val adam: SpringAdamSource, +) : BotalkaTestSuite() { + @Test + fun issueTokenExactlyOnce(): Unit = runBlocking { + val tokenIssuedCount = AtomicInteger(0) + + coroutineScope { + repeat(4) { + launch { + val adam = adam.adam() + if (adam.token != null) { + tokenIssuedCount.addAndGet(1) + } + } + } + } + + // As Adam was already created at startup + tokenIssuedCount shouldBe 0 + } +}