Refactoring of OpinionVoter

This commit is contained in:
2021-01-18 09:51:48 +01:00
parent c196bfadbc
commit ba673943d8
14 changed files with 101 additions and 149 deletions

View File

@@ -42,7 +42,6 @@ import fr.dcproject.event.EventNotification
import fr.dcproject.event.EventSubscriber
import fr.dcproject.routes.*
import fr.dcproject.security.voter.OpinionChoiceVoter
import fr.dcproject.security.voter.OpinionVoter
import fr.ktorVoter.AuthorizationVoter
import fr.ktorVoter.VoterException
import fr.postgresjson.migration.Migrations
@@ -92,7 +91,6 @@ fun Application.module(env: Env = PROD) {
install(AuthorizationVoter) {
voters = listOf(
OpinionVoter(),
OpinionChoiceVoter()
)
}
@@ -177,7 +175,7 @@ fun Application.module(env: Env = PROD) {
commentConstitution(get(), get())
voteArticle(get(), get(), get(), get())
voteConstitution(get(), get())
opinionArticle(get())
opinionArticle(get(), get())
opinionChoice(get())
definition()
}

View File

@@ -25,6 +25,7 @@ import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.repository.CommentConstitutionRepository
import fr.dcproject.security.voter.ConstitutionVoter
import fr.dcproject.security.voter.FollowVoter
import fr.dcproject.security.voter.OpinionVoter
import fr.dcproject.security.voter.VoteVoter
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
@@ -129,6 +130,7 @@ val KoinModule = module {
single { ConstitutionVoter() }
single { VoteVoter() }
single { FollowVoter() }
single { OpinionVoter() }
// Elasticsearch Client
single<RestClient> {

View File

@@ -72,7 +72,7 @@ open class CommentParent<T : TargetI>(
interface CommentParentI<T : TargetI> : CommentI, EntityDeletedAt, CommentWithTargetI<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, HasTarget<T>
interface CommentWithParentI<T : TargetI> {
val parent: CommentParent<T>?

View File

@@ -1,7 +1,7 @@
package fr.dcproject.component.comment.generic
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.AsTarget
import fr.dcproject.entity.HasTarget
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityCreatedBy
@@ -23,7 +23,7 @@ class CommentVoter : Voter() {
where S : CommentI,
S : EntityCreatedBy<CR>,
S : CommentWithParentI<*>,
S : AsTarget<*> = when {
S : HasTarget<*> = when {
citizen == null -> denied("You must be connected to create user", "comment.create.notConnected")
subject.createdBy.id != citizen.id -> denied("You cannot create a comment with other user than yours", "comment.create.wrongUser")
subject.parent?.isDeleted() ?: false -> denied("You cannot create a comment on deleted parent", "comment.create.deletedParent")

View File

@@ -13,11 +13,11 @@ import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI, C : CitizenI> :
UuidEntityI,
AsTarget<T>,
HasTarget<T>,
EntityCreatedAt,
EntityCreatedBy<C>
interface AsTarget<T : TargetI> {
interface HasTarget<T : TargetI> {
val target: T
}
@@ -45,7 +45,7 @@ interface TargetI : UuidEntityI {
t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(Opinion::class) -> TargetName.Opinion.targetReference
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
}
}

View File

@@ -29,7 +29,7 @@ class FollowForUpdate<T : TargetI, C : CitizenI>(
override val target: T,
override val createdBy: C
) : FollowRef(id),
AsTarget<T>,
HasTarget<T>,
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
open class FollowRef(

View File

@@ -14,8 +14,8 @@ open class Opinion<T : TargetI>(
override val createdBy: CitizenBasic,
override val target: T,
val choice: OpinionChoice
) : ExtraI<T, CitizenBasicI>,
TargetRef(id),
) : OpinionRef(id),
ExtraI<T, CitizenBasicI>,
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
@@ -32,14 +32,15 @@ class OpinionArticle(
data class OpinionForUpdate<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
val target: T,
val choice: OpinionChoice,
override val target: T,
val choice: OpinionChoiceRef,
override val createdBy: CitizenRef
) : OpinionRef(id),
HasTarget<T>,
EntityCreatedBy<CitizenI> by EntityCreatedByImp(createdBy)
open class OpinionRef(
override val id: UUID
) : OpinionI
) : OpinionI, TargetRef(id)
interface OpinionI : UuidEntityI

View File

@@ -33,7 +33,7 @@ class VoteForUpdate<T : TargetI, C : CitizenI>(
VoteForUpdateI<T, C>,
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
interface VoteForUpdateI<T : TargetI, C : CitizenI> : VoteI, AsTarget<T>, EntityCreatedBy<C> {
interface VoteForUpdateI<T : TargetI, C : CitizenI> : VoteI, HasTarget<T>, EntityCreatedBy<C> {
override val id: UUID
val note: Int
override val target: T

View File

@@ -2,8 +2,6 @@ package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.OpinionForUpdate
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
@@ -67,9 +65,9 @@ abstract class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requ
/**
* Create an Opinion on target (article,...)
*/
abstract fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>>
fun updateOpinions(choice: OpinionChoiceRef, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>> =
updateOpinions(listOf(choice), citizen, target)
abstract fun updateOpinions(opinions: List<OpinionForUpdate<*>>): List<OpinionEntity<T>>
fun updateOpinions(opinion: OpinionForUpdate<*>): List<OpinionEntity<T>> =
updateOpinions(listOf(opinion))
abstract fun addOpinion(opinion: OpinionForUpdate<T>): OpinionEntity<T>
@@ -135,14 +133,15 @@ class OpinionArticle(requester: Requester) : Opinion<ArticleRef>(requester) {
/**
* Update Opinions on Article (Delete old one)
*/
override fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionArticleEntity> {
override fun updateOpinions(opinions: List<OpinionForUpdate<*>>): List<OpinionArticleEntity> {
return requester
/* TODO change SQL function to not use .first() and pass all createdBy and target */
.getFunction("update_citizen_opinions_by_target_id")
.select(
"choices_ids" to choices.map { it.id },
"citizen_id" to citizen.id,
"target_id" to target.id,
"target_reference" to target.reference
"choices_ids" to opinions.map { it.choice.id },
"citizen_id" to opinions.first().createdBy.id,
"target_id" to opinions.first().target.id,
"target_reference" to opinions.first().target.reference
)
}

View File

@@ -1,14 +1,14 @@
package fr.dcproject.routes
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
import fr.dcproject.entity.*
import fr.dcproject.security.voter.OpinionVoter
import fr.dcproject.utils.toUUID
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
@@ -41,7 +41,7 @@ object OpinionArticlePaths {
@KtorExperimentalAPI
class ArticleOpinion(val article: ArticleForView) {
class Body(ids: List<String>) {
val ids = ids.map { OpinionChoiceRef(it.toUUID()) }
val ids: List<UUID> = ids.map { it.toUUID() }
}
}
@@ -51,29 +51,35 @@ object OpinionArticlePaths {
@Location("/citizens/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>) : KoinComponent {
val id: List<UUID> = id.toUUID()
val opinionsEntities = get<OpinionArticleRepository>()
val opinionsEntities: List<Opinion<ArticleRef>> = get<OpinionArticleRepository>()
.findCitizenOpinionsByTargets(citizen, this.id)
}
}
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.opinionArticle(repo: OpinionArticleRepository) {
fun Route.opinionArticle(repo: OpinionArticleRepository, voter: OpinionVoter) {
get<OpinionArticlePaths.CitizenOpinionArticleRequest> {
val opinions = repo.findCitizenOpinions(citizen, it.page, it.limit)
call.respond(opinions)
}
get<OpinionArticlePaths.CitizenOpinions> {
assertCanAll(VIEW, it.opinionsEntities)
voter.assert { canView(it.opinionsEntities, citizenOrNull) }
call.respond(it.opinionsEntities)
}
put<OpinionArticlePaths.ArticleOpinion> {
call.receive<OpinionArticlePaths.ArticleOpinion.Body>().ids.let { choices ->
assertCan(CREATE, it.article)
repo.updateOpinions(choices, citizen, it.article)
call.receive<OpinionArticlePaths.ArticleOpinion.Body>().ids.map { id ->
OpinionForUpdate(
choice = OpinionChoiceRef(id),
target = it.article,
createdBy = citizen
)
}.let { opinions ->
voter.assert { canCreate(opinions, citizenOrNull) }
repo.updateOpinions(opinions)
}.let {
call.respond(HttpStatusCode.Created, it)
}

View File

@@ -1,48 +1,35 @@
package fr.dcproject.security.voter
import fr.dcproject.component.article.ArticleAuthI
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.auth.user
import fr.dcproject.entity.Opinion
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.HasTarget
import fr.dcproject.entity.OpinionI
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityDeletedAt
class OpinionVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
CREATE,
VIEW,
DELETE
class OpinionVoter : Voter() {
fun <S> canCreate(subjects: List<S>, citizen: CitizenI?): VoterResponse where S : OpinionI, S : HasTarget<*> =
canAll(subjects) { canCreate(it, citizen) }
fun <S> canCreate(subject: S, citizen: CitizenI?): VoterResponse where S : OpinionI, S : HasTarget<*> {
val target = subject.target
return when {
citizen == null -> denied("You must be connected to make an opinion", "opinion.create.notConnected")
target is EntityDeletedAt && target.isDeleted() -> denied("You cannot make opinion on deleted target", "opinion.create.deletedTarget")
else -> granted()
}
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action) &&
(subject is Opinion<*>? || subject is ArticleAuthI<*>))) return abstain()
fun <S : OpinionI, SS : List<S>> canView(subjects: SS, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
val user = context.user
if (action == Action.CREATE) {
if (user == null) return denied("You must be connected to make an opinion", "opinion.create.notConnected")
if (subject is ArticleAuthI<*> && !subject.isDeleted()) return granted()
if (subject is Opinion<*> && subject.createdBy.user.id == user.id) return granted()
fun <S : OpinionI> canView(subject: S, citizen: CitizenI?): VoterResponse = granted()
throw NoSubjectDefinedException(action)
}
if (action == Action.VIEW) {
return if (subject is Opinion<*> || subject is ArticleForView) granted() else throw NoSubjectDefinedException(action)
}
if (action == Action.DELETE) {
if (user == null) return denied("You must be connected to delete opinion", "opinion.delete.notConnected")
if (subject !is Opinion<*>) throw NoSubjectDefinedException(action)
return if (subject.createdBy.user.id == user.id) granted() else denied("You can only delete your opinions", "opinion.delete.notYours")
}
if (action is Action) {
throw NoRuleDefinedException(action)
}
return abstain()
fun <S, C : CitizenI> canDelete(subject: S, citizen: CitizenI?): VoterResponse where S : EntityCreatedBy<C>, S : OpinionI = when {
citizen == null -> denied("You must be connected to delete opinion", "opinion.delete.notConnected")
subject.createdBy.id != citizen.id -> denied("You can only delete your opinions", "opinion.delete.notYours")
else -> granted()
}
}