Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roles and Permissions for Gramex SAAS #728

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion gramex/gramex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions gramex/handlers/basehandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
'''
Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions tests/gramex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
70 changes: 69 additions & 1 deletion tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__))

Expand Down Expand Up @@ -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'

Expand Down