Refactoring of VoteVoter

This commit is contained in:
2021-01-17 23:32:43 +01:00
parent 308a284280
commit d6840e8064
6 changed files with 57 additions and 144 deletions

View File

@@ -96,7 +96,6 @@ fun Application.module(env: Env = PROD) {
install(AuthorizationVoter) { install(AuthorizationVoter) {
voters = listOf( voters = listOf(
VoteVoter(),
FollowVoter(), FollowVoter(),
OpinionVoter(), OpinionVoter(),
OpinionChoiceVoter() OpinionChoiceVoter()
@@ -211,8 +210,8 @@ fun Application.module(env: Env = PROD) {
followArticle(get()) followArticle(get())
followConstitution(get()) followConstitution(get())
commentConstitution(get(), get()) commentConstitution(get(), get())
voteArticle(get(), get(), get()) voteArticle(get(), get(), get(), get())
voteConstitution(get()) voteConstitution(get(), get())
opinionArticle(get()) opinionArticle(get())
opinionChoice(get()) opinionChoice(get())
definition() definition()

View File

@@ -24,6 +24,7 @@ import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.repository.CommentConstitutionRepository import fr.dcproject.repository.CommentConstitutionRepository
import fr.dcproject.security.voter.ConstitutionVoter import fr.dcproject.security.voter.ConstitutionVoter
import fr.dcproject.security.voter.VoteVoter
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
@@ -125,6 +126,7 @@ val KoinModule = module {
single { CommentVoter() } single { CommentVoter() }
single { WorkgroupVoter() } single { WorkgroupVoter() }
single { ConstitutionVoter() } single { ConstitutionVoter() }
single { VoteVoter() }
// Elasticsearch Client // Elasticsearch Client
single<RestClient> { single<RestClient> {

View File

@@ -2,17 +2,16 @@ package fr.dcproject.routes
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.comment.generic.CommentRepository import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.entity.VoteForUpdate import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.repository.VoteComment import fr.dcproject.repository.VoteComment
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest
import fr.dcproject.security.voter.VoteVoter.Action.CREATE import fr.dcproject.security.voter.VoteVoter
import fr.dcproject.security.voter.VoteVoter.Action.VIEW
import fr.dcproject.utils.toUUID import fr.dcproject.utils.toUUID
import fr.ktorVoter.assertCan import fr.dcproject.voter.assert
import fr.ktorVoter.assertCanAll
import io.ktor.application.* import io.ktor.application.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.locations.* import io.ktor.locations.*
@@ -49,7 +48,7 @@ object VoteArticlePaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentRepository) { fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentRepository, voter: VoteVoter) {
put<ArticleVoteRequest> { put<ArticleVoteRequest> {
val content = call.receive<ArticleVoteRequest.Content>() val content = call.receive<ArticleVoteRequest.Content>()
val vote = VoteForUpdate( val vote = VoteForUpdate(
@@ -57,7 +56,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
note = content.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
assertCan(CREATE, vote) voter.assert { canCreate(vote, citizenOrNull) }
val votes = repo.vote(vote) val votes = repo.vote(vote)
call.respond(HttpStatusCode.Created, votes) call.respond(HttpStatusCode.Created, votes)
} }
@@ -70,14 +69,14 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
note = content.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
assertCan(CREATE, vote) voter.assert { canCreate(vote, citizenOrNull) }
val votes = voteCommentRepo.vote(vote) val votes = voteCommentRepo.vote(vote)
call.respond(HttpStatusCode.Created, votes) call.respond(HttpStatusCode.Created, votes)
} }
get<VoteArticlePaths.CitizenVoteArticleRequest> { get<VoteArticlePaths.CitizenVoteArticleRequest> {
val votes = repo.findByCitizen(it.citizen, it.page, it.limit) val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
assertCanAll(VIEW, votes.result) voter.assert { canView(votes.result, citizenOrNull) }
call.respond(votes) call.respond(votes)
} }
@@ -85,7 +84,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
get<VoteArticlePaths.CitizenVotesByIdsRequest> { get<VoteArticlePaths.CitizenVotesByIdsRequest> {
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id) val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
if (votes.isNotEmpty()) { if (votes.isNotEmpty()) {
assertCanAll(VIEW, votes) voter.assert { canView(votes, citizenOrNull) }
} }
call.respond(votes) call.respond(votes)
} }

View File

@@ -1,11 +1,12 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.VoteForUpdate import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
import fr.dcproject.security.voter.VoteVoter.Action.CREATE import fr.dcproject.security.voter.VoteVoter
import fr.ktorVoter.assertCan import fr.dcproject.voter.assert
import io.ktor.application.* import io.ktor.application.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.locations.* import io.ktor.locations.*
@@ -27,7 +28,7 @@ object VoteConstitutionPaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.voteConstitution(repo: VoteConstitutionRepository) { fun Route.voteConstitution(repo: VoteConstitutionRepository, voter: VoteVoter) {
put<VoteConstitutionPaths.ConstitutionVoteRequest> { put<VoteConstitutionPaths.ConstitutionVoteRequest> {
val content = call.receive<Content>() val content = call.receive<Content>()
val vote = VoteForUpdate( val vote = VoteForUpdate(
@@ -35,7 +36,7 @@ fun Route.voteConstitution(repo: VoteConstitutionRepository) {
note = content.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
assertCan(CREATE, vote) voter.assert { canCreate(vote, citizenOrNull) }
repo.vote(vote) repo.vote(vote)
call.respond(HttpStatusCode.Created) call.respond(HttpStatusCode.Created)
} }

View File

@@ -1,50 +1,26 @@
package fr.dcproject.security.voter package fr.dcproject.security.voter
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.VoteForUpdateI import fr.dcproject.entity.VoteForUpdateI
import fr.dcproject.entity.VoteI import fr.dcproject.voter.Voter
import fr.dcproject.voter.NoSubjectDefinedException import fr.dcproject.voter.VoterResponse
import fr.ktorVoter.*
import fr.postgresjson.entity.EntityDeletedAt import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
import fr.dcproject.entity.Vote as VoteEntity import fr.dcproject.entity.Vote as VoteEntity
class VoteVoter : Voter<ApplicationCall> { class VoteVoter : Voter() {
enum class Action : ActionI { fun <S> canCreate(subject: VoteForUpdateI<S, *>, citizen: CitizenI?): VoterResponse where S : EntityDeletedAt, S : TargetI = when {
CREATE, citizen == null -> denied("You must be connected for vote", "vote.create.connected")
VIEW subject.target.isDeleted() -> denied("You cannot vote on deleted target", "vote.create.isDeleted")
else -> granted()
} }
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { fun <S : VoteEntity<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action) canAll(subjects) { canView(it, citizen) }
if (!(action is Action && subject is VoteI)) return abstain()
val citizen = context.citizenOrNull ?: return denied("You must be connected for vote", "vote.connected") fun canView(subject: VoteEntity<*>, citizen: CitizenI?): VoterResponse = when {
citizen == null -> denied("You must be connected for view your votes", "vote.view.connected")
if (action == Action.CREATE) { subject.createdBy.id != citizen.id -> denied("You can only display your votes", "vote.view.onlyYours")
if (subject !is VoteForUpdateI<*, *>) throw NoSubjectDefinedException(action) else -> granted()
subject.target.let {
if (it is EntityDeletedAt) {
if (it.isDeleted()) return denied("You cannot vote on deleted target", "vote.create.isDeleted")
} else {
throw NoSubjectDefinedException(action)
}
}
return granted()
}
if (action == Action.VIEW) {
if (subject is VoteEntity<*>) {
return if (subject.createdBy.id != citizen.id) {
denied("You can view only your votes", "vote.view")
} else {
granted()
}
} else {
throw NoSubjectDefinedException(action)
}
}
return abstain()
} }
} }

View File

@@ -4,28 +4,21 @@ import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.User import fr.dcproject.component.auth.User
import fr.dcproject.component.auth.UserI import fr.dcproject.component.auth.UserI
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenBasic import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.VoteForUpdate import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.security.voter.VoteVoter import fr.dcproject.security.voter.VoteVoter
import fr.dcproject.voter.NoSubjectDefinedException import fr.dcproject.voter.Vote.DENIED
import fr.ktorVoter.ActionI import fr.dcproject.voter.Vote.GRANTED
import fr.ktorVoter.Vote
import fr.ktorVoter.can
import fr.ktorVoter.canAll
import io.ktor.application.* import io.ktor.application.*
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
import java.util.* import java.util.*
@@ -120,101 +113,44 @@ internal class VoteVoterTest {
} }
@Test @Test
fun `support vote`(): Unit = VoteVoter().run { fun `can be view your the vote`() {
val p = object : ActionI {} VoteVoter()
mockk<ApplicationCall> { .canView(vote1, tesla)
every { citizenOrNull } returns tesla .vote `should be` GRANTED
}.let {
this(VoteVoter.Action.VIEW, it, vote1).vote `should be` Vote.GRANTED
this(VoteVoter.Action.VIEW, it, article1).vote `should be` Vote.ABSTAIN
this(p, it, vote1).vote `should be` Vote.ABSTAIN
}
} }
@Test @Test
fun `can be view your the vote`(): Unit = listOf(VoteVoter()).run { fun `can not be view vote of other`() {
mockk<ApplicationCall> { VoteVoter()
every { citizenOrNull } returns tesla .canView(vote1, einstein)
}.let { .vote `should be` DENIED
can(VoteVoter.Action.VIEW, it, vote1) `should be` true
}
} }
@Test @Test
fun `can not be view vote of other`(): Unit = listOf(VoteVoter()).run { fun `can be view your votes list`() {
mockk<ApplicationCall> { VoteVoter()
every { citizenOrNull } returns einstein .canView(listOf(vote1), tesla)
}.let { .vote `should be` GRANTED
can(VoteVoter.Action.VIEW, it, vote1) `should be` false
}
}
@Test
fun `can be not view the vote if is null`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
assertThrows<NoSubjectDefinedException> {
can(VoteVoter.Action.VIEW, it, null)
}
}
}
@Test
fun `can be view your votes list`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
canAll(VoteVoter.Action.VIEW, it, listOf(vote1)) `should be` true
}
} }
@Test @Test
fun `can be vote an article`() { fun `can be vote an article`() {
listOf(VoteVoter()).run { VoteVoter()
mockk<ApplicationCall> { .canCreate(voteForUpdate, tesla)
every { citizenOrNull } returns tesla .vote `should be` GRANTED
}.let {
can(VoteVoter.Action.CREATE, it, voteForUpdate) `should be` true
}
}
} }
@Test @Test
fun `can not be vote if not connected`(): Unit = listOf(VoteVoter()).run { fun `can not be vote if not connected`() {
mockk<ApplicationCall> { VoteVoter()
every { citizenOrNull } returns null .canCreate(voteForUpdate, null)
}.let { .vote `should be` DENIED
can(VoteVoter.Action.CREATE, it, voteForUpdate) `should be` false
}
} }
@Test @Test
fun `can not be vote an article if article is deleted`(): Unit = listOf(VoteVoter()).run { fun `can not be vote an article if article is deleted`() {
mockk<ApplicationCall> { VoteVoter()
every { citizenOrNull } returns tesla .canCreate(voteOnDeleted, tesla)
}.let { .vote `should be` DENIED
can(VoteVoter.Action.CREATE, it, voteOnDeleted) `should be` false
}
}
@Test
fun `can not be vote an article if article have no user`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
assertThrows<NoSubjectDefinedException> {
can(VoteVoter.Action.CREATE, it, voteWithoutTargetUser)
}
}
}
@Test
fun `can not be comment an article if article is deleted`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
can(VoteVoter.Action.CREATE, it, voteOnDeleted) `should be` false
}
} }
} }