Big refactoring #77
@@ -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())
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user