Add Security to Citizen
This commit is contained in:
@@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
42
src/main/kotlin/fr/dcproject/security/voter/CitizenVoter.kt
Normal file
42
src/main/kotlin/fr/dcproject/security/voter/CitizenVoter.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user