Refactoring of FollowVoter

This commit is contained in:
2021-01-17 23:46:51 +01:00
parent d6840e8064
commit 55cd97078a
6 changed files with 67 additions and 123 deletions

View File

@@ -96,7 +96,6 @@ fun Application.module(env: Env = PROD) {
install(AuthorizationVoter) { install(AuthorizationVoter) {
voters = listOf( voters = listOf(
FollowVoter(),
OpinionVoter(), OpinionVoter(),
OpinionChoiceVoter() OpinionChoiceVoter()
) )
@@ -207,8 +206,8 @@ fun Application.module(env: Env = PROD) {
updateMemberOfWorkgroup(get(), get()) updateMemberOfWorkgroup(get(), get())
/* TODO */ /* TODO */
constitution(get(), get()) constitution(get(), get())
followArticle(get()) followArticle(get(), get())
followConstitution(get()) followConstitution(get(), get())
commentConstitution(get(), get()) commentConstitution(get(), get())
voteArticle(get(), get(), get(), get()) voteArticle(get(), get(), get(), get())
voteConstitution(get(), get()) voteConstitution(get(), get())

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.FollowVoter
import fr.dcproject.security.voter.VoteVoter 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
@@ -127,6 +128,7 @@ val KoinModule = module {
single { WorkgroupVoter() } single { WorkgroupVoter() }
single { ConstitutionVoter() } single { ConstitutionVoter() }
single { VoteVoter() } single { VoteVoter() }
single { FollowVoter() }
// Elasticsearch Client // Elasticsearch Client
single<RestClient> { single<RestClient> {

View File

@@ -2,11 +2,11 @@ package fr.dcproject.routes
import fr.dcproject.component.article.ArticleRef import fr.dcproject.component.article.ArticleRef
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.FollowForUpdate import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.security.voter.FollowVoter.Action.* import fr.dcproject.security.voter.FollowVoter
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.*
@@ -24,24 +24,24 @@ object FollowArticlePaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.followArticle(repo: FollowArticleRepository) { fun Route.followArticle(repo: FollowArticleRepository, voter: FollowVoter) {
post<FollowArticlePaths.ArticleFollowRequest> { post<FollowArticlePaths.ArticleFollowRequest> {
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
assertCan(CREATE, follow) voter.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow) repo.follow(follow)
call.respond(HttpStatusCode.Created) call.respond(HttpStatusCode.Created)
} }
delete<FollowArticlePaths.ArticleFollowRequest> { delete<FollowArticlePaths.ArticleFollowRequest> {
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
assertCan(DELETE, follow) voter.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow) repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
get<FollowArticlePaths.ArticleFollowRequest> { get<FollowArticlePaths.ArticleFollowRequest> {
repo.findFollow(citizen, it.article)?.let { follow -> repo.findFollow(citizen, it.article)?.let { follow ->
assertCan(VIEW, follow) voter.assert { canView(follow, citizenOrNull) }
call.respond(follow) call.respond(follow)
} ?: call.respond(HttpStatusCode.NoContent) } ?: call.respond(HttpStatusCode.NoContent)
} }
@@ -49,7 +49,7 @@ fun Route.followArticle(repo: FollowArticleRepository) {
get<FollowArticlePaths.CitizenFollowArticleRequest> { get<FollowArticlePaths.CitizenFollowArticleRequest> {
val follows = repo.findByCitizen(it.citizen) val follows = repo.findByCitizen(it.citizen)
if (follows.result.isNotEmpty()) { if (follows.result.isNotEmpty()) {
assertCanAll(VIEW, follows.result) voter.assert { canView(follows.result, citizenOrNull) }
} }
call.respond(follows) call.respond(follows)
} }

View File

@@ -1,12 +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.CitizenRef import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.ConstitutionRef import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.FollowForUpdate import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.security.voter.FollowVoter.Action.* import fr.dcproject.security.voter.FollowVoter
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.*
@@ -24,31 +24,31 @@ object FollowConstitutionPaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.followConstitution(repo: FollowConstitutionRepository) { fun Route.followConstitution(repo: FollowConstitutionRepository, voter: FollowVoter) {
post<FollowConstitutionPaths.ConstitutionFollowRequest> { post<FollowConstitutionPaths.ConstitutionFollowRequest> {
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
assertCan(CREATE, follow) voter.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow) repo.follow(follow)
call.respond(HttpStatusCode.Created) call.respond(HttpStatusCode.Created)
} }
delete<FollowConstitutionPaths.ConstitutionFollowRequest> { delete<FollowConstitutionPaths.ConstitutionFollowRequest> {
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
assertCan(DELETE, follow) voter.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow) repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
get<FollowConstitutionPaths.ConstitutionFollowRequest> { get<FollowConstitutionPaths.ConstitutionFollowRequest> {
repo.findFollow(citizen, it.constitution)?.let { follow -> repo.findFollow(citizen, it.constitution)?.let { follow ->
assertCan(VIEW, follow) voter.assert { canView(follow, citizenOrNull) }
call.respond(follow) call.respond(follow)
} ?: call.respond(HttpStatusCode.NotFound) } ?: call.respond(HttpStatusCode.NotFound)
} }
get<FollowConstitutionPaths.CitizenFollowConstitutionRequest> { get<FollowConstitutionPaths.CitizenFollowConstitutionRequest> {
val follows = repo.findByCitizen(it.citizen) val follows = repo.findByCitizen(it.citizen)
assertCanAll(VIEW, follows.result) voter.assert { canView(follows.result, citizenOrNull) }
call.respond(follows) call.respond(follows)
} }
} }

View File

@@ -1,46 +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.component.citizen.CitizenI
import fr.dcproject.entity.FollowI import fr.dcproject.entity.FollowI
import fr.dcproject.voter.NoSubjectDefinedException import fr.dcproject.voter.Voter
import fr.ktorVoter.* import fr.dcproject.voter.VoterResponse
import io.ktor.application.*
import fr.dcproject.entity.Follow as FollowEntity import fr.dcproject.entity.Follow as FollowEntity
class FollowVoter : Voter<ApplicationCall> { class FollowVoter : Voter() {
enum class Action : ActionI { fun canCreate(subject: FollowI, citizen: CitizenI?): VoterResponse {
CREATE, return if (citizen == null) denied("You must be connected to follow", "follow.create.notConnected")
DELETE, else granted()
VIEW
} }
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { fun canDelete(subject: FollowI, citizen: CitizenI?): VoterResponse {
if (action !is Action) return abstain() return if (citizen == null) denied("You must be connected to unfollow", "follow.delete.notConnected")
if (subject !is FollowI) throw NoSubjectDefinedException(action) else granted()
val citizen = context.citizenOrNull
if (action == Action.CREATE) {
return if (citizen == null) denied("You must be connected to follow", "follow.create.notConnected")
else granted()
}
if (action == Action.DELETE) {
return if (citizen == null) denied("You must be connected to unfollow", "follow.delete.notConnected")
else granted()
}
if (action == Action.VIEW) {
if (subject is FollowEntity<*>) {
return voteView(citizen, subject)
}
throw NoSubjectDefinedException(action)
}
return abstain()
} }
private fun voteView(citizen: CitizenI?, subject: FollowEntity<*>): VoterResponseI { fun <S : FollowEntity<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
fun canView(subject: FollowEntity<*>, citizen: CitizenI?): VoterResponse {
return if ((citizen != null && subject.createdBy.id == citizen.id) || !subject.createdBy.followAnonymous) granted() return if ((citizen != null && subject.createdBy.id == citizen.id) || !subject.createdBy.followAnonymous) granted()
else denied("You cannot view an anonymous follow", "follow.view.anonymous") else denied("You cannot view an anonymous follow", "follow.view.anonymous")
} }

View File

@@ -3,28 +3,21 @@ package unit.voter
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
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.Follow import fr.dcproject.entity.Follow
import fr.dcproject.security.voter.FollowVoter import fr.dcproject.security.voter.FollowVoter
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.*
@@ -109,88 +102,58 @@ internal class FollowVoterTest {
} }
@Test @Test
fun `support follow`(): Unit = FollowVoter().run { fun `can be view the follow`() {
val p = object : ActionI {} FollowVoter()
mockk<ApplicationCall> { .canView(follow1, tesla2)
every { citizenOrNull } returns tesla2 .vote `should be` GRANTED
}.let {
this(FollowVoter.Action.VIEW, it, follow1).vote `should be` Vote.GRANTED
assertThrows<NoSubjectDefinedException> {
this(FollowVoter.Action.VIEW, it, article1)
}
this(p, it, follow1).vote `should be` Vote.ABSTAIN
}
} }
@Test @Test
fun `can be view the follow`(): Unit = listOf(FollowVoter()).run { fun `can be view the follow list`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns tesla2 .canView(listOf(follow1), tesla2)
}.let { .vote `should be` GRANTED
can(FollowVoter.Action.VIEW, it, follow1) `should be` true
}
} }
@Test @Test
fun `can be view the follow list`(): Unit = listOf(FollowVoter()).run { fun `can be view your anonymous follow`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns tesla2 .canView(followAnon, einstein3)
}.let { .vote `should be` GRANTED
canAll(FollowVoter.Action.VIEW, it, listOf(follow1)) `should be` true
}
} }
@Test @Test
fun `can be view your anonymous follow`(): Unit = listOf(FollowVoter()).run { fun `can not be view the anonymous follow of other`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns einstein3 .canView(followAnon, tesla2)
}.let { .vote `should be` DENIED
can(FollowVoter.Action.VIEW, it, followAnon) `should be` true
}
} }
@Test @Test
fun `can not be view the anonymous follow of other`(): Unit = listOf(FollowVoter()).run { fun `can be follow article`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns tesla2 .canCreate(follow1, tesla2)
}.let { .vote `should be` GRANTED
can(FollowVoter.Action.VIEW, it, followAnon) `should be` false
}
} }
@Test @Test
fun `can be follow article`(): Unit = listOf(FollowVoter()).run { fun `can not be follow article if not connected`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns tesla2 .canCreate(follow1, null)
}.let { .vote `should be` DENIED
can(FollowVoter.Action.CREATE, it, follow1) `should be` true
}
} }
@Test @Test
fun `can not be follow article if not connected`(): Unit = listOf(FollowVoter()).run { fun `can be unfollow article`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns null .canDelete(follow1, tesla2)
}.let { .vote `should be` GRANTED
can(FollowVoter.Action.CREATE, it, follow1) `should be` false
}
} }
@Test @Test
fun `can be unfollow article`(): Unit = listOf(FollowVoter()).run { fun `can not be unfollow article if not connected`() {
mockk<ApplicationCall> { FollowVoter()
every { citizenOrNull } returns tesla2 .canDelete(follow1, null)
}.let { .vote `should be` DENIED
can(FollowVoter.Action.DELETE, it, follow1) `should be` true
}
}
@Test
fun `can not be unfollow article if not connected`(): Unit = listOf(FollowVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns null
}.let {
can(FollowVoter.Action.DELETE, it, follow1) `should be` false
}
} }
} }