diff --git a/gramex/gramex.yaml b/gramex/gramex.yaml index d06ec760..f1fec4ac 100644 --- a/gramex/gramex.yaml +++ b/gramex/gramex.yaml @@ -378,7 +378,33 @@ storelocations: start: TEXT # ISO8601 encoded (YYYY-MM-DD HH:MM:SS.SSS UTC) end: TEXT # ISO8601 encoded (YYYY-MM-DD HH:MM:SS.SSS UTC) error: TEXT # Error stage + traceback - + permissions: + url: sqlite:///$GRAMEXDATA/roles.db + table: permissions + columns: + app: TEXT + namespace: TEXT + project: TEXT + role: TEXT + permission: TEXT + roles: + url: sqlite:///$GRAMEXDATA/roles.db + table: roles + columns: + app: TEXT + namespace: TEXT + project: TEXT + user: TEXT + role: TEXT + user_permissions: + url: sqlite:///$GRAMEXDATA/roles.db + table: user_permissions + columns: + app: TEXT + namespace: TEXT + project: TEXT + user: TEXT + permission: TEXT # The `schedule:` section defines when specific code is to run. schedule: # Every day and on startup, check if Gramex needs to be updated diff --git a/gramex/handlers/basehandler.py b/gramex/handlers/basehandler.py index a501a47b..9aa51ae8 100644 --- a/gramex/handlers/basehandler.py +++ b/gramex/handlers/basehandler.py @@ -368,6 +368,7 @@ def setup_session(cls, session_conf): if 'cookiepath' in session_conf: cls._session_cookie['path'] = session_conf['cookiepath'] cls._on_init_methods.append(cls.override_user) + cls._on_init_methods.append(cls.add_roles) cls._on_finish_methods.append(cls.set_last_visited) # Ensure that session is saved AFTER we set last visited cls._on_finish_methods.append(cls.save_session) @@ -1155,6 +1156,27 @@ def initialize_handler(self): if self._set_xsrf: self.xsrf_token + def add_roles(self, roles_key='roles', permissions_key='permissions'): + '''Add roles and permissions to the current user''' + user = self.session.get('user') + if not isinstance(user, dict): + return + id = user['id'] + args = {} + _set_arg(args, 'app', gramex.config.variables.get('APPNAME', None)) + _set_arg(args, 'namespace', self.path_kwargs.get('namespace', None)) + _set_arg(args, 'project', self.path_kwargs.get('project', None)) + roles = user[roles_key] = gramex.data.filter( + **gramex.service.storelocations.roles, args={**args, 'user': [id]} + )['role'].tolist() + perms = gramex.data.filter( + **gramex.service.storelocations.permissions, args={**args, 'role': roles} + )['permission'].tolist() + perms += gramex.data.filter( + **gramex.service.storelocations.user_permissions, args={**args, 'user': [id]} + )['permission'].tolist() + user[permissions_key] = list(set(perms)) + class BaseHandler(RequestHandler, BaseMixin): ''' @@ -1524,3 +1546,10 @@ def _check_condition(condition, user): elif node not in values: return False return True + + +def _set_arg(args, key, value): + if value is None: + args[key + '!'] = [] + else: + args[key] = value diff --git a/tests/gramex.yaml b/tests/gramex.yaml index acbf8cc6..7a401480 100644 --- a/tests/gramex.yaml +++ b/tests/gramex.yaml @@ -2222,6 +2222,16 @@ url: sheet_name: userinfo id: userid + auth/roles: + pattern: /auth/roles + handler: SimpleAuth + kwargs: + template: $YAMLPATH/auth.html + credentials: + alpha: alpha + beta: beta + gamma: gamma + auth/rules: pattern: /auth/rulesdb handler: SimpleAuth @@ -2950,3 +2960,9 @@ storelocations: uri: TEXT user: TEXT request.method: TEXT + roles: + url: sqlite:///$YAMLPATH/roles.db + permissions: + url: sqlite:///$YAMLPATH/roles.db + user_permissions: + url: sqlite:///$YAMLPATH/roles.db diff --git a/tests/test_auth.py b/tests/test_auth.py index a7a04eca..3ff6b9e8 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -15,7 +15,7 @@ import gramex.config import gramex.cache from gramex.http import OK, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST -from . import TestGramex, server, tempfiles, dbutils, in_ +from . import TestGramex, server, tempfiles, dbutils, in_, remove_if_possible folder = os.path.dirname(os.path.abspath(__file__)) @@ -579,6 +579,74 @@ def test_lookup(self): eq_(session['user']['gender'], 'female') +class TestRoles(AuthBase): + @classmethod + def setUpClass(cls): + AuthBase.setUpClass() + cls.url = server.base_url + '/auth/roles' + cls.roles_db = tempfiles.roles_db = os.path.join(folder, 'roles.db') + remove_if_possible(cls.roles_db) + + def test_roles(self): + self.login_ok('alpha', 'alpha', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], []) + eq_(session['user']['permissions'], []) + + self.login_ok('beta', 'beta', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], []) + eq_(session['user']['permissions'], []) + + gramex.data.insert( + url=f'sqlite:///{self.roles_db}', + table='roles', + args={ + 'app': [None, None, None, None], + 'namespace': [None, None, None, None], + 'project': [None, None, None, None], + 'user': ['alpha', 'alpha', 'beta', 'gamma'], + 'role': ['junior', 'senior', 'lead', 'junior'], + }, + ) + gramex.data.insert( + url=f'sqlite:///{self.roles_db}', + table='permissions', + args={ + 'app': [None, None, None, None, None, None], + 'namespace': [None, None, None, None, None, None], + 'project': [None, None, None, None, None, None], + 'role': ['junior', 'senior', 'senior', 'lead', 'lead', 'lead'], + 'permission': ['read', 'read', 'write', 'read', 'write', 'delete'] + }, + ) + gramex.data.insert( + url=f'sqlite:///{self.roles_db}', + table='user_permissions', + args={ + 'app': [None, None, None], + 'namespace': [None, None, None], + 'project': [None, None, None], + 'user': ['gamma', 'gamma', 'gamma'], + 'permission': ['read', 'write', 'delete'] + }, + ) + self.login_ok('alpha', 'alpha', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], ['junior', 'senior']) + eq_(sorted(session['user']['permissions']), sorted(['read', 'write'])) + + self.login_ok('beta', 'beta', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], ['lead']) + eq_(sorted(session['user']['permissions']), sorted(['read', 'write', 'delete'])) + + self.login_ok('gamma', 'gamma', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], ['junior']) + eq_(sorted(session['user']['permissions']), sorted(['read', 'write', 'delete'])) + + class TestRules(AuthBase): url = server.base_url + '/auth/rulesdb'