From e6816e9193736acad2c406db8f10834bcd3a0ce6 Mon Sep 17 00:00:00 2001 From: S Anand Date: Tue, 9 May 2023 11:43:53 +0530 Subject: [PATCH 1/5] ENH: current_user.roles|permissions set up --- gramex/gramex.yaml | 15 +++++++++++++++ gramex/handlers/basehandler.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/gramex/gramex.yaml b/gramex/gramex.yaml index d06ec760..09dc189f 100644 --- a/gramex/gramex.yaml +++ b/gramex/gramex.yaml @@ -378,6 +378,21 @@ 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 + roles: + url: sqlite:///$GRAMEXDATA/roles.db + table: roles + columns: + app: TEXT + namespace: TEXT + project: TEXT + user: TEXT + role: TEXT + permissions: + url: sqlite:///$GRAMEXDATA/roles.db + table: permissions + columns: + role: TEXT + permission: TEXT # The `schedule:` section defines when specific code is to run. schedule: diff --git a/gramex/handlers/basehandler.py b/gramex/handlers/basehandler.py index 8b087ddb..2d2abc58 100644 --- a/gramex/handlers/basehandler.py +++ b/gramex/handlers/basehandler.py @@ -449,6 +449,7 @@ def setup_ratelimit(cls, ratelimit: Union[dict, None], ratelimit_app_conf: Union cls._ratelimit.store = cls._get_store(ratelimit_app_conf) cls._on_init_methods.append(cls.check_ratelimit) cls._on_finish_methods.append(cls.update_ratelimit) + cls._on_finish_methods.append(cls.add_roles) @classmethod def reset_ratelimit(cls, pool: str, keys: List[Any], value: int = 0) -> bool: @@ -1098,6 +1099,22 @@ def initialize_handler(self): if self._set_xsrf: self.xsrf_token + def add_roles(self): + '''Add roles to the current user''' + if isinstance(self.current_user, dict): + args = { + 'app': [gramex.config.variables.get('APPNAME', None)], + 'namespace': [self.path_kwargs.get('namespace', None)], + 'project': [self.path_kwargs.get('project', None)], + 'user': [self.current_user.get('id', None)], + } + self.current_user['roles'] = gramex.data.filter( + **gramex.service.storelocations.roles, args=args + )['role'].tolist() + self.current_user['permissions'] = gramex.data.filter( + **gramex.service.storelocations.roles, args={'role': self.current_user['roles']} + )['permission'].tolist() + class BaseHandler(RequestHandler, BaseMixin): ''' From 9742fa80ebf0da2c76666a58b001131f39b3ad8d Mon Sep 17 00:00:00 2001 From: Shraddheya Shrivastava Date: Fri, 12 May 2023 13:36:00 +0530 Subject: [PATCH 2/5] ADD: more tables for roles and permission mapping --- gramex/gramex.yaml | 28 ++++++++++++++++++++++++---- gramex/handlers/basehandler.py | 22 ++++++++++++++++------ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/gramex/gramex.yaml b/gramex/gramex.yaml index 09dc189f..3e0cb90e 100644 --- a/gramex/gramex.yaml +++ b/gramex/gramex.yaml @@ -378,6 +378,15 @@ 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 + permission: TEXT + description: TEXT roles: url: sqlite:///$GRAMEXDATA/roles.db table: roles @@ -385,15 +394,26 @@ storelocations: app: TEXT namespace: TEXT project: TEXT - user: TEXT role: TEXT - permissions: + permission: TEXT + map_user_role: url: sqlite:///$GRAMEXDATA/roles.db - table: permissions + table: map_user_role columns: + app: TEXT + namespace: TEXT + project: TEXT + user: TEXT role: TEXT + map_user_extra_permissions: + url: sqlite:///$GRAMEXDATA/roles.db + table: map_user_extra_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 2d2abc58..acdade7f 100644 --- a/gramex/handlers/basehandler.py +++ b/gramex/handlers/basehandler.py @@ -366,6 +366,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) @@ -449,7 +450,6 @@ def setup_ratelimit(cls, ratelimit: Union[dict, None], ratelimit_app_conf: Union cls._ratelimit.store = cls._get_store(ratelimit_app_conf) cls._on_init_methods.append(cls.check_ratelimit) cls._on_finish_methods.append(cls.update_ratelimit) - cls._on_finish_methods.append(cls.add_roles) @classmethod def reset_ratelimit(cls, pool: str, keys: List[Any], value: int = 0) -> bool: @@ -1100,21 +1100,31 @@ def initialize_handler(self): self.xsrf_token def add_roles(self): - '''Add roles to the current user''' + '''Add roles and permissions to the current user''' if isinstance(self.current_user, dict): - args = { + filter_args = { 'app': [gramex.config.variables.get('APPNAME', None)], 'namespace': [self.path_kwargs.get('namespace', None)], 'project': [self.path_kwargs.get('project', None)], 'user': [self.current_user.get('id', None)], } self.current_user['roles'] = gramex.data.filter( - **gramex.service.storelocations.roles, args=args + **gramex.service.storelocations.map_user_role, args=filter_args )['role'].tolist() - self.current_user['permissions'] = gramex.data.filter( - **gramex.service.storelocations.roles, args={'role': self.current_user['roles']} + + perms = gramex.data.filter( + **gramex.service.storelocations.map_user_extra_permissions, args=filter_args )['permission'].tolist() + del filter_args['user'] + filter_args['role'] = self.current_user['roles'] + + perms += gramex.data.filter( + **gramex.service.storelocations.roles, args=filter_args + )['permission'].tolist() + + self.current_user['permissions'] = list(set(perms)) + class BaseHandler(RequestHandler, BaseMixin): ''' From 7003d4b7972ac7d5fd58b69b348acacef967e2bf Mon Sep 17 00:00:00 2001 From: S Anand Date: Fri, 12 May 2023 15:29:10 +0530 Subject: [PATCH 3/5] TST: Refactor and add test cases --- gramex/gramex.yaml | 15 ++-------- gramex/handlers/basehandler.py | 50 ++++++++++++++++++---------------- tests/gramex.yaml | 11 ++++++++ tests/test_auth.py | 41 +++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 37 deletions(-) diff --git a/gramex/gramex.yaml b/gramex/gramex.yaml index 3e0cb90e..f1fec4ac 100644 --- a/gramex/gramex.yaml +++ b/gramex/gramex.yaml @@ -385,29 +385,20 @@ storelocations: app: TEXT namespace: TEXT project: TEXT + role: TEXT permission: TEXT - description: TEXT roles: url: sqlite:///$GRAMEXDATA/roles.db table: roles - columns: - app: TEXT - namespace: TEXT - project: TEXT - role: TEXT - permission: TEXT - map_user_role: - url: sqlite:///$GRAMEXDATA/roles.db - table: map_user_role columns: app: TEXT namespace: TEXT project: TEXT user: TEXT role: TEXT - map_user_extra_permissions: + user_permissions: url: sqlite:///$GRAMEXDATA/roles.db - table: map_user_extra_permissions + table: user_permissions columns: app: TEXT namespace: TEXT diff --git a/gramex/handlers/basehandler.py b/gramex/handlers/basehandler.py index acdade7f..ef205e05 100644 --- a/gramex/handlers/basehandler.py +++ b/gramex/handlers/basehandler.py @@ -1099,32 +1099,27 @@ def initialize_handler(self): if self._set_xsrf: self.xsrf_token - def add_roles(self): + def add_roles(self, roles_key='roles', permissions_key='permissions'): '''Add roles and permissions to the current user''' - if isinstance(self.current_user, dict): - filter_args = { - 'app': [gramex.config.variables.get('APPNAME', None)], - 'namespace': [self.path_kwargs.get('namespace', None)], - 'project': [self.path_kwargs.get('project', None)], - 'user': [self.current_user.get('id', None)], - } - self.current_user['roles'] = gramex.data.filter( - **gramex.service.storelocations.map_user_role, args=filter_args - )['role'].tolist() - - perms = gramex.data.filter( - **gramex.service.storelocations.map_user_extra_permissions, args=filter_args - )['permission'].tolist() - - del filter_args['user'] - filter_args['role'] = self.current_user['roles'] + 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)) - perms += gramex.data.filter( - **gramex.service.storelocations.roles, args=filter_args - )['permission'].tolist() - - self.current_user['permissions'] = list(set(perms)) - class BaseHandler(RequestHandler, BaseMixin): ''' @@ -1478,3 +1473,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 19c33a77..2a1130f9 100644 --- a/tests/gramex.yaml +++ b/tests/gramex.yaml @@ -2200,6 +2200,15 @@ url: sheet_name: userinfo id: userid + auth/roles: + pattern: /auth/roles + handler: SimpleAuth + kwargs: + template: $YAMLPATH/auth.html + credentials: + alpha: alpha + beta: beta + auth/rules: pattern: /auth/rulesdb handler: SimpleAuth @@ -2928,3 +2937,5 @@ storelocations: uri: TEXT user: TEXT request.method: TEXT + roles: + url: sqlite:///$YAMLPATH/roles.db diff --git a/tests/test_auth.py b/tests/test_auth.py index a7a04eca..37f5bada 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,45 @@ 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], + 'namespace': [None, None, None], + 'project': [None, None, None], + 'user': ['alpha', 'alpha', 'beta'], + 'role': ['junior', 'senior', 'lead'], + }, + ) + self.login_ok('alpha', 'alpha', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], ['junior', 'senior']) + + self.login_ok('beta', 'beta', check_next='/') + session = self.session.get(server.base_url + '/auth/session').json() + eq_(session['user']['roles'], ['lead']) + + class TestRules(AuthBase): url = server.base_url + '/auth/rulesdb' From 07e78680f9c87e8da70ff519537448bf94e875af Mon Sep 17 00:00:00 2001 From: Shraddheya Shrivastava Date: Thu, 1 Jun 2023 18:00:47 +0530 Subject: [PATCH 4/5] ENH: test cases for permissions --- tests/gramex.yaml | 4 ++++ tests/test_auth.py | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/gramex.yaml b/tests/gramex.yaml index 198c5aff..5780e387 100644 --- a/tests/gramex.yaml +++ b/tests/gramex.yaml @@ -2961,3 +2961,7 @@ storelocations: 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 37f5bada..54c5e468 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -601,21 +601,51 @@ def test_roles(self): 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': ['alpha', 'alpha', 'beta'], - 'role': ['junior', 'senior', 'lead'], + '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'])) + + # FIXME: find a proper user to test this scenario + # 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): From 2a52a564fa6530341b1c80d2ff4e4189fd0cb8c6 Mon Sep 17 00:00:00 2001 From: Shraddheya Shrivastava Date: Thu, 1 Jun 2023 18:03:43 +0530 Subject: [PATCH 5/5] ADD: gamma user for permissions testing --- tests/gramex.yaml | 1 + tests/test_auth.py | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/gramex.yaml b/tests/gramex.yaml index 5780e387..7a401480 100644 --- a/tests/gramex.yaml +++ b/tests/gramex.yaml @@ -2230,6 +2230,7 @@ url: credentials: alpha: alpha beta: beta + gamma: gamma auth/rules: pattern: /auth/rulesdb diff --git a/tests/test_auth.py b/tests/test_auth.py index 54c5e468..3ff6b9e8 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -641,11 +641,10 @@ def test_roles(self): eq_(session['user']['roles'], ['lead']) eq_(sorted(session['user']['permissions']), sorted(['read', 'write', 'delete'])) - # FIXME: find a proper user to test this scenario - # 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'])) + 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):