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
|
||||
# Copyright © 2010-2018 Lance Edgar
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -30,7 +30,8 @@ import six
|
|||
from sqlalchemy import orm
|
||||
|
||||
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
|
||||
from deform import widget as dfwidget
|
||||
|
@ -65,6 +66,42 @@ class RolesView(PrincipalMasterView):
|
|||
g.set_sort_defaults('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):
|
||||
query = self.Session.query(model.Role)\
|
||||
.filter(model.Role.name == value)
|
||||
|
@ -82,7 +119,7 @@ class RolesView(PrincipalMasterView):
|
|||
f.set_validator('name', self.unique_name)
|
||||
|
||||
# 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_node('permissions', colander.Set())
|
||||
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()):
|
||||
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):
|
||||
if role is guest_role(self.Session()):
|
||||
return "(not applicable)"
|
||||
|
@ -109,12 +184,34 @@ class RolesView(PrincipalMasterView):
|
|||
return six.text_type(role.session_timeout)
|
||||
|
||||
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:
|
||||
data = form.validated
|
||||
role = super(RolesView, self).objectify(form, data)
|
||||
role.permissions = data['permissions']
|
||||
self.update_permissions(role, data['permissions'])
|
||||
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):
|
||||
role = kwargs['instance']
|
||||
if role.users:
|
||||
|
@ -152,6 +249,24 @@ class RolesView(PrincipalMasterView):
|
|||
roles.append(role)
|
||||
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):
|
||||
template = 'permissions'
|
||||
|
|
|
@ -132,6 +132,44 @@ class UsersView(PrincipalMasterView):
|
|||
g.set_link('last_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):
|
||||
query = self.Session.query(model.User)\
|
||||
.filter(model.User.username == value)
|
||||
|
@ -328,16 +366,6 @@ class UsersView(PrincipalMasterView):
|
|||
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
||||
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):
|
||||
return self.Session.query(model.UserEvent)\
|
||||
.filter(model.UserEvent.user == user)
|
||||
|
|
Loading…
Reference in a new issue