From 4f5cd827c465819c5791bdfc3e1195f1af26a0a2 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Fri, 23 Aug 2019 16:45:33 +0200 Subject: [PATCH] Add Security to Citizen --- src/main/kotlin/fr/dcproject/Application.kt | 5 ++- src/main/kotlin/fr/dcproject/entity/User.kt | 6 ++- .../kotlin/fr/dcproject/routes/Article.kt | 10 ++++- .../kotlin/fr/dcproject/routes/Citizen.kt | 6 +++ .../dcproject/security/voter/CitizenVoter.kt | 42 +++++++++++++++++++ .../fr/dcproject/security/voter/Voter.kt | 8 ++++ src/main/resources/sql/fixtures/01-user.sql | 5 ++- .../sql/functions/user/insert_user.sql | 5 ++- .../sql/migrations/0000-init_schema.up.sql | 3 +- 9 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/fr/dcproject/security/voter/CitizenVoter.kt diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 4ea3400..6a4017f 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -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() ) } diff --git a/src/main/kotlin/fr/dcproject/entity/User.kt b/src/main/kotlin/fr/dcproject/entity/User.kt index f0f9a5b..7e2b91d 100644 --- a/src/main/kotlin/fr/dcproject/entity/User.kt +++ b/src/main/kotlin/fr/dcproject/entity/User.kt @@ -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 = emptyList() ) : UuidEntity(id), EntityCreatedAt by EntityCreatedAtImp(), EntityUpdatedAt by EntityUpdatedAtImp(), Principal +{ + enum class Roles { ROLE_USER, ROLE_ADMIN } +} diff --git a/src/main/kotlin/fr/dcproject/routes/Article.kt b/src/main/kotlin/fr/dcproject/routes/Article.kt index fe6d233..a535cac 100644 --- a/src/main/kotlin/fr/dcproject/routes/Article.kt +++ b/src/main/kotlin/fr/dcproject/routes/Article.kt @@ -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 { + assertCan(VIEW) + val articles = repo.find(it.page, it.limit, it.sort, it.direction, it.search) call.respond(articles) } get { + assertCan(VIEW, it.article) + call.respond(it.article) } post { - call.assertCan(ArticleVoter.Action.CREATE) + assertCan(CREATE) + val article = call.receive() repo.upsert(article) call.respond(article) diff --git a/src/main/kotlin/fr/dcproject/routes/Citizen.kt b/src/main/kotlin/fr/dcproject/routes/Citizen.kt index e2e5a2d..82d2c73 100644 --- a/src/main/kotlin/fr/dcproject/routes/Citizen.kt +++ b/src/main/kotlin/fr/dcproject/routes/Citizen.kt @@ -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 { + assertCan(VIEW) + val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) call.respond(citizens) } get { + assertCan(VIEW, it.citizen) + call.respond(it.citizen) } } \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/security/voter/CitizenVoter.kt b/src/main/kotlin/fr/dcproject/security/voter/CitizenVoter.kt new file mode 100644 index 0000000..5470784 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/security/voter/CitizenVoter.kt @@ -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 + } +} diff --git a/src/main/kotlin/fr/dcproject/security/voter/Voter.kt b/src/main/kotlin/fr/dcproject/security/voter/Voter.kt index 29f833a..2545bbc 100644 --- a/src/main/kotlin/fr/dcproject/security/voter/Voter.kt +++ b/src/main/kotlin/fr/dcproject/security/voter/Voter.kt @@ -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.assertCan(action: ActionI, subject: Any? = null) = + context.assertCan(action, subject) + +fun PipelineContext.can(action: ActionI, subject: Any? = null) = + context.can(action, subject) + fun ApplicationCall.can(action: ActionI, subject: Any? = null): Boolean { val voters = attributes[votersAttributeKey] diff --git a/src/main/resources/sql/fixtures/01-user.sql b/src/main/resources/sql/fixtures/01-user.sql index a541831..dfe1c25 100644 --- a/src/main/resources/sql/fixtures/01-user.sql +++ b/src/main/resources/sql/fixtures/01-user.sql @@ -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'; diff --git a/src/main/resources/sql/functions/user/insert_user.sql b/src/main/resources/sql/functions/user/insert_user.sql index d8d20e4..6135f66 100644 --- a/src/main/resources/sql/functions/user/insert_user.sql +++ b/src/main/resources/sql/functions/user/insert_user.sql @@ -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; diff --git a/src/main/resources/sql/migrations/0000-init_schema.up.sql b/src/main/resources/sql/migrations/0000-init_schema.up.sql index 8969113..620dee0 100644 --- a/src/main/resources/sql/migrations/0000-init_schema.up.sql +++ b/src/main/resources/sql/migrations/0000-init_schema.up.sql @@ -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