Big refactoring #77

Merged
flecomte merged 166 commits from refactoring-component-and-immutable into master 2021-03-24 19:06:07 +01:00
13 changed files with 169 additions and 192 deletions
Showing only changes of commit 7c106f7cf8 - Show all commits

View File

@@ -16,7 +16,6 @@ import fr.dcproject.component.citizen.routes.changeMyPassword
import fr.dcproject.component.citizen.routes.findCitizen import fr.dcproject.component.citizen.routes.findCitizen
import fr.dcproject.component.citizen.routes.getCurrentCitizen import fr.dcproject.component.citizen.routes.getCurrentCitizen
import fr.dcproject.component.citizen.routes.getOneCitizen import fr.dcproject.component.citizen.routes.getOneCitizen
import fr.dcproject.component.comment.generic.CommentVoter
import fr.dcproject.component.comment.generic.routes.createCommentChildren import fr.dcproject.component.comment.generic.routes.createCommentChildren
import fr.dcproject.component.comment.generic.routes.editComment import fr.dcproject.component.comment.generic.routes.editComment
import fr.dcproject.component.comment.generic.routes.getChildrenComments import fr.dcproject.component.comment.generic.routes.getChildrenComments
@@ -80,7 +79,6 @@ fun Application.module(env: Env = PROD) {
install(AuthorizationVoter) { install(AuthorizationVoter) {
voters = listOf( voters = listOf(
ConstitutionVoter(), ConstitutionVoter(),
CommentVoter(),
VoteVoter(), VoteVoter(),
FollowVoter(), FollowVoter(),
OpinionVoter(), OpinionVoter(),
@@ -170,17 +168,17 @@ fun Application.module(env: Env = PROD) {
getCurrentCitizen(get()) getCurrentCitizen(get())
changeMyPassword(get(), get()) changeMyPassword(get(), get())
/* Comment */ /* Comment */
editComment(get()) editComment(get(), get())
getOneComment(get()) getOneComment(get(), get())
createCommentChildren(get()) createCommentChildren(get(), get())
getChildrenComments(get()) getChildrenComments(get(), get())
/* TODO */ /* TODO */
auth(get(), get(), get()) auth(get(), get(), get())
constitution(get()) constitution(get())
followArticle(get()) followArticle(get())
followConstitution(get()) followConstitution(get())
commentArticle(get()) commentArticle(get(), get())
commentConstitution(get()) commentConstitution(get(), get())
voteArticle(get(), get(), get()) voteArticle(get(), get(), get())
voteConstitution(get()) voteConstitution(get())
opinionArticle(get()) opinionArticle(get())

View File

@@ -14,6 +14,7 @@ import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.component.citizen.CitizenRepository import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.component.citizen.CitizenVoter import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.component.comment.article.CommentArticleRepository import fr.dcproject.component.comment.article.CommentArticleRepository
import fr.dcproject.component.comment.generic.CommentVoter
import fr.dcproject.event.publisher.Publisher import fr.dcproject.event.publisher.Publisher
import fr.dcproject.messages.Mailer import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender import fr.dcproject.messages.NotificationEmailSender
@@ -119,6 +120,7 @@ val KoinModule = module {
// Voters // Voters
single { ArticleVoter(get()) } single { ArticleVoter(get()) }
single { CitizenVoter() } single { CitizenVoter() }
single { CommentVoter() }
// Elasticsearch Client // Elasticsearch Client
single<RestClient> { single<RestClient> {

View File

@@ -16,6 +16,7 @@ class CommentForView<T : TargetI, C : CitizenRef>(
val childrenCount: Int? = null, val childrenCount: Int? = null,
override val deletedAt: DateTime? = null override val deletedAt: DateTime? = null
) : ExtraI<T, C>, ) : ExtraI<T, C>,
CommentWithParentI<T>,
CommentForUpdate<T, C>(id, createdBy, target, content, parent, deletedAt), CommentForUpdate<T, C>(id, createdBy, target, content, parent, deletedAt),
CommentWithTargetI<T>, CommentWithTargetI<T>,
EntityCreatedBy<C> by EntityCreatedByImp(createdBy), EntityCreatedBy<C> by EntityCreatedByImp(createdBy),
@@ -40,9 +41,10 @@ open class CommentForUpdate<T : TargetI, C : CitizenRef>(
override val createdBy: C, override val createdBy: C,
override val target: T, override val target: T,
open var content: String, open var content: String,
open val parent: CommentParent<T>? = null, override val parent: CommentParent<T>? = null,
override val deletedAt: DateTime? = null override val deletedAt: DateTime? = null
) : CommentParent<T>(id, deletedAt, target), ) : CommentParent<T>(id, deletedAt, target),
CommentWithParentI<T>,
ExtraI<T, C>, ExtraI<T, C>,
CommentWithTargetI<T>, CommentWithTargetI<T>,
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
@@ -72,6 +74,10 @@ interface CommentParentI<T : TargetI> : CommentI, EntityDeletedAt, CommentWithTa
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T> interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T>
interface CommentWithParentI<T : TargetI> {
val parent: CommentParent<T>?
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id) open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
interface CommentI : EntityI interface CommentI : EntityI

View File

@@ -1,61 +1,41 @@
package fr.dcproject.component.comment.generic package fr.dcproject.component.comment.generic
import fr.dcproject.citizenOrNull import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.voter.NoRuleDefinedException import fr.dcproject.entity.AsTarget
import fr.dcproject.voter.NoSubjectDefinedException import fr.dcproject.voter.Voter
import fr.ktorVoter.* import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityDeletedAt import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
class CommentVoter : Voter<ApplicationCall> { class CommentVoter : Voter() {
enum class Action : ActionI { fun <S> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse
CREATE, where S : CommentI,
UPDATE, S : EntityDeletedAt = canAll(subjects) { canView(it, citizen) }
VIEW,
DELETE fun <S> canView(subject: S, citizen: CitizenI?): VoterResponse
where S : CommentI,
S : EntityDeletedAt = when {
subject.isDeleted() -> denied("Your cannot view a deleted comment", "comment.view.deleted")
else -> granted()
} }
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI { fun <S, CR : CitizenI> canCreate(subject: S, citizen: CitizenI?): VoterResponse
if (!(action is Action && subject is CommentI?)) return abstain() where S : CommentI,
S : EntityCreatedBy<CR>,
S : CommentWithParentI<*>,
S : AsTarget<*> = 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")
subject.target.let { it is EntityDeletedAt && it.isDeleted() } -> denied("You cannot create a comment on deleted target", "comment.create.deletedTarget")
else -> granted()
}
val citizen = context.citizenOrNull fun <S, CR : CitizenI> canUpdate(subject: S, citizen: CitizenI?): VoterResponse
where S : CommentI,
if (subject == null) { S : EntityCreatedBy<CR> = when {
throw NoSubjectDefinedException(action) citizen == null -> denied("You must be connected to update comment", "comment.update.notConnected")
} citizen.id != subject.createdBy.id -> denied("You cannot update another user of yours", "comment.update.notYours")
else -> granted()
if (action == Action.CREATE) {
return when {
citizen == null -> denied("You must be connected to create user", "comment.create.notConnected")
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
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")
subject.target.let { it is EntityDeletedAt && it.isDeleted() } -> denied("You cannot create a comment on deleted target", "comment.create.deletedTarget")
else -> granted()
}
}
if (action == Action.VIEW) {
return when {
subject !is CommentForView<*, *> -> throw NoSubjectDefinedException(action)
subject.isDeleted() -> denied("Your cannot view a deleted comment", "comment.view.deleted")
else -> granted()
}
}
if (action == Action.UPDATE) {
if (citizen == null) return denied("You must be connected to update comment", "comment.update.notConnected")
return when {
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
citizen.id == subject.createdBy.id -> granted()
else -> denied("You cannot update another user of yours", "comment.update.notYours")
}
}
if (action == Action.DELETE) {
return denied("A comment can never be deleted", "comment.deleted.never")
}
throw NoRuleDefinedException(action)
} }
} }

View File

@@ -1,11 +1,12 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.component.comment.generic.CommentForUpdate import fr.dcproject.component.comment.generic.CommentForUpdate
import fr.dcproject.component.comment.generic.CommentRef import fr.dcproject.component.comment.generic.CommentRef
import fr.dcproject.component.comment.generic.CommentRepository import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCan import fr.dcproject.voter.assert
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.* import io.ktor.features.*
import io.ktor.http.* import io.ktor.http.*
@@ -23,7 +24,7 @@ class CreateCommentChildrenRequest(val comment: CommentRef) {
@KtorExperimentalAPI @KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.createCommentChildren(repo: CommentRepository) { fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) {
post<CreateCommentChildrenRequest> { post<CreateCommentChildrenRequest> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate( val newComment = CommentForUpdate(
@@ -32,7 +33,7 @@ fun Route.createCommentChildren(repo: CommentRepository) {
parent = parent parent = parent
) )
assertCan(CommentVoter.Action.CREATE, newComment) voter.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment) repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment) call.respond(HttpStatusCode.Created, newComment)

View File

@@ -1,10 +1,12 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.comment.generic.CommentRef import fr.dcproject.component.comment.generic.CommentRef
import fr.dcproject.component.comment.generic.CommentRepository import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCan import fr.dcproject.voter.assert
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.locations.* import io.ktor.locations.*
import io.ktor.request.* import io.ktor.request.*
@@ -18,10 +20,10 @@ class EditCommentRequest(val comment: CommentRef)
@KtorExperimentalAPI @KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.editComment(repo: CommentRepository) { fun Route.editComment(repo: CommentRepository, voter: CommentVoter) {
put<EditCommentRequest> { put<EditCommentRequest> {
val comment = repo.findById(it.comment.id)!! val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
assertCan(CommentVoter.Action.UPDATE, comment) voter.assert { canUpdate(comment, citizenOrNull) }
comment.content = call.receiveText() comment.content = call.receiveText()
repo.edit(comment) repo.edit(comment)

View File

@@ -1,8 +1,9 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.comment.generic.CommentRepository import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCanAll 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.*
@@ -25,7 +26,7 @@ class CommentChildrenRequest(
@KtorExperimentalAPI @KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.getChildrenComments(repo: CommentRepository) { fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) {
get<CommentChildrenRequest> { get<CommentChildrenRequest> {
val comments = val comments =
repo.findByParent( repo.findByParent(
@@ -34,7 +35,7 @@ fun Route.getChildrenComments(repo: CommentRepository) {
it.limit it.limit
) )
assertCanAll(CommentVoter.Action.VIEW, comments.result) voter.assert { canView(comments.result, citizenOrNull) }
call.respond(HttpStatusCode.OK, comments) call.respond(HttpStatusCode.OK, comments)
} }

View File

@@ -1,9 +1,10 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.comment.generic.CommentRef import fr.dcproject.component.comment.generic.CommentRef
import fr.dcproject.component.comment.generic.CommentRepository import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCan import fr.dcproject.voter.assert
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.* import io.ktor.features.*
import io.ktor.http.* import io.ktor.http.*
@@ -18,10 +19,10 @@ class CommentRequest(val comment: CommentRef)
@KtorExperimentalAPI @KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.getOneComment(repo: CommentRepository) { fun Route.getOneComment(repo: CommentRepository, voter: CommentVoter) {
get<CommentRequest> { get<CommentRequest> {
val comment = repo.findById(it.comment.id) ?: NotFoundException("Comment ${it.comment.id} not found") val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
assertCan(CommentVoter.Action.VIEW, comment) voter.assert { canView(comment, citizenOrNull) }
call.respond(HttpStatusCode.OK, comment) call.respond(HttpStatusCode.OK, comment)
} }

View File

@@ -1,16 +1,15 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.comment.article.CommentArticleRepository import fr.dcproject.component.comment.article.CommentArticleRepository
import fr.dcproject.component.comment.article.CommentArticleRepository.Sort import fr.dcproject.component.comment.article.CommentArticleRepository.Sort
import fr.dcproject.component.comment.generic.CommentForUpdate import fr.dcproject.component.comment.generic.CommentForUpdate
import fr.dcproject.component.comment.generic.CommentVoter.Action.CREATE import fr.dcproject.component.comment.generic.CommentVoter
import fr.dcproject.component.comment.generic.CommentVoter.Action.VIEW import fr.dcproject.voter.assert
import fr.ktorVoter.assertCan
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.*
@@ -55,18 +54,18 @@ object CommentArticlePaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.commentArticle(repo: CommentArticleRepository) { fun Route.commentArticle(repo: CommentArticleRepository, voter: CommentVoter) {
get<CommentArticlePaths.ArticleCommentRequest> { get<CommentArticlePaths.ArticleCommentRequest> {
val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort) val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort)
if (comment.result.isNotEmpty()) { if (comment.result.isNotEmpty()) {
assertCanAll(VIEW, comment.result) voter.assert { canView(comment.result, citizenOrNull) }
} }
call.respond(HttpStatusCode.OK, comment) call.respond(HttpStatusCode.OK, comment)
} }
post<CommentArticlePaths.PostArticleCommentRequest> { post<CommentArticlePaths.PostArticleCommentRequest> {
it.getComment(call).let { comment -> it.getComment(call).let { comment ->
assertCan(CREATE, comment) voter.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment) repo.comment(comment)
call.respond(HttpStatusCode.Created, comment) call.respond(HttpStatusCode.Created, comment)
} }
@@ -74,7 +73,7 @@ fun Route.commentArticle(repo: CommentArticleRepository) {
get<CommentArticlePaths.CitizenCommentArticleRequest> { get<CommentArticlePaths.CitizenCommentArticleRequest> {
repo.findByCitizen(it.citizen).let { comments -> repo.findByCitizen(it.citizen).let { comments ->
assertCanAll(VIEW, comments.result) voter.assert { canView(comments.result, citizenOrNull) }
call.respond(comments) call.respond(comments)
} }
} }

View File

@@ -1,14 +1,13 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.comment.generic.CommentForUpdate import fr.dcproject.component.comment.generic.CommentForUpdate
import fr.dcproject.component.comment.generic.CommentVoter.Action.CREATE import fr.dcproject.component.comment.generic.CommentVoter
import fr.dcproject.component.comment.generic.CommentVoter.Action.VIEW
import fr.dcproject.entity.ConstitutionRef import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.repository.CommentConstitutionRepository import fr.dcproject.repository.CommentConstitutionRepository
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.*
@@ -26,10 +25,10 @@ object CommentConstitutionPaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.commentConstitution(repo: CommentConstitutionRepository) { fun Route.commentConstitution(repo: CommentConstitutionRepository, voter: CommentVoter) {
get<CommentConstitutionPaths.ConstitutionCommentRequest> { get<CommentConstitutionPaths.ConstitutionCommentRequest> {
val comments = repo.findByTarget(it.constitution) val comments = repo.findByTarget(it.constitution)
assertCanAll(VIEW, comments.result) voter.assert { canView(comments.result, citizenOrNull) }
call.respond(HttpStatusCode.OK, comments) call.respond(HttpStatusCode.OK, comments)
} }
@@ -40,7 +39,7 @@ fun Route.commentConstitution(repo: CommentConstitutionRepository) {
createdBy = citizen, createdBy = citizen,
content = content content = content
) )
assertCan(CREATE, comment) voter.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment) repo.comment(comment)
call.respond(HttpStatusCode.Created, comment) call.respond(HttpStatusCode.Created, comment)
@@ -48,7 +47,7 @@ fun Route.commentConstitution(repo: CommentConstitutionRepository) {
get<CommentConstitutionPaths.CitizenCommentConstitutionRequest> { get<CommentConstitutionPaths.CitizenCommentConstitutionRequest> {
val comments = repo.findByCitizen(it.citizen) val comments = repo.findByCitizen(it.citizen)
assertCanAll(VIEW, comments.result) voter.assert { canView(comments.result, citizenOrNull) }
call.respond(comments) call.respond(comments)
} }
} }

View File

@@ -1,7 +1,6 @@
package fr.dcproject.security.voter package fr.dcproject.security.voter
import fr.dcproject.component.comment.generic.CommentForView import fr.dcproject.component.comment.generic.CommentForView
import fr.dcproject.component.comment.generic.CommentVoter
import fr.dcproject.entity.ConstitutionSimple import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.entity.UserI import fr.dcproject.entity.UserI
import fr.dcproject.user import fr.dcproject.user
@@ -20,7 +19,7 @@ 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 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
@@ -44,7 +43,6 @@ class ConstitutionVoter : Voter<ApplicationCall> {
return granted() return granted()
} }
if (action is CommentVoter.Action) return voteForComment(action)
if (action is VoteVoter.Action) return voteForVote(action, subject) if (action is VoteVoter.Action) return voteForVote(action, subject)
if (action is Action) { if (action is Action) {
@@ -66,16 +64,4 @@ class ConstitutionVoter : Voter<ApplicationCall> {
} }
return abstain() return abstain()
} }
private fun voteForComment(action: CommentVoter.Action): VoterResponseI {
if (action == CommentVoter.Action.CREATE) {
return granted()
}
if (action == CommentVoter.Action.VIEW) {
return granted()
}
return abstain()
}
} }

View File

@@ -13,6 +13,9 @@ enum class Vote {
} }
} }
/**
* Convert vote to boolean
*/
fun toBoolean(): Boolean = when (this) { fun toBoolean(): Boolean = when (this) {
GRANTED -> true GRANTED -> true
DENIED -> false DENIED -> false
@@ -20,51 +23,103 @@ enum class Vote {
} }
abstract class Voter { abstract class Voter {
/**
* A Shortcut for return a GrantedResponse
*/
protected fun granted(message: String? = null, code: String? = null): GrantedResponse = GrantedResponse(this, message, code) protected fun granted(message: String? = null, code: String? = null): GrantedResponse = GrantedResponse(this, message, code)
/**
* A Shortcut for return a DeniedResponse
*/
protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code) protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code)
/**
* Check all responses and return DENIED if one is DENIED
*
* If the list of responses is empty, return GRANTED
*/
private fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: granted() private fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: granted()
/**
* An helper to convert a list of subject into one response
*/
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()
} }
/**
* Throw an Exception if voter return a DENIED response
*/
fun <T : Voter> T.assert(action: T.() -> VoterResponse) { fun <T : Voter> T.assert(action: T.() -> VoterResponse) {
action().assert() action().assert()
} }
/**
* Check all responses and return DENIED if one is DENIED
*
* If the list of responses is empty, return GRANTED
*/
fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: GrantedResponse(first().voter) fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: GrantedResponse(first().voter)
/**
* Throw an Exception if one response is DENIED
*/
fun VoterResponses.assert() = this.getOneResponse().assert() fun VoterResponses.assert() = this.getOneResponse().assert()
class VoterDeniedException(private val voterResponses: VoterResponses) : Throwable(voterResponses.first().message) { class VoterDeniedException(private val voterResponses: VoterResponses) : Throwable(voterResponses.first().message) {
constructor(voterResponse: VoterResponse) : this(listOf(voterResponse)) constructor(voterResponse: VoterResponse) : this(listOf(voterResponse))
/**
* Get first response
*/
fun first(): VoterResponse = voterResponses.first() fun first(): VoterResponse = voterResponses.first()
/**
* Check if the error code is present into the responses
*/
fun hasErrorCode(code: String): Boolean = voterResponses fun hasErrorCode(code: String): Boolean = voterResponses
.filter { it.vote == Vote.DENIED } .filter { it.vote == Vote.DENIED }
.any { it.code == code } .any { it.code == code }
/**
* Find and return the response than match with the error code
*/
fun getErrorCode(code: String): VoterResponse? = voterResponses fun getErrorCode(code: String): VoterResponse? = voterResponses
.firstOrNull { it.vote == Vote.DENIED && it.code == code } .firstOrNull { it.vote == Vote.DENIED && it.code == code }
/**
* Get a list of messages of all responses
*/
fun getMessages(): List<String> = voterResponses fun getMessages(): List<String> = voterResponses
.mapNotNull { it.message } .mapNotNull { it.message }
/**
* Get the first message
*/
fun getFirstMessage(): String? = voterResponses fun getFirstMessage(): String? = voterResponses
.first() .first()
.message .message
} }
/**
* The response that all Voter method return
* @see GrantedResponse
* @see DeniedResponse
*/
sealed class VoterResponse( sealed class VoterResponse(
val vote: Vote, val vote: Vote,
val voter: Voter, val voter: Voter,
val message: String?, val message: String?,
val code: String? val code: String?
) { ) {
/**
* Convert response as boolean
*/
fun toBoolean(): Boolean = vote.toBoolean() fun toBoolean(): Boolean = vote.toBoolean()
/**
* Throw Exception if response if DENIED
*/
fun assert() { fun assert() {
if (this.vote == Vote.DENIED) { if (this.vote == Vote.DENIED) {
throw VoterDeniedException(this) throw VoterDeniedException(this)

View File

@@ -1,6 +1,5 @@
package fr.dcproject.security.voter package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
@@ -11,10 +10,9 @@ import fr.dcproject.component.comment.generic.CommentForView
import fr.dcproject.component.comment.generic.CommentVoter import fr.dcproject.component.comment.generic.CommentVoter
import fr.dcproject.entity.User import fr.dcproject.entity.User
import fr.dcproject.entity.UserI import fr.dcproject.entity.UserI
import fr.dcproject.voter.NoSubjectDefinedException import fr.dcproject.voter.Vote.DENIED
import fr.ktorVoter.* import fr.dcproject.voter.Vote.GRANTED
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import io.ktor.application.*
import io.ktor.locations.* import io.ktor.locations.*
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@@ -24,7 +22,6 @@ 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 java.util.* import java.util.*
import fr.dcproject.component.article.ArticleRepository as ArticleRepo import fr.dcproject.component.article.ArticleRepository as ArticleRepo
@@ -112,109 +109,59 @@ internal class CommentVoterTest {
mockkStatic("fr.dcproject.ApplicationContextKt") mockkStatic("fr.dcproject.ApplicationContextKt")
} }
@Test
fun `support comment`(): Unit = CommentVoter().run {
val p = object : ActionI {}
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
this(CommentVoter.Action.VIEW, it, comment1).vote `should be` Vote.GRANTED
this(CommentVoter.Action.VIEW, it, article1).vote `should be` Vote.ABSTAIN
this(p, it, comment1).vote `should be` Vote.ABSTAIN
}
}
@Test @Test
fun `can be view the comment`() { fun `can be view the comment`() {
listOf(CommentVoter()).run { CommentVoter()
mockk<ApplicationCall> { .canView(comment1, tesla)
every { citizenOrNull } returns tesla .vote `should be` GRANTED
}.let {
can(CommentVoter.Action.VIEW, it, comment1) `should be` true
}
}
} }
@Test @Test
fun `can be view the comment list`(): Unit = listOf(CommentVoter()).run { fun `can be view the comment list`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns einstein .canView(listOf(comment1, comment2), einstein)
}.let { .vote `should be` GRANTED
canAll(CommentVoter.Action.VIEW, it, listOf(comment1)) `should be` true
}
} }
@Test @Test
fun `can be update your comment`(): Unit = listOf(CommentVoter()).run { fun `can be update your comment`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns tesla .canUpdate(comment1, tesla)
}.let { .vote `should be` GRANTED
can(CommentVoter.Action.UPDATE, it, comment1) `should be` true
}
} }
@Test @Test
fun `can not be update other comment`(): Unit = listOf(CommentVoter()).run { fun `can not be update other comment`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns einstein .canUpdate(comment1, einstein)
}.let { .vote `should be` DENIED
can(CommentVoter.Action.UPDATE, it, comment1) `should be` false
}
} }
@Test @Test
fun `can not be delete your comment`(): Unit = listOf(CommentVoter()).run { fun `can be create a comment`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns tesla .canCreate(comment1, tesla)
}.let { .vote `should be` GRANTED
can(CommentVoter.Action.DELETE, it, comment1) `should be` false
}
} }
@Test @Test
fun `can be create a comment`(): Unit = listOf(CommentVoter()).run { fun `can not be create a comment if target is deleted`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns tesla .canCreate(commentTargetDeleted, tesla)
}.let { .vote `should be` DENIED
can(CommentVoter.Action.CREATE, it, comment1) `should be` true
}
} }
@Test @Test
fun `can not be create a comment if target is deleted`(): Unit = listOf(CommentVoter()).run { fun `can not be create a comment with other creator`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns tesla .canCreate(comment1, einstein)
}.let { .vote `should be` DENIED
can(CommentVoter.Action.CREATE, it, commentTargetDeleted) `should be` false
}
} }
@Test @Test
fun `can not be create a comment with other creator`(): Unit = listOf(CommentVoter()).run { fun `can not be create a comment if not connected`() {
mockk<ApplicationCall> { CommentVoter()
every { citizenOrNull } returns einstein .canCreate(comment1, null)
}.let { .vote `should be` DENIED
can(CommentVoter.Action.CREATE, it, comment1) `should be` false
}
}
@Test
fun `can not be create a comment if is null`(): Unit = listOf(CommentVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns einstein
}.let {
assertThrows<NoSubjectDefinedException> {
assertCan(CommentVoter.Action.CREATE, it, null)
}
}
}
@Test
fun `can not be create a comment if not connected`(): Unit = listOf(CommentVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns null
}.let {
can(CommentVoter.Action.CREATE, it, comment1) `should be` false
}
} }
} }