Misc. changes to User, Role permissions and management thereof
* only "root" can edit the Administrator role * edit of Authenticated and Guest roles requires dedicated permission * edit of role(s) to which current user belongs, requires dedicated permission * delete is not allowed for any built-in role * when editing a role, user can only add/remove permissions they themselves have * settings can define some "protected" users, which only "root" can edit/delete
This commit is contained in:
parent
413e9b0f1e
commit
edd48ef667
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -30,7 +30,8 @@ import six
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.db.auth import has_permission, administrator_role, guest_role, authenticated_role
|
from rattail.db.auth import (has_permission, grant_permission, revoke_permission,
|
||||||
|
administrator_role, guest_role, authenticated_role)
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
|
@ -65,6 +66,42 @@ class RolesView(PrincipalMasterView):
|
||||||
g.set_sort_defaults('name')
|
g.set_sort_defaults('name')
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
|
|
||||||
|
def editable_instance(self, role):
|
||||||
|
"""
|
||||||
|
We must prevent edit for certain built-in roles etc., depending on
|
||||||
|
current user's permissions.
|
||||||
|
"""
|
||||||
|
# only "root" can edit Administrator
|
||||||
|
if role is administrator_role(self.Session()):
|
||||||
|
return self.request.is_root
|
||||||
|
|
||||||
|
# can edit Authenticated only if user has permission
|
||||||
|
if role is authenticated_role(self.Session()):
|
||||||
|
return self.has_perm('edit_authenticated')
|
||||||
|
|
||||||
|
# can edit Guest only if user has permission
|
||||||
|
if role is guest_role(self.Session()):
|
||||||
|
return self.has_perm('edit_guest')
|
||||||
|
|
||||||
|
# current user can edit their own roles, only if they have permission
|
||||||
|
user = self.request.user
|
||||||
|
if user and role in user.roles:
|
||||||
|
return self.has_perm('edit_my')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def deletable_instance(self, role):
|
||||||
|
"""
|
||||||
|
We must prevent deletion for all built-in roles.
|
||||||
|
"""
|
||||||
|
if role is administrator_role(self.Session()):
|
||||||
|
return False
|
||||||
|
if role is authenticated_role(self.Session()):
|
||||||
|
return False
|
||||||
|
if role is guest_role(self.Session()):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def unique_name(self, node, value):
|
def unique_name(self, node, value):
|
||||||
query = self.Session.query(model.Role)\
|
query = self.Session.query(model.Role)\
|
||||||
.filter(model.Role.name == value)
|
.filter(model.Role.name == value)
|
||||||
|
@ -82,7 +119,7 @@ class RolesView(PrincipalMasterView):
|
||||||
f.set_validator('name', self.unique_name)
|
f.set_validator('name', self.unique_name)
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
self.tailbone_permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
self.tailbone_permissions = self.get_available_permissions()
|
||||||
f.set_renderer('permissions', PermissionsRenderer(permissions=self.tailbone_permissions))
|
f.set_renderer('permissions', PermissionsRenderer(permissions=self.tailbone_permissions))
|
||||||
f.set_node('permissions', colander.Set())
|
f.set_node('permissions', colander.Set())
|
||||||
f.set_widget('permissions', PermissionsWidget(permissions=self.tailbone_permissions))
|
f.set_widget('permissions', PermissionsWidget(permissions=self.tailbone_permissions))
|
||||||
|
@ -101,6 +138,44 @@ class RolesView(PrincipalMasterView):
|
||||||
if self.editing and role is guest_role(self.Session()):
|
if self.editing and role is guest_role(self.Session()):
|
||||||
f.set_readonly('session_timeout')
|
f.set_readonly('session_timeout')
|
||||||
|
|
||||||
|
def get_available_permissions(self):
|
||||||
|
"""
|
||||||
|
Should return a dictionary with all "available" permissions. The
|
||||||
|
result of this will vary depending on the current user, because a user
|
||||||
|
is only allowed to "manage" permissions which they themselves have been
|
||||||
|
granted.
|
||||||
|
|
||||||
|
In other words the return value will be the "full set" of permissions,
|
||||||
|
if the current user is an admin; otherwise it will be the "subset" of
|
||||||
|
permissions which the current user has been granted.
|
||||||
|
"""
|
||||||
|
# fetch full set of permissions registered in the app
|
||||||
|
permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
||||||
|
|
||||||
|
# admin user gets to manage all permissions
|
||||||
|
if self.request.is_admin:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# when viewing, we allow all permissions to be exposed for all users
|
||||||
|
if self.viewing:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# non-admin user can only manage permissions they've been granted
|
||||||
|
# TODO: it seems a bit ugly, to "rebuild" permission groups like this,
|
||||||
|
# but not sure if there's a better way?
|
||||||
|
available = {}
|
||||||
|
for gkey, group in six.iteritems(permissions):
|
||||||
|
for pkey, perm in six.iteritems(group['perms']):
|
||||||
|
if self.request.has_perm(pkey):
|
||||||
|
if gkey not in available:
|
||||||
|
available[gkey] = {
|
||||||
|
'key': gkey,
|
||||||
|
'label': group['label'],
|
||||||
|
'perms': {},
|
||||||
|
}
|
||||||
|
available[gkey]['perms'][pkey] = perm
|
||||||
|
return available
|
||||||
|
|
||||||
def render_session_timeout(self, role, field):
|
def render_session_timeout(self, role, field):
|
||||||
if role is guest_role(self.Session()):
|
if role is guest_role(self.Session()):
|
||||||
return "(not applicable)"
|
return "(not applicable)"
|
||||||
|
@ -109,12 +184,34 @@ class RolesView(PrincipalMasterView):
|
||||||
return six.text_type(role.session_timeout)
|
return six.text_type(role.session_timeout)
|
||||||
|
|
||||||
def objectify(self, form, data=None):
|
def objectify(self, form, data=None):
|
||||||
|
"""
|
||||||
|
Supplements the default logic, as follows:
|
||||||
|
|
||||||
|
The role is updated as per usual, and then we also invoke
|
||||||
|
:meth:`update_permissions()` in order to correctly handle that part,
|
||||||
|
i.e. ensure the user can't modify permissions which they do not have.
|
||||||
|
"""
|
||||||
if data is None:
|
if data is None:
|
||||||
data = form.validated
|
data = form.validated
|
||||||
role = super(RolesView, self).objectify(form, data)
|
role = super(RolesView, self).objectify(form, data)
|
||||||
role.permissions = data['permissions']
|
self.update_permissions(role, data['permissions'])
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
def update_permissions(self, role, permissions):
|
||||||
|
"""
|
||||||
|
Update the given role's permissions, according to those specified.
|
||||||
|
Note that this will not simply "clobber" the role's existing
|
||||||
|
permissions, but rather each "available" permission (depends on current
|
||||||
|
user) will be examined individually, and updated as needed.
|
||||||
|
"""
|
||||||
|
available = self.tailbone_permissions
|
||||||
|
for gkey, group in six.iteritems(available):
|
||||||
|
for pkey, perm in six.iteritems(group['perms']):
|
||||||
|
if pkey in permissions:
|
||||||
|
grant_permission(role, pkey)
|
||||||
|
else:
|
||||||
|
revoke_permission(role, pkey)
|
||||||
|
|
||||||
def template_kwargs_view(self, **kwargs):
|
def template_kwargs_view(self, **kwargs):
|
||||||
role = kwargs['instance']
|
role = kwargs['instance']
|
||||||
if role.users:
|
if role.users:
|
||||||
|
@ -152,6 +249,24 @@ class RolesView(PrincipalMasterView):
|
||||||
roles.append(role)
|
roles.append(role)
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._principal_defaults(config)
|
||||||
|
cls._role_defaults(config)
|
||||||
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _role_defaults(cls, config):
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
|
||||||
|
# extra permissions for editing built-in roles etc.
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.edit_authenticated'.format(permission_prefix),
|
||||||
|
"Edit the \"Authenticated\" Role")
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.edit_guest'.format(permission_prefix),
|
||||||
|
"Edit the \"Guest\" Role")
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.edit_my'.format(permission_prefix),
|
||||||
|
"Edit Role(s) to which current user belongs")
|
||||||
|
|
||||||
|
|
||||||
class PermissionsWidget(dfwidget.Widget):
|
class PermissionsWidget(dfwidget.Widget):
|
||||||
template = 'permissions'
|
template = 'permissions'
|
||||||
|
|
|
@ -132,6 +132,44 @@ class UsersView(PrincipalMasterView):
|
||||||
g.set_link('last_name')
|
g.set_link('last_name')
|
||||||
g.set_link('display_name')
|
g.set_link('display_name')
|
||||||
|
|
||||||
|
def editable_instance(self, user):
|
||||||
|
"""
|
||||||
|
If the given user is "protected" then we only allow edit if current
|
||||||
|
user is "root". But if the given user is not protected, this simply
|
||||||
|
returns ``True``.
|
||||||
|
"""
|
||||||
|
if self.user_is_protected(user):
|
||||||
|
return self.request.is_root
|
||||||
|
return True
|
||||||
|
|
||||||
|
def deletable_instance(self, user):
|
||||||
|
"""
|
||||||
|
If the given user is "protected" then we only allow delete if current
|
||||||
|
user is "root". But if the given user is not protected, this simply
|
||||||
|
returns ``True``.
|
||||||
|
"""
|
||||||
|
if self.user_is_protected(user):
|
||||||
|
return self.request.is_root
|
||||||
|
return True
|
||||||
|
|
||||||
|
def user_is_protected(self, user):
|
||||||
|
"""
|
||||||
|
This logic will consult the settings, for a list of "protected"
|
||||||
|
usernames, which should require root privileges to edit. If no setting
|
||||||
|
is found, or the given ``user`` is not represented in the setting, then
|
||||||
|
edit is allowed.
|
||||||
|
|
||||||
|
But if there is a setting, and the ``user`` is represented in it, then
|
||||||
|
this method will return ``True`` only if the "current" app user is
|
||||||
|
"root", otherwise will return ``False``.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, 'protected_usernames'):
|
||||||
|
self.protected_usernames = self.rattail_config.getlist(
|
||||||
|
'tailbone', 'protected_usernames')
|
||||||
|
if self.protected_usernames and user.username in self.protected_usernames:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def unique_username(self, node, value):
|
def unique_username(self, node, value):
|
||||||
query = self.Session.query(model.User)\
|
query = self.Session.query(model.User)\
|
||||||
.filter(model.User.username == value)
|
.filter(model.User.username == value)
|
||||||
|
@ -328,16 +366,6 @@ class UsersView(PrincipalMasterView):
|
||||||
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
||||||
return HTML.tag('ul', c=items)
|
return HTML.tag('ul', c=items)
|
||||||
|
|
||||||
def editable_instance(self, user):
|
|
||||||
if self.rattail_config.demo():
|
|
||||||
return user.username != 'chuck'
|
|
||||||
return True
|
|
||||||
|
|
||||||
def deletable_instance(self, user):
|
|
||||||
if self.rattail_config.demo():
|
|
||||||
return user.username != 'chuck'
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_row_data(self, user):
|
def get_row_data(self, user):
|
||||||
return self.Session.query(model.UserEvent)\
|
return self.Session.query(model.UserEvent)\
|
||||||
.filter(model.UserEvent.user == user)
|
.filter(model.UserEvent.user == user)
|
||||||
|
|
Loading…
Reference in a new issue