improve security.

This commit is contained in:
2019-08-30 22:32:30 +02:00
parent f5bff403f0
commit 9e88b33595
14 changed files with 109 additions and 39 deletions

View File

@@ -1,12 +1,9 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.EntityCreatedAt import fr.postgresjson.entity.*
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.UuidEntity
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
class Citizen( class Citizen(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
var name: Name?, var name: Name?,
@@ -16,7 +13,8 @@ class Citizen(
var followanonymous: Boolean? = null, var followanonymous: Boolean? = null,
var user: User? var user: User?
) : UuidEntity(id), ) : UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp() { EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp() {
data class Name( data class Name(
var firstName: String?, var firstName: String?,
var lastName: String?, var lastName: String?,

View File

@@ -1,8 +1,6 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.EntityUpdatedAt import fr.postgresjson.entity.*
import fr.postgresjson.entity.EntityUpdatedAtImp
import fr.postgresjson.entity.UuidEntity
import java.util.* import java.util.*
open class Comment <T: UuidEntity> ( open class Comment <T: UuidEntity> (
@@ -14,4 +12,5 @@ open class Comment <T: UuidEntity> (
var parent: Comment<T>? = null, var parent: Comment<T>? = null,
var parentsIds: List<UUID>? = null var parentsIds: List<UUID>? = null
): Extra<T>(id, createdBy), ): Extra<T>(id, createdBy),
EntityUpdatedAt by EntityUpdatedAtImp() EntityUpdatedAt by EntityUpdatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp()

View File

@@ -16,7 +16,6 @@ import io.ktor.routing.Route
import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.repository.Article as ArticleRepository import fr.dcproject.repository.Article as ArticleRepository
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object ArticlesPaths { object ArticlesPaths {
@Location("/articles") class ArticlesRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: RepositoryI.Direction? = null, val search: String? = null) { @Location("/articles") class ArticlesRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: RepositoryI.Direction? = null, val search: String? = null) {
@@ -30,9 +29,8 @@ object ArticlesPaths {
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.article(repo: ArticleRepository) { fun Route.article(repo: ArticleRepository) {
get<ArticlesPaths.ArticlesRequest> { get<ArticlesPaths.ArticlesRequest> {
assertCan(VIEW)
val articles = repo.find(it.page, it.limit, it.sort, it.direction, it.search) val articles = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
assertCan(VIEW, articles.result)
call.respond(articles) call.respond(articles)
} }
@@ -43,11 +41,11 @@ fun Route.article(repo: ArticleRepository) {
} }
post<ArticlesPaths.PostArticleRequest> { post<ArticlesPaths.PostArticleRequest> {
assertCan(CREATE)
val article = call.receive<ArticleEntity>() val article = call.receive<ArticleEntity>()
article.createdBy = citizen article.createdBy = citizen
assertCan(CREATE, article)
repo.upsert(article) repo.upsert(article)
call.respond(article) call.respond(article)

View File

@@ -26,9 +26,8 @@ object CitizenPaths {
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.citizen(repo: CitizenRepository) { fun Route.citizen(repo: CitizenRepository) {
get<CitizenPaths.CitizensRequest> { get<CitizenPaths.CitizensRequest> {
assertCan(VIEW)
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
assertCan(VIEW, citizens.result)
call.respond(citizens) call.respond(citizens)
} }

View File

@@ -27,10 +27,8 @@ object CommentArticlePaths {
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.commentArticle(repo: CommentArticleRepository) { fun Route.commentArticle(repo: CommentArticleRepository) {
get<CommentArticlePaths.ArticleCommentRequest> { get<CommentArticlePaths.ArticleCommentRequest> {
assertCan(VIEW, it.article)
val comment = repo.findByTarget(it.article) val comment = repo.findByTarget(it.article)
assertCan(VIEW, comment.result)
call.respond(HttpStatusCode.OK, comment) call.respond(HttpStatusCode.OK, comment)
} }
@@ -50,6 +48,7 @@ fun Route.commentArticle(repo: CommentArticleRepository) {
get<CommentArticlePaths.CitizenCommentArticleRequest> { get<CommentArticlePaths.CitizenCommentArticleRequest> {
val comments = repo.findByCitizen(it.citizen) val comments = repo.findByCitizen(it.citizen)
assertCan(VIEW, comments.result)
call.respond(comments) call.respond(comments)
} }
} }

View File

@@ -27,22 +27,19 @@ object CommentConstitutionPaths {
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.commentConstitution(repo: CommentConstitutionRepository) { fun Route.commentConstitution(repo: CommentConstitutionRepository) {
get<CommentConstitutionPaths.ConstitutionCommentRequest> { get<CommentConstitutionPaths.ConstitutionCommentRequest> {
assertCan(VIEW, it.constitution) val comments = repo.findByTarget(it.constitution)
assertCan(VIEW, comments.result)
val comment = repo.findByTarget(it.constitution) call.respond(HttpStatusCode.OK, comments)
call.respond(HttpStatusCode.OK, comment)
} }
post<CommentConstitutionPaths.ConstitutionCommentRequest> { post<CommentConstitutionPaths.ConstitutionCommentRequest> {
assertCan(CREATE, it.constitution)
val content = call.receiveText() val content = call.receiveText()
val comment = CommentEntity( val comment = CommentEntity(
target = it.constitution, target = it.constitution,
createdBy = citizen, createdBy = citizen,
content = content content = content
) )
assertCan(CREATE, comment)
repo.comment(comment) repo.comment(comment)
call.respond(HttpStatusCode.Created, comment) call.respond(HttpStatusCode.Created, comment)
@@ -50,6 +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)
assertCan(VIEW, comments.result)
call.respond(comments) call.respond(comments)
} }
} }

View File

@@ -1,6 +1,9 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.security.voter.ConstitutionVoter.Action.CREATE
import fr.dcproject.security.voter.ConstitutionVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -28,16 +31,19 @@ object ConstitutionPaths {
fun Route.constitution(repo: ConstitutionRepository) { fun Route.constitution(repo: ConstitutionRepository) {
get<ConstitutionPaths.ConstitutionsRequest> { get<ConstitutionPaths.ConstitutionsRequest> {
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search) val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
assertCan(VIEW, constitutions.result)
call.respond(constitutions) call.respond(constitutions)
} }
get<ConstitutionPaths.ConstitutionRequest> { get<ConstitutionPaths.ConstitutionRequest> {
assertCan(VIEW, it.constitution)
call.respond(it.constitution) call.respond(it.constitution)
} }
post<ConstitutionPaths.PostConstitutionRequest> { post<ConstitutionPaths.PostConstitutionRequest> {
val constitution = call.receive<ConstitutionEntity>() val constitution = call.receive<ConstitutionEntity>()
constitution.createdBy = citizen constitution.createdBy = citizen
assertCan(CREATE, constitution)
repo.upsert(constitution) repo.upsert(constitution)

View File

@@ -20,17 +20,25 @@ object FollowArticlePaths {
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.followArticle(repo: FollowArticleRepository) { fun Route.followArticle(repo: FollowArticleRepository) {
post<FollowArticlePaths.ArticleFollowRequest> { post<FollowArticlePaths.ArticleFollowRequest> {
repo.follow(FollowEntity(target = it.article, createdBy = this.citizen)) val follow = FollowEntity(target = it.article, createdBy = this.citizen)
// TODO create voter
// assertCan(FollowVoter.Action.CREATE, follow)
repo.follow(follow)
call.respond(HttpStatusCode.Created) call.respond(HttpStatusCode.Created)
} }
delete<FollowArticlePaths.ArticleFollowRequest> { delete<FollowArticlePaths.ArticleFollowRequest> {
repo.unfollow(FollowEntity(target = it.article, createdBy = this.citizen)) val follow = FollowEntity(target = it.article, createdBy = this.citizen)
// TODO create voter
// assertCan(FollowVoter.Action.DELETE, follow)
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
get<FollowArticlePaths.CitizenFollowArticleRequest> { get<FollowArticlePaths.CitizenFollowArticleRequest> {
val follows = repo.findByCitizen(it.citizen) val follows = repo.findByCitizen(it.citizen)
// TODO add security
// assertCan(FollowVoter.Action.VIEW, follows)
call.respond(follows) call.respond(follows)
} }
} }

View File

@@ -20,17 +20,25 @@ object FollowConstitutionPaths {
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.followConstitution(repo: FollowConstitutionRepository) { fun Route.followConstitution(repo: FollowConstitutionRepository) {
post<FollowConstitutionPaths.ConstitutionFollowRequest> { post<FollowConstitutionPaths.ConstitutionFollowRequest> {
repo.follow(FollowEntity(target = it.constitution, createdBy = this.citizen)) val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
// TODO create voter
// assertCan(FollowVoter.Action.CREATE, follow)
repo.follow(follow)
call.respond(HttpStatusCode.Created) call.respond(HttpStatusCode.Created)
} }
delete<FollowConstitutionPaths.ConstitutionFollowRequest> { delete<FollowConstitutionPaths.ConstitutionFollowRequest> {
repo.unfollow(FollowEntity(target = it.constitution, createdBy = this.citizen)) val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
// TODO create voter
// assertCan(FollowVoter.Action.DELETE, follow)
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
get<FollowConstitutionPaths.CitizenFollowConstitutionRequest> { get<FollowConstitutionPaths.CitizenFollowConstitutionRequest> {
val follows = repo.findByCitizen(it.citizen) val follows = repo.findByCitizen(it.citizen)
// TODO create voter
// assertCan(FollowVoter.Action.VIEW, follows)
call.respond(follows) call.respond(follows)
} }
} }

View File

@@ -3,6 +3,7 @@ package fr.dcproject.security.voter
import fr.dcproject.entity.User import fr.dcproject.entity.User
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.entity.Vote as VoteEntity import fr.dcproject.entity.Vote as VoteEntity
class ArticleVoter: Voter { class ArticleVoter: Voter {
@@ -15,7 +16,8 @@ class ArticleVoter: Voter {
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action) return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
&& subject is ArticleEntity? &&
(subject is List<*> || subject is ArticleEntity? || subject is VoteEntity<*> || subject is CommentEntity<*>)
} }
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
@@ -25,7 +27,19 @@ class ArticleVoter: Voter {
} }
if (action == Action.VIEW) { if (action == Action.VIEW) {
return Vote.GRANTED if (subject is ArticleEntity) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
}
if (subject is List<*>) {
subject.forEach {
if (it !is ArticleEntity || it.isDeleted()) {
return Vote.DENIED
}
}
return Vote.GRANTED
}
return Vote.DENIED
} }
if (action is CommentVoter.Action) return voteForComment(action) if (action is CommentVoter.Action) return voteForComment(action)

View File

@@ -13,7 +13,9 @@ class CitizenVoter: Voter {
} }
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return action is Action && subject is Citizen? return (action is Action)
&&
(subject is List<*> || subject is Citizen?)
} }
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
@@ -22,8 +24,20 @@ class CitizenVoter: Voter {
return Vote.GRANTED return Vote.GRANTED
} }
if (action == Action.VIEW && user != null) { if (action == Action.VIEW) {
return Vote.GRANTED if (subject is Citizen) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
}
if (subject is List<*>) {
subject.forEach {
if (it !is Citizen || it.isDeleted()) {
return Vote.DENIED
}
}
return Vote.GRANTED
}
return Vote.DENIED
} }
if (action == Action.DELETE) { if (action == Action.DELETE) {

View File

@@ -12,7 +12,8 @@ class CommentVoter: Voter {
} }
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return action is Action && subject is Comment<*>? return (action is Action) &&
(subject is Comment<*>? || subject is List<*>)
} }
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
@@ -22,7 +23,19 @@ class CommentVoter: Voter {
} }
if (action == Action.VIEW) { if (action == Action.VIEW) {
return Vote.GRANTED if (subject is Comment<*>) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
}
if (subject is List<*>) {
subject.forEach {
if (it !is Comment<*> || it.isDeleted()) {
return Vote.DENIED
}
}
return Vote.GRANTED
}
return Vote.DENIED
} }
if (action == Action.UPDATE && user != null && subject is Comment<*> && user.id == subject.createdBy?.userId) { if (action == Action.UPDATE && user != null && subject is Comment<*> && user.id == subject.createdBy?.userId) {

View File

@@ -1,5 +1,6 @@
package fr.dcproject.security.voter package fr.dcproject.security.voter
import fr.dcproject.entity.Comment
import fr.dcproject.entity.User import fr.dcproject.entity.User
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import fr.dcproject.entity.Constitution as ConstitutionEntity import fr.dcproject.entity.Constitution as ConstitutionEntity
@@ -14,7 +15,9 @@ class ConstitutionVoter: Voter {
} }
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return (action is Action || action is CommentVoter.Action) && subject is ConstitutionEntity? return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
&&
(subject is List<*> || subject is ConstitutionEntity? || subject is VoteEntity<*> || subject is Comment<*>)
} }
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
@@ -24,7 +27,19 @@ class ConstitutionVoter: Voter {
} }
if (action == Action.VIEW) { if (action == Action.VIEW) {
return Vote.GRANTED if (subject is ConstitutionEntity) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
}
if (subject is List<*>) {
subject.forEach {
if (it !is ConstitutionEntity || it.isDeleted()) {
return Vote.DENIED
}
}
return Vote.GRANTED
}
return Vote.DENIED
} }
if (action == Action.DELETE && user is User && subject is ConstitutionEntity && subject.createdBy?.userId == user.id) { if (action == Action.DELETE && user is User && subject is ConstitutionEntity && subject.createdBy?.userId == user.id) {

View File

@@ -236,6 +236,7 @@ create table comment
"content" text not null check ( content != '' and length(content) < 4096), "content" text not null check ( content != '' and length(content) < 4096),
parent_id uuid references comment (id), parent_id uuid references comment (id),
parents_ids uuid[], parents_ids uuid[],
deleted_at timestamptz null,
foreign key (created_by_id) references citizen (id), foreign key (created_by_id) references citizen (id),
primary key (id) primary key (id)
) inherits (extra); ) inherits (extra);