This commit is contained in:
2021-01-14 22:53:48 +01:00
parent 91ab800272
commit caadc2a969
29 changed files with 50 additions and 56 deletions

View File

@@ -6,7 +6,7 @@ import fr.postgresjson.entity.*
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
data class ArticleForView ( data class ArticleForView(
override val id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
override val title: String, override val title: String,
val anonymous: Boolean = true, val anonymous: Boolean = true,
@@ -32,7 +32,7 @@ data class ArticleForView (
val lastVersion: Boolean = false val lastVersion: Boolean = false
} }
interface ArticleForUpdateI<C: CitizenRef> : ArticleI, ArticleWithTitleI, VersionableRef, TargetI, CreatedBy<C> { interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableRef, TargetI, CreatedBy<C> {
val anonymous: Boolean val anonymous: Boolean
val content: String val content: String
val description: String val description: String
@@ -40,7 +40,7 @@ interface ArticleForUpdateI<C: CitizenRef> : ArticleI, ArticleWithTitleI, Versio
val workgroup: WorkgroupRef? val workgroup: WorkgroupRef?
} }
class ArticleForUpdate ( class ArticleForUpdate(
id: UUID? = null, id: UUID? = null,
override val title: String, override val title: String,
override val anonymous: Boolean = true, override val anonymous: Boolean = true,

View File

@@ -6,17 +6,17 @@ import fr.dcproject.entity.VersionableRef
import fr.dcproject.voter.Voter import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse import fr.dcproject.voter.VoterResponse
class ArticleVoter(private val articleRepo: ArticleRepository): Voter() { class ArticleVoter(private val articleRepo: ArticleRepository) : Voter() {
fun <S: ArticleAuthI<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse = fun <S : ArticleAuthI<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) } canAll(subjects) { canView(it, citizen) }
fun <S: ArticleAuthI<*>> canView(subject: S, citizen: CitizenI?): VoterResponse { fun <S : ArticleAuthI<*>> canView(subject: S, citizen: CitizenI?): VoterResponse {
return if (subject.isDeleted()) denied("Article is deleted", "article.deleted") return if (subject.isDeleted()) denied("Article is deleted", "article.deleted")
else if (subject.draft && (citizen == null || subject.createdBy.id != citizen.id)) denied("Article is draft, but it's not yours", "article.draft.not.yours") else if (subject.draft && (citizen == null || subject.createdBy.id != citizen.id)) denied("Article is draft, but it's not yours", "article.draft.not.yours")
else granted() else granted()
} }
fun <S: CreatedBy<*>> canDelete(subject: S, citizen: CitizenI?): VoterResponse { fun <S : CreatedBy<*>> canDelete(subject: S, citizen: CitizenI?): VoterResponse {
if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected") if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected")
return if (subject.createdBy.id == citizen.id) { return if (subject.createdBy.id == citizen.id) {
granted() granted()
@@ -26,9 +26,9 @@ class ArticleVoter(private val articleRepo: ArticleRepository): Voter() {
} }
fun <S> canUpsert(subject: S, citizen: CitizenI?): VoterResponse fun <S> canUpsert(subject: S, citizen: CitizenI?): VoterResponse
where S: ArticleI, where S : ArticleI,
S: CreatedBy<*>, S : CreatedBy<*>,
S: VersionableRef { S : VersionableRef {
if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected") if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected")
/* The new Article must by created by the same citizen of the connected citizen */ /* The new Article must by created by the same citizen of the connected citizen */
if (subject.createdBy.id == citizen.id) { if (subject.createdBy.id == citizen.id) {

View File

@@ -37,7 +37,7 @@ private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<
) )
} }
fun Route.findArticles (repo: ArticleRepository, voter: ArticleVoter) { fun Route.findArticles(repo: ArticleRepository, voter: ArticleVoter) {
get<ArticlesRequest> { get<ArticlesRequest> {
repo.findArticles(it) repo.findArticles(it)
.apply { voter.assert { canView(result, citizenOrNull) } } .apply { voter.assert { canView(result, citizenOrNull) } }

View File

@@ -19,7 +19,6 @@ import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
import java.util.* import java.util.*
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles/{articleId}") @Location("/articles/{articleId}")
class ArticleRequest(val articleId: UUID) : KoinComponent { class ArticleRequest(val articleId: UUID) : KoinComponent {

View File

@@ -5,21 +5,21 @@ import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityDeletedAt import fr.postgresjson.entity.EntityDeletedAt
class CitizenVoter : Voter() { class CitizenVoter : Voter() {
fun <S> canView(subjects: List<S>, connectedCitizen: CitizenI?): VoterResponse where S : CitizenI, S: EntityDeletedAt = fun <S> canView(subjects: List<S>, connectedCitizen: CitizenI?): VoterResponse where S : CitizenI, S : EntityDeletedAt =
canAll(subjects) { canView(it, connectedCitizen) } canAll(subjects) { canView(it, connectedCitizen) }
fun <S> canView(subject: S, connectedCitizen: CitizenI?): VoterResponse where S : CitizenI, S: EntityDeletedAt { fun <S> canView(subject: S, connectedCitizen: CitizenI?): VoterResponse where S : CitizenI, S : EntityDeletedAt {
if (connectedCitizen == null) return denied("You must be connected to view citizen", "citizen.view.connected") if (connectedCitizen == null) return denied("You must be connected to view citizen", "citizen.view.connected")
return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted") return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted")
else granted() else granted()
} }
fun <S: CitizenI> canUpdate(subject: S, connectedCitizen: CitizenI?): VoterResponse { fun <S : CitizenI> canUpdate(subject: S, connectedCitizen: CitizenI?): VoterResponse {
if (connectedCitizen == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected") if (connectedCitizen == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected")
return if (subject.id == connectedCitizen.id) granted() else denied("You can only update your citizen", "citizen.update.notYours") return if (subject.id == connectedCitizen.id) granted() else denied("You can only update your citizen", "citizen.update.notYours")
} }
fun <S: CitizenI> canChangePassword(subject: S, connectedCitizen: CitizenI?): VoterResponse { fun <S : CitizenI> canChangePassword(subject: S, connectedCitizen: CitizenI?): VoterResponse {
if (connectedCitizen == null) return denied("You must be connected to change your password", "citizen.changePassword.notConnected") if (connectedCitizen == null) return denied("You must be connected to change your password", "citizen.changePassword.notConnected")
return if (subject.id == connectedCitizen.id) granted() else denied("You can only change your password", "citizen.password.notYours") return if (subject.id == connectedCitizen.id) granted() else denied("You can only change your password", "citizen.password.notYours")
} }

View File

@@ -35,7 +35,7 @@ class CommentForView<T : TargetI, C : CitizenRef>(
) )
} }
open class CommentForUpdate<T : TargetI, C: CitizenRef>( open class CommentForUpdate<T : TargetI, C : CitizenRef>(
override val id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
override val createdBy: C, override val createdBy: C,
override val target: T, override val target: T,
@@ -61,14 +61,14 @@ open class CommentForUpdate<T : TargetI, C: CitizenRef>(
) )
} }
open class CommentParent<T: TargetI>( open class CommentParent<T : TargetI>(
override val id: UUID, override val id: UUID,
override val deletedAt: DateTime?, override val deletedAt: DateTime?,
override val target: T override val target: T
) : CommentRef(id), ) : CommentRef(id),
CommentParentI<T> CommentParentI<T>
interface CommentParentI<T: TargetI> : CommentI, EntityDeletedAt, CommentWithTargetI<T> interface CommentParentI<T : TargetI> : CommentI, EntityDeletedAt, CommentWithTargetI<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T> interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T>

View File

@@ -67,7 +67,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
} }
} }
fun <I : T, C: CitizenRef> comment(comment: CommentForUpdate<I, C>) { fun <I : T, C : CitizenRef> comment(comment: CommentForUpdate<I, C>) {
requester requester
.getFunction("comment") .getFunction("comment")
.sendQuery( .sendQuery(

View File

@@ -23,7 +23,6 @@ class CommentChildrenRequest(
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
} }
@KtorExperimentalAPI @KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.getChildrenComments(repo: CommentRepository) { fun Route.getChildrenComments(repo: CommentRepository) {

View File

@@ -16,7 +16,6 @@ import io.ktor.util.*
@Location("/comments/{comment}") @Location("/comments/{comment}")
class CommentRequest(val comment: CommentRef) class CommentRequest(val comment: CommentRef)
@KtorExperimentalAPI @KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.getOneComment(repo: CommentRepository) { fun Route.getOneComment(repo: CommentRepository) {

View File

@@ -5,7 +5,7 @@ typealias Opinions = Map<String, Int>
interface Opinionable { interface Opinionable {
val opinions: Opinions val opinions: Opinions
class Imp(parent: fr.dcproject.entity.Opinionable): Opinionable { class Imp(parent: fr.dcproject.entity.Opinionable) : Opinionable {
override val opinions: Opinions = parent.opinions override val opinions: Opinions = parent.opinions
} }
} }

View File

@@ -12,4 +12,3 @@ interface Versionable {
override val versionId: UUID = parent.versionId override val versionId: UUID = parent.versionId
} }
} }

View File

@@ -3,7 +3,7 @@ package fr.dcproject.dto
interface Votable { interface Votable {
val votes: VoteAggregation val votes: VoteAggregation
class Imp(parent: fr.dcproject.entity.Votable): Votable { class Imp(parent: fr.dcproject.entity.Votable) : Votable {
override val votes: VoteAggregation = VoteAggregation(parent) override val votes: VoteAggregation = VoteAggregation(parent)
} }
} }

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
fun waitElasticsearchIsUp(client: RestClient) { fun waitElasticsearchIsUp(client: RestClient) {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch") val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch")
val request = Request("GET", "/_cluster/health") val request = Request("GET", "/_cluster/health")
repeat(5*60/2) { // 5 minutes repeat(5*60 / 2) { // 5 minutes
runCatching { runCatching {
client.performRequest(request).statusLine.statusCode client.performRequest(request).statusLine.statusCode
}.onSuccess { }.onSuccess {

View File

@@ -17,7 +17,7 @@ interface ExtraI<T : TargetI, C : CitizenI> :
EntityCreatedAt, EntityCreatedAt,
EntityCreatedBy<C> EntityCreatedBy<C>
interface AsTarget<T: TargetI> { interface AsTarget<T : TargetI> {
val target: T val target: T
} }

View File

@@ -24,7 +24,7 @@ open class FollowSimple<T : TargetI, C : CitizenI>(
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C> by EntityCreatedByImp(createdBy) EntityCreatedBy<C> by EntityCreatedByImp(createdBy)
class FollowForUpdate<T: TargetI, C: CitizenI>( class FollowForUpdate<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val target: T, override val target: T,
override val createdBy: C override val createdBy: C
@@ -36,4 +36,4 @@ open class FollowRef(
override val id: UUID override val id: UUID
) : FollowI ) : FollowI
interface FollowI: UuidEntityI interface FollowI : UuidEntityI

View File

@@ -30,7 +30,7 @@ class OpinionArticle(
choice: OpinionChoice choice: OpinionChoice
) : Opinion<ArticleRef>(id, createdBy, target, choice) ) : Opinion<ArticleRef>(id, createdBy, target, choice)
data class OpinionForUpdate<T: TargetI>( data class OpinionForUpdate<T : TargetI>(
override val id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
val target: T, val target: T,
val choice: OpinionChoice, val choice: OpinionChoice,
@@ -42,4 +42,4 @@ open class OpinionRef(
override val id: UUID override val id: UUID
) : OpinionI ) : OpinionI
interface OpinionI: UuidEntityI interface OpinionI : UuidEntityI

View File

@@ -7,11 +7,11 @@ interface VersionableRef {
val versionId: UUID val versionId: UUID
} }
class VersionableRefImp ( class VersionableRefImp(
override val versionId: UUID override val versionId: UUID
) : VersionableRef ) : VersionableRef
interface Versionable: VersionableRef, EntityVersioning<UUID, Int> { interface Versionable : VersionableRef, EntityVersioning<UUID, Int> {
override val versionId: UUID override val versionId: UUID
override val versionNumber: Int override val versionNumber: Int
} }

View File

@@ -24,7 +24,7 @@ class Vote<T : TargetI>(
} }
} }
class VoteForUpdate<T: TargetI, C: CitizenI>( class VoteForUpdate<T : TargetI, C : CitizenI>(
override val id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
override val note: Int, override val note: Int,
override val target: T, override val target: T,
@@ -33,14 +33,13 @@ class VoteForUpdate<T: TargetI, C: CitizenI>(
VoteForUpdateI<T, C>, VoteForUpdateI<T, C>,
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy) EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
interface VoteForUpdateI<T: TargetI, C: CitizenI> : VoteI, AsTarget<T>, EntityCreatedBy<C> { interface VoteForUpdateI<T : TargetI, C : CitizenI> : VoteI, AsTarget<T>, EntityCreatedBy<C> {
override val id: UUID override val id: UUID
val note: Int val note: Int
override val target: T override val target: T
override val createdBy: C override val createdBy: C
} }
open class VoteRef( open class VoteRef(
override val id: UUID override val id: UUID
) : VoteI ) : VoteI

View File

@@ -10,7 +10,7 @@ import fr.postgresjson.entity.EntityI
import java.util.* import java.util.*
@Deprecated("") @Deprecated("")
data class Workgroup <C: CitizenBasicI>( data class Workgroup <C : CitizenBasicI>(
override val id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
override var name: String, override var name: String,
override var description: String, override var description: String,

View File

@@ -42,11 +42,11 @@ class Workgroup(override var requester: Requester) : RepositoryI {
) )
} }
fun <C: CitizenI, W: WorkgroupSimple<C>> upsert(workgroup: W): WorkgroupEntity<CitizenBasic> = requester fun <C : CitizenI, W : WorkgroupSimple<C>> upsert(workgroup: W): WorkgroupEntity<CitizenBasic> = requester
.getFunction("upsert_workgroup") .getFunction("upsert_workgroup")
.selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null") .selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null")
fun <W: WorkgroupRef> delete(workgroup: W) = requester fun <W : WorkgroupRef> delete(workgroup: W) = requester
.getFunction("delete_workgroup") .getFunction("delete_workgroup")
.perform("id" to workgroup.id) .perform("id" to workgroup.id)

View File

@@ -20,8 +20,8 @@ class ConstitutionVoter : Voter<ApplicationCall> {
} }
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if(!((action is Action || action is CommentVoter.Action || action is VoteVoter.Action) if (!((action is Action || action is CommentVoter.Action || action is VoteVoter.Action) &&
&& (subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is CommentForView<*, *>))) return abstain() (subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is CommentForView<*, *>))) return abstain()
val user = context.user val user = context.user
if (action == Action.CREATE && user != null) { if (action == Action.CREATE && user != null) {

View File

@@ -11,8 +11,8 @@ class OpinionChoiceVoter : Voter<ApplicationCall> {
} }
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action) if (!((action is Action) &&
&& (subject is OpinionChoice?))) return abstain() (subject is OpinionChoice?))) return abstain()
if (action == Action.VIEW) { if (action == Action.VIEW) {
if (subject is OpinionChoice) { if (subject is OpinionChoice) {

View File

@@ -17,8 +17,8 @@ class OpinionVoter : Voter<ApplicationCall> {
} }
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action) if (!((action is Action) &&
&& (subject is Opinion<*>? || subject is ArticleAuthI<*>))) return abstain() (subject is Opinion<*>? || subject is ArticleAuthI<*>))) return abstain()
val user = context.user val user = context.user
if (action == Action.CREATE) { if (action == Action.CREATE) {

View File

@@ -26,7 +26,7 @@ class VoteVoter : Voter<ApplicationCall> {
subject.target.let { subject.target.let {
if (it is EntityDeletedAt) { if (it is EntityDeletedAt) {
if (it.isDeleted()) return denied("You cannot vote on deleted target", "vote.create.isDeleted") if (it.isDeleted()) return denied("You cannot vote on deleted target", "vote.create.isDeleted")
} else { } else {
throw NoSubjectDefinedException(action) throw NoSubjectDefinedException(action)
} }
} }

View File

@@ -25,12 +25,12 @@ abstract class Voter {
private fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: granted() private fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: granted()
protected fun <S: List<T>, T> canAll(items: S, action: (T) -> VoterResponse): VoterResponse = items protected fun <S : List<T>, T> canAll(items: S, action: (T) -> VoterResponse): VoterResponse = items
.map { action(it) } .map { action(it) }
.getOneResponse() .getOneResponse()
} }
fun <T: Voter> T.assert(action: T.() -> VoterResponse) { fun <T : Voter> T.assert(action: T.() -> VoterResponse) {
action().assert() action().assert()
} }
@@ -58,7 +58,7 @@ class VoterDeniedException(private val voterResponses: VoterResponses) : Throwab
.message .message
} }
sealed class VoterResponse ( sealed class VoterResponse(
val vote: Vote, val vote: Vote,
val voter: Voter, val voter: Voter,
val message: String?, val message: String?,

View File

@@ -27,8 +27,8 @@ class WorkgroupVoter : Voter<ApplicationCall> {
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action) if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
if (!((action is Action || action is ActionMembers) if (!((action is Action || action is ActionMembers) &&
&& (subject is WorkgroupI? || (subject is List<*> && subject.first() is WorkgroupI)))) return abstain() (subject is WorkgroupI? || (subject is List<*> && subject.first() is WorkgroupI)))) return abstain()
val user = context.user val user = context.user
if (action == Action.CREATE) { if (action == Action.CREATE) {
@@ -38,7 +38,6 @@ class WorkgroupVoter : Voter<ApplicationCall> {
} }
} }
if (action == Action.VIEW) { if (action == Action.VIEW) {
if (subject is WorkgroupWithAuthI<*>) { if (subject is WorkgroupWithAuthI<*>) {
return if (subject.isDeleted()) denied("You cannot view a deleted workgroup", "workgroup.view.deleted") return if (subject.isDeleted()) denied("You cannot view a deleted workgroup", "workgroup.view.deleted")

View File

@@ -134,7 +134,7 @@ class ArticleVoterTest {
} }
@Test @Test
fun `can update article if yours`(): Unit { fun `can update article if yours`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleVoter(getRepo(article)) ArticleVoter(getRepo(article))
.canUpsert(article, tesla) .canUpsert(article, tesla)
@@ -142,7 +142,7 @@ class ArticleVoterTest {
} }
@Test @Test
fun `can not update article if not yours`(): Unit { fun `can not update article if not yours`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleVoter(getRepo(article)) ArticleVoter(getRepo(article))
.canUpsert(article, einstein) .canUpsert(article, einstein)

View File

@@ -125,7 +125,7 @@ internal class CommentVoterTest {
} }
@Test @Test
fun `can be view the comment`(): Unit { fun `can be view the comment`() {
listOf(CommentVoter()).run { listOf(CommentVoter()).run {
mockk<ApplicationCall> { mockk<ApplicationCall> {
every { citizenOrNull } returns tesla every { citizenOrNull } returns tesla

View File

@@ -168,7 +168,7 @@ internal class VoteVoterTest {
} }
@Test @Test
fun `can be vote an article`(): Unit { fun `can be vote an article`() {
listOf(VoteVoter()).run { listOf(VoteVoter()).run {
mockk<ApplicationCall> { mockk<ApplicationCall> {
every { citizenOrNull } returns tesla every { citizenOrNull } returns tesla