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.routes.*
import fr.dcproject.security.voter.ArticleVoter import fr.dcproject.security.voter.ArticleVoter
import fr.dcproject.security.voter.AuthorizationVoter import fr.dcproject.security.voter.AuthorizationVoter
import fr.dcproject.security.voter.CitizenVoter
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
@@ -99,7 +101,8 @@ fun Application.module() {
install(AuthorizationVoter) { install(AuthorizationVoter) {
voters = mutableListOf( voters = mutableListOf(
ArticleVoter() ArticleVoter(),
CitizenVoter()
) )
} }

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package fr.dcproject.routes package fr.dcproject.routes
import Paths import Paths
import fr.dcproject.security.voter.CitizenVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.get import io.ktor.locations.get
@@ -11,11 +13,15 @@ import fr.dcproject.repository.Citizen as CitizenRepository
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.citizen(repo: CitizenRepository) { fun Route.citizen(repo: CitizenRepository) {
get<Paths.CitizensRequest> { get<Paths.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)
call.respond(citizens) call.respond(citizens)
} }
get<Paths.CitizenRequest> { get<Paths.CitizenRequest> {
assertCan(VIEW, it.citizen)
call.respond(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.response.respond
import io.ktor.util.AttributeKey import io.ktor.util.AttributeKey
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.pipeline.PipelineContext
interface ActionI interface ActionI
@@ -39,6 +40,13 @@ fun ApplicationCall.assertCan(action: ActionI, subject: Any? = null) {
throw UnauthorizedException(action) 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 { fun ApplicationCall.can(action: ActionI, subject: Any? = null): Boolean {
val voters = attributes[votersAttributeKey] val voters = attributes[votersAttributeKey]

View File

@@ -5,12 +5,13 @@ declare
multiple int = coalesce(current_setting('fixture.quantity.multiple', true), '50')::int; multiple int = coalesce(current_setting('fixture.quantity.multiple', true), '50')::int;
begin begin
delete from "user"; delete from "user";
insert into "user" (id, username, password, blocked_at) insert into "user" (id, username, password, blocked_at, roles)
select select
uuid_in(md5('user'||rn::text)::cstring), uuid_in(md5('user'||rn::text)::cstring),
'username' || rn, 'username' || rn,
_password, _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; from generate_series(1, multiple) rn;
raise notice 'user fixtures done'; raise notice 'user fixtures done';

View File

@@ -3,12 +3,13 @@ $$
declare declare
new_id uuid; new_id uuid;
begin begin
insert into "user" (id, username, password, blocked_at) insert into "user" (id, username, password, blocked_at, roles)
select select
coalesce(t.id, uuid_generate_v4()), coalesce(t.id, uuid_generate_v4()),
t.username, t.username,
crypt(resource->>'plain_password', gen_salt('bf', 8)), 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 from json_populate_record(null::"user", resource) t
returning id into new_id; 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 ), updated_at timestamptz default now() not null check ( updated_at >= created_at ),
blocked_at timestamptz default null null, blocked_at timestamptz default null null,
username varchar(64) not null check ( username != '' and lower(username) = username) unique, 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 create table citizen