feature #9: Create Voter for Article

This commit is contained in:
2019-08-23 09:41:41 +02:00
parent 21b6a525fd
commit 0108d496e0
5 changed files with 154 additions and 27 deletions

View File

@@ -11,10 +11,13 @@ import fr.dcproject.entity.Citizen
import fr.dcproject.entity.Constitution import fr.dcproject.entity.Constitution
import fr.dcproject.entity.User import fr.dcproject.entity.User
import fr.dcproject.routes.* import fr.dcproject.routes.*
import fr.dcproject.security.voter.ArticleVoter
import fr.dcproject.security.voter.AuthorizationVoter
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
import io.ktor.auth.authenticate
import io.ktor.auth.jwt.jwt import io.ktor.auth.jwt.jwt
import io.ktor.features.AutoHeadResponse import io.ktor.features.AutoHeadResponse
import io.ktor.features.CallLogging import io.ktor.features.CallLogging
@@ -95,6 +98,12 @@ fun Application.module() {
install(Locations) { install(Locations) {
} }
install(AuthorizationVoter) {
voters = mutableListOf(
ArticleVoter()
)
}
install(Authentication) { install(Authentication) {
/** /**
* Setup the JWT authentication to be used in [Routing]. * Setup the JWT authentication to be used in [Routing].
@@ -131,12 +140,14 @@ fun Application.module() {
} }
install(Routing) { install(Routing) {
article(get()) authenticate(optional = true) {
auth(get()) article(get())
citizen(get()) auth(get())
constitution(get()) citizen(get())
followArticle(get()) constitution(get())
followConstitution(get()) followArticle(get())
followConstitution(get())
}
} }
// TODO move to postgresJson lib // TODO move to postgresJson lib

View File

@@ -11,7 +11,7 @@ class Citizen(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
var name: Name?, var name: Name?,
var birthday: DateTime?, var birthday: DateTime?,
var userId: String? = null, var userId: UUID? = null,
var voteAnnonymous: Boolean? = null, var voteAnnonymous: Boolean? = null,
var followAnnonymous: Boolean? = null, var followAnnonymous: Boolean? = null,
var user: User? var user: User?

View File

@@ -1,11 +1,9 @@
package fr.dcproject.routes package fr.dcproject.routes
import Paths import Paths
import io.ktor.application.ApplicationCall import fr.dcproject.security.voter.ArticleVoter
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.auth.authentication
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.locations.post import io.ktor.locations.post
@@ -13,11 +11,8 @@ import io.ktor.request.receive
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.User as UserEntity
import fr.dcproject.repository.Article as ArticleRepository import fr.dcproject.repository.Article as ArticleRepository
val ApplicationCall.user get() = authentication.principal<UserEntity>()
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.article(repo: ArticleRepository) { fun Route.article(repo: ArticleRepository) {
get<Paths.ArticlesRequest> { get<Paths.ArticlesRequest> {
@@ -29,17 +24,10 @@ fun Route.article(repo: ArticleRepository) {
call.respond(it.article) call.respond(it.article)
} }
authenticate(optional = true) { post<Paths.PostArticleRequest> {
post<Paths.PostArticleRequest>() { call.assertCan(ArticleVoter.Action.CREATE)
// TODO replace to voter val article = call.receive<ArticleEntity>()
val user = call.user repo.upsert(article)
if (user == null) { call.respond(article)
call.respond(HttpStatusCode.Unauthorized)
} else {
val article = call.receive<ArticleEntity>()
repo.upsert(article)
call.respond(article)
}
}
} }
} }

View File

@@ -0,0 +1,39 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Article
import fr.dcproject.entity.User
import io.ktor.application.ApplicationCall
class ArticleVoter: Voter {
enum class Action: ActionI {
CREATE,
UPDATE,
VIEW,
DELETE
}
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return action is Action && subject is Article?
}
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
val user = call.user
if (action == Action.CREATE && user != null) {
return Vote.GRANTED
}
if (action == Action.VIEW) {
return Vote.GRANTED
}
if (action == Action.DELETE && user is User && subject is Article && subject.createdBy?.userId == user.id) {
return Vote.GRANTED
}
if (action == Action.UPDATE && user is User && subject is Article && subject.createdBy?.userId == user.id) {
return Vote.GRANTED
}
return Vote.ABSTAIN
}
}

View File

@@ -0,0 +1,89 @@
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
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 votes = 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
}
private val votersAttributeKey = AttributeKey<List<Voter>>("voters")
fun ApplicationCall.assertCan(action: ActionI, subject: Any? = null) {
if (!can(action, subject)) {
throw UnauthorizedException(action)
}
}
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 : Throwable()
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()
}
}
}