Add Security to Citizen

This commit is contained in:
2019-08-23 16:45:33 +02:00
parent 9b6f3aab88
commit 4f5cd827c4
9 changed files with 81 additions and 9 deletions

View File

@@ -13,10 +13,12 @@ import fr.dcproject.entity.User
import fr.dcproject.routes.*
import fr.dcproject.security.voter.ArticleVoter
import fr.dcproject.security.voter.AuthorizationVoter
import fr.dcproject.security.voter.CitizenVoter
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.authenticate
import io.ktor.auth.jwt.jwt
import io.ktor.features.AutoHeadResponse
import io.ktor.features.CallLogging
@@ -99,7 +101,8 @@ fun Application.module() {
install(AuthorizationVoter) {
voters = mutableListOf(
ArticleVoter()
ArticleVoter(),
CitizenVoter()
)
}

View File

@@ -9,8 +9,12 @@ class User(
id: UUID? = UUID.randomUUID(),
var username: String?,
var blockedAt: DateTime? = null,
var plainPassword: String?
var plainPassword: String?,
var roles: List<Roles> = emptyList()
) : UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp(),
Principal
{
enum class Roles { ROLE_USER, ROLE_ADMIN }
}

View File

@@ -1,7 +1,8 @@
package fr.dcproject.routes
import Paths
import fr.dcproject.security.voter.ArticleVoter
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -16,16 +17,21 @@ import fr.dcproject.repository.Article as ArticleRepository
@KtorExperimentalLocationsAPI
fun Route.article(repo: ArticleRepository) {
get<Paths.ArticlesRequest> {
assertCan(VIEW)
val articles = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
call.respond(articles)
}
get<Paths.ArticleRequest> {
assertCan(VIEW, it.article)
call.respond(it.article)
}
post<Paths.PostArticleRequest> {
call.assertCan(ArticleVoter.Action.CREATE)
assertCan(CREATE)
val article = call.receive<ArticleEntity>()
repo.upsert(article)
call.respond(article)

View File

@@ -1,6 +1,8 @@
package fr.dcproject.routes
import Paths
import fr.dcproject.security.voter.CitizenVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.get
@@ -11,11 +13,15 @@ import fr.dcproject.repository.Citizen as CitizenRepository
@KtorExperimentalLocationsAPI
fun Route.citizen(repo: CitizenRepository) {
get<Paths.CitizensRequest> {
assertCan(VIEW)
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
call.respond(citizens)
}
get<Paths.CitizenRequest> {
assertCan(VIEW, it.citizen)
call.respond(it.citizen)
}
}

View File

@@ -0,0 +1,42 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.User
import io.ktor.application.ApplicationCall
class CitizenVoter: 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 Citizen?
}
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) {
return Vote.DENIED
}
if (action == Action.UPDATE &&
user is User &&
subject is Citizen &&
subject.user?.id == user.id) {
return Vote.GRANTED
}
return Vote.ABSTAIN
}
}

View File

@@ -9,6 +9,7 @@ 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
@@ -39,6 +40,13 @@ fun ApplicationCall.assertCan(action: ActionI, subject: Any? = null) {
throw UnauthorizedException(action)
}
}
fun PipelineContext<Unit, ApplicationCall>.assertCan(action: ActionI, subject: Any? = null) =
context.assertCan(action, subject)
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]

View File

@@ -5,12 +5,13 @@ declare
multiple int = coalesce(current_setting('fixture.quantity.multiple', true), '50')::int;
begin
delete from "user";
insert into "user" (id, username, password, blocked_at)
insert into "user" (id, username, password, blocked_at, roles)
select
uuid_in(md5('user'||rn::text)::cstring),
'username' || rn,
_password,
case when rn % 10 = 0 then now() else null end
case when rn % 10 = 0 then now() else null end,
case when rn % 2 = 0 then '{ROLE_USER}'::text[] else '{ROLE_ADMIN}'::text[] end
from generate_series(1, multiple) rn;
raise notice 'user fixtures done';

View File

@@ -3,12 +3,13 @@ $$
declare
new_id uuid;
begin
insert into "user" (id, username, password, blocked_at)
insert into "user" (id, username, password, blocked_at, roles)
select
coalesce(t.id, uuid_generate_v4()),
t.username,
crypt(resource->>'plain_password', gen_salt('bf', 8)),
case when t.blocked_at is not null then now() else null end
case when t.blocked_at is not null then now() else null end,
t.roles
from json_populate_record(null::"user", resource) t
returning id into new_id;

View File

@@ -6,7 +6,8 @@ create table "user"
updated_at timestamptz default now() not null check ( updated_at >= created_at ),
blocked_at timestamptz default null null,
username varchar(64) not null check ( username != '' and lower(username) = username) unique,
password text not null check ( password != '' )
password text not null check ( password != '' ),
roles text[] default '{}' not null
);
create table citizen