#24 Move voter code to an external library

This commit is contained in:
2020-03-25 02:08:31 +01:00
parent 575752cdc7
commit e572ca0024
35 changed files with 95 additions and 154 deletions

View File

@@ -13,6 +13,8 @@ import fr.dcproject.event.EventSubscriber
import fr.dcproject.event.configEvent
import fr.dcproject.routes.*
import fr.dcproject.security.voter.*
import fr.ktorVoter.AuthorizationVoter
import fr.ktorVoter.ForbiddenException
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationCall

View File

@@ -1,7 +1,8 @@
package fr.dcproject
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.security.voter.ForbiddenException
import fr.ktorVoter.ForbiddenException
import io.ktor.application.ApplicationCall
import io.ktor.auth.authentication
import io.ktor.util.AttributeKey
@@ -26,3 +27,5 @@ val ApplicationCall.citizenOrNull: CitizenEntity?
val PipelineContext<Unit, ApplicationCall>.citizen get() = context.citizen
val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull
val ApplicationCall.user get() = authentication.principal<User>()

View File

@@ -6,7 +6,7 @@ import fr.dcproject.event.ArticleUpdate
import fr.dcproject.repository.Article.Filter
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import fr.dcproject.views.ArticleViewManager
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.application

View File

@@ -9,7 +9,7 @@ import fr.dcproject.routes.CitizenPaths.CitizensRequest
import fr.dcproject.routes.CitizenPaths.CurrentCitizenRequest
import fr.dcproject.security.voter.CitizenVoter.Action.CHANGE_PASSWORD
import fr.dcproject.security.voter.CitizenVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import fr.postgresjson.repository.RepositoryI.Direction
import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential

View File

@@ -5,7 +5,7 @@ import fr.dcproject.entity.Comment
import fr.dcproject.entity.CommentRef
import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content
import fr.dcproject.security.voter.CommentVoter.Action.*
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode

View File

@@ -6,7 +6,7 @@ import fr.dcproject.entity.Citizen
import fr.dcproject.repository.CommentArticle.Sort
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI

View File

@@ -5,7 +5,7 @@ import fr.dcproject.entity.Citizen
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI

View File

@@ -4,7 +4,7 @@ import fr.dcproject.citizen
import fr.dcproject.entity.request.Constitution
import fr.dcproject.security.voter.ConstitutionVoter.Action.CREATE
import fr.dcproject.security.voter.ConstitutionVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI

View File

@@ -4,7 +4,7 @@ import fr.dcproject.citizen
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.Citizen
import fr.dcproject.security.voter.FollowVoter.Action.*
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.*

View File

@@ -4,7 +4,7 @@ import fr.dcproject.citizen
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.security.voter.FollowVoter.Action.*
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.*

View File

@@ -7,7 +7,7 @@ import fr.dcproject.entity.request.RequestBuilder
import fr.dcproject.entity.request.getContent
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import fr.dcproject.utils.toUUID
import io.ktor.application.ApplicationCall
import io.ktor.application.call

View File

@@ -2,7 +2,7 @@ package fr.dcproject.routes
import fr.dcproject.entity.OpinionChoice
import fr.dcproject.security.voter.OpinionChoiceVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location

View File

@@ -8,7 +8,7 @@ import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
import fr.dcproject.security.voter.VoteVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import fr.dcproject.utils.toUUID
import io.ktor.application.call
import io.ktor.http.HttpStatusCode

View File

@@ -4,7 +4,7 @@ import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI

View File

@@ -12,7 +12,7 @@ import fr.dcproject.security.voter.WorkgroupVoter.Action.UPDATE
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.ADD as ADD_MEMBERS
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.UPDATE as UPDATE_MEMBERS
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.REMOVE as REMOVE_MEMBERS
import fr.dcproject.security.voter.assertCan
import fr.ktorVoter.assertCan
import fr.dcproject.utils.toUUID
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.ApplicationCall

View File

@@ -3,6 +3,11 @@ package fr.dcproject.security.voter
import fr.dcproject.entity.ArticleAuthI
import fr.dcproject.entity.ArticleI
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import fr.ktorVoter.checkClass
import io.ktor.application.ApplicationCall
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.entity.Vote as VoteEntity

View File

@@ -1,20 +0,0 @@
package fr.dcproject.security.voter
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
class WrongClassException(
expected: KClass<*>,
current: KClass<*>?
) : VoterException("Can not define authorization with class $current. Need $expected")
fun Voter.checkClass(
expected: KClass<*>,
subject: Any?
) {
if (subject != null && !subject::class.isSubclassOf(expected)) {
throw WrongClassException(expected, subject::class)
} else if (subject == null) {
throw WrongClassException(expected, null)
}
}

View File

@@ -2,6 +2,10 @@ package fr.dcproject.security.voter
import fr.dcproject.entity.CitizenBasicI
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import io.ktor.locations.KtorExperimentalLocationsAPI

View File

@@ -1,6 +1,10 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Comment
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
class CommentVoter : Voter {

View File

@@ -3,6 +3,10 @@ package fr.dcproject.security.voter
import fr.dcproject.entity.Comment
import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.entity.Vote as VoteEntity

View File

@@ -1,5 +1,9 @@
package fr.dcproject.security.voter
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.entity.User as UserEntity

View File

@@ -1,6 +1,9 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.OpinionChoice
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
class OpinionChoiceVoter : Voter {

View File

@@ -2,6 +2,10 @@ package fr.dcproject.security.voter
import fr.dcproject.entity.ArticleAuthI
import fr.dcproject.entity.Opinion
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
class OpinionVoter : Voter {

View File

@@ -1,5 +1,9 @@
package fr.dcproject.security.voter
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.entity.Vote as VoteEntity

View File

@@ -1,110 +0,0 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.User
import io.ktor.application.ApplicationCall
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature
import io.ktor.auth.authentication
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.util.AttributeKey
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.pipeline.PipelineContext
interface ActionI
interface Voter {
fun supports(action: ActionI, call: ApplicationCall, subject: Any? = null): Boolean
fun vote(action: ActionI, call: ApplicationCall, subject: Any? = null): Vote
}
fun List<Voter>.can(action: ActionI, call: ApplicationCall, subject: Any? = null): Boolean {
val listOfSubject: List<Any?> = if (subject !is List<*>) listOf(subject) else subject
val votes: List<Vote> = listOfSubject.flatMap { subject ->
this
.filter { it.supports(action, call, subject) }
.ifEmpty { throw NoVoterException(action) }
.map { it.vote(action, call, subject) }
}
return votes.all { it in listOf(Vote.GRANTED, Vote.ABSTAIN) } and votes.any { it == Vote.GRANTED }
}
enum class Vote {
GRANTED,
ABSTAIN,
DENIED;
companion object {
fun isGranted(lambda: () -> Boolean): Vote {
return if (lambda()) GRANTED else DENIED
}
}
}
private val votersAttributeKey = AttributeKey<List<Voter>>("voters")
fun ApplicationCall.assertCan(action: ActionI, subject: Any? = null, agreeIfNullOrEmpty: Boolean = true) {
val isNullOrEmpty = (subject == null || (subject is Collection<*> && subject.isNullOrEmpty()))
if (!can(action, subject) && !agreeIfNullOrEmpty && isNullOrEmpty) {
throw UnauthorizedException(action)
}
}
fun PipelineContext<Unit, ApplicationCall>.assertCan(action: ActionI, subject: Any? = null, agreeIfNullOrEmpty: Boolean = true) =
context.assertCan(action, subject, agreeIfNullOrEmpty)
fun PipelineContext<Unit, ApplicationCall>.can(action: ActionI, subject: Any? = null) =
context.can(action, subject)
fun ApplicationCall.can(action: ActionI, subject: Any? = null): Boolean {
val voters = attributes[votersAttributeKey]
return voters.can(action, this, subject)
}
abstract class VoterException(message: String) : Throwable(message)
class NoVoterException(action: ActionI) : VoterException("No voter found for action '$action'")
class UnauthorizedException(action: ActionI) : VoterException("Unauthorized for action '$action'")
class ForbiddenException(message: String? = null) : Throwable(message)
val ApplicationCall.user get() = authentication.principal<User>()
class AuthorizationVoter {
/**
* Configuration for [AuthorizationVoter] feature.
*/
class Configuration {
var voters = mutableListOf<Voter>()
fun voter(voter: Voter) = voters.add(voter)
}
/**
* Object for installing feature
*/
companion object Feature : ApplicationFeature<ApplicationCallPipeline, Configuration, AuthorizationVoter> {
override val key = AttributeKey<AuthorizationVoter>("Voter")
@KtorExperimentalAPI
override fun install(
pipeline: ApplicationCallPipeline,
configure: Configuration.() -> Unit
): AuthorizationVoter {
val configuration = Configuration().apply(configure)
pipeline.intercept(ApplicationCallPipeline.Features) {
context.attributes.put(votersAttributeKey, configuration.voters)
try {
proceed()
} catch (e: VoterException) {
context.respond(HttpStatusCode.Forbidden)
}
}
return AuthorizationVoter()
}
}
}

View File

@@ -2,6 +2,11 @@ package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.*
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import fr.ktorVoter.VoterException
import io.ktor.application.ApplicationCall
class WorkgroupVoter : Voter {