Commit 178ab77c authored by sim's avatar sim

Bootstrap access control system

parent 4ffaeb60
"""Bootstrap access control system
Revision ID: 4db5dcbe4bc7
Revises: 73304ae9f1fb
Create Date: 2017-10-06 17:23:27.765318
"""
from alembic import op
import sqlalchemy as sa
from gargantext.util.alembic import ReplaceableObject
# revision identifiers, used by Alembic.
revision = '4db5dcbe4bc7'
down_revision = '73304ae9f1fb'
branch_labels = None
depends_on = None
# Publicly exposed schema through PostgREST
api_schema = ReplaceableObject("api")
api_nodes_view = ReplaceableObject(
"api.nodes",
"SELECT id, typename AS type, user_id, parent_id, name, date AS created, hyperdata AS data, title_abstract FROM nodes")
# Mere mortals have 'gargantext' role, admin is 'gargantua'
gargantext_role = ReplaceableObject("gargantext", "NOLOGIN")
# PostgREST authentification system; could be used without PostgREST
authenticator_role = ReplaceableObject(
"authenticator",
"LOGIN NOINHERIT PASSWORD 'CHANGEME'")
anon_role = ReplaceableObject("anon", "NOLOGIN")
roles = [gargantext_role, authenticator_role, anon_role]
grants = [
# Enable login through PostgREST auth system for gargantua, anon and
# gargantext
('gargantua, anon, gargantext', 'authenticator'),
# Basic privileges for gargantext role
('USAGE ON SCHEMA api', 'gargantext'),
('SELECT ON nodes', 'gargantext'),
('UPDATE (parent_id, name, date, hyperdata) ON nodes', 'gargantext'),
('INSERT ON nodes', 'gargantext'),
('USAGE, SELECT ON SEQUENCE nodes_id_seq', 'gargantext'),
('DELETE ON nodes', 'gargantext'),
]
current_user_id_sp = ReplaceableObject(
"current_user_id()",
"""
-- Assuming JWT and claim.user_id is set to user.id at login
-- https://stackoverflow.com/questions/2082686/how-do-i-cast-a-string-to-integer-and-have-0-in-case-of-error-in-the-cast-with-p
RETURNS integer AS $$
DECLARE
user_id INTEGER NOT NULL DEFAULT 0;
BEGIN
BEGIN
user_id := current_setting('request.jwt.claim.user_id')::int;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'Invalid user_id: %. Check JWT generation.',
current_setting('request.jwt.claim.user_id', TRUE);
RETURN -1;
END;
RETURN user_id;
END;
$$ LANGUAGE plpgsql""")
stored_procedures = [current_user_id_sp]
is_owner = "COALESCE(current_user_id() = user_id, FALSE)"
is_parent_owner = "COALESCE(current_user_id() = (SELECT user_id FROM nodes n WHERE id = nodes.parent_id), FALSE)"
owner_select_policy = ReplaceableObject("owner_select", "nodes", "FOR SELECT USING (%s)" % is_owner)
owner_update_policy = ReplaceableObject("owner_update", "nodes", "FOR UPDATE USING (%s)" % is_owner)
owner_insert_policy = ReplaceableObject("owner_insert", "nodes", "FOR INSERT WITH CHECK (%s)" % is_parent_owner)
owner_delete_policy = ReplaceableObject("owner_delete", "nodes", "FOR DELETE USING (%s)" % is_parent_owner)
policies = [owner_select_policy, owner_update_policy, owner_insert_policy,
owner_delete_policy]
def upgrade():
op.create_schema(api_schema)
for role in roles:
op.create_role(role)
op.create_view(api_nodes_view)
op.execute("ALTER VIEW api.nodes OWNER TO gargantext")
# BYPASSRLS is only useful if gargantua is not owner of tables
op.execute("ALTER ROLE gargantua WITH BYPASSRLS")
for grant in grants:
op.execute('GRANT {} TO {}'.format(*grant))
op.execute("ALTER TABLE nodes ENABLE ROW LEVEL SECURITY")
for sp in stored_procedures:
op.create_sp(sp)
for policy in policies:
op.create_policy(policy)
def downgrade():
for policy in policies:
op.drop_policy(policy)
for sp in stored_procedures:
op.drop_sp(sp)
op.execute("ALTER TABLE nodes DISABLE ROW LEVEL SECURITY")
for grant in grants:
op.execute('REVOKE {} FROM {}'.format(*grant))
op.execute("ALTER ROLE gargantua WITH NOBYPASSRLS")
op.drop_view(api_nodes_view)
for role in roles:
op.drop_role(role)
op.drop_schema(api_schema)
......@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import os
import datetime
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
......@@ -116,12 +117,18 @@ REST_FRAMEWORK = {
),
}
# http://getblimp.github.io/django-rest-framework-jwt/
JWT_AUTH = {
'JWT_VERIFY_EXPIRATION': False,
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_PAYLOAD_HANDLER': 'gargantext.util.jwt.jwt_payload_handler',
'JWT_VERIFY_EXPIRATION': True,
'JWT_SECRET_KEY': 'Mw/q=efK3ai7}?}?!D68}a2.j}G5;1]ceI;OV1l=N^(-mH+%l=',
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=36000),
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
}
ROLE_ADMIN = 'gargantua'
ROLE_USER = 'gargantext'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
......
from gargantext import settings
from rest_framework_jwt.settings import api_settings
from calendar import timegm
from .dates import datetime
def jwt_payload_handler(user):
username = user.username
payload = {
'user_id': user.pk,
'role': settings.ROLE_ADMIN if username == settings.ROLE_ADMIN else \
settings.ROLE_USER,
'username': username,
'exp': datetime.now() + api_settings.JWT_EXPIRATION_DELTA
}
# Include original issued at time for a brand new token,
# to allow token refresh
if api_settings.JWT_ALLOW_REFRESH:
payload['orig_iat'] = timegm(
datetime.utcnow().utctimetuple()
)
if api_settings.JWT_AUDIENCE is not None:
payload['aud'] = api_settings.JWT_AUDIENCE
if api_settings.JWT_ISSUER is not None:
payload['iss'] = api_settings.JWT_ISSUER
return payload
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment