3
0
Fork 0

feat: add permission checks for menus, view routes

This commit is contained in:
Lance Edgar 2024-08-14 21:20:00 -05:00
parent 675b51cac2
commit e3942ce65e
11 changed files with 537 additions and 40 deletions

View file

@ -272,9 +272,8 @@ class MenuHandler(GenericHandler):
current user.
"""
perm = item.get('perm')
# TODO
# if perm:
# return request.has_perm(perm)
if perm:
return request.has_perm(perm)
return True
def _mark_allowed(self, request, menus):

View file

@ -139,6 +139,16 @@ def new_request_set_user(
pyramid_config.add_subscriber('wuttaweb.subscribers.new_request_set_user',
'pyramid.events.NewRequest')
You may wish to "supplement" this hook by registering your own
custom hook and then invoking this one as needed. You can then
pass certain params to override only parts of the logic:
:param user_getter: Optional getter function to retrieve the user
from database, instead of :func:`default_user_getter()`.
:param db_session: Optional :term:`db session` to use,
instead of :class:`wuttaweb.db.Session`.
This will add to the request object:
.. attribute:: request.user
@ -158,19 +168,36 @@ def new_request_set_user(
privileges. This is only possible if :attr:`request.is_admin`
is also true.
You may wish to "supplement" this hook by registering your own
custom hook and then invoking this one as needed. You can then
pass certain params to override only parts of the logic:
.. attribute:: request.user_permissions
:param user_getter: Optional getter function to retrieve the user
from database, instead of :func:`default_user_getter()`.
The ``set`` of permission names which are granted to the
current user.
This set is obtained by calling
:meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.get_permissions()`.
.. function:: request.has_perm(name)
Shortcut to check if current user has the given permission::
if not request.has_perm('users.edit'):
raise self.forbidden()
.. function:: request.has_any_perm(*names)
Shortcut to check if current user has any of the given
permissions::
if request.has_any_perm('users.list', 'users.view'):
return "can either list or view"
else:
raise self.forbidden()
:param db_session: Optional :term:`db session` to use,
instead of :class:`wuttaweb.db.Session`.
"""
request = event.request
config = request.registry.settings['wutta_config']
app = config.get_app()
auth = app.get_auth_handler()
# request.user
if db_session:
@ -179,7 +206,6 @@ def new_request_set_user(
# request.is_admin
def is_admin(request):
auth = app.get_auth_handler()
return auth.user_is_admin(request.user)
request.set_property(is_admin, reify=True)
@ -191,6 +217,29 @@ def new_request_set_user(
return False
request.set_property(is_root, reify=True)
# request.user_permissions
def user_permissions(request):
session = db_session or Session()
return auth.get_permissions(session, request.user)
request.set_property(user_permissions, reify=True)
# request.has_perm()
def has_perm(name):
if request.is_root:
return True
if name in request.user_permissions:
return True
return False
request.has_perm = has_perm
# request.has_any_perm()
def has_any_perm(*names):
for name in names:
if request.has_perm(name):
return True
return False
request.has_any_perm = has_any_perm
def before_render(event):
"""

View file

@ -222,7 +222,7 @@
% else:
<h1 class="title">${index_title}</h1>
% endif
% if master and master.creatable and not master.creating:
% if master and master.creatable and not master.creating and master.has_perm('create'):
<wutta-button once type="is-primary"
tag="a" href="${url(f'{route_prefix}.create')}"
icon-left="plus"
@ -235,8 +235,7 @@
<div class="level-right">
## TODO
% if master and master.configurable and not master.configuring:
% if master and master.configurable and not master.configuring and master.has_perm('configure'):
<div class="level-item">
<wutta-button once type="is-primary"
tag="a" href="${url(f'{route_prefix}.configure')}"

View file

@ -102,10 +102,50 @@ class CommonView(View):
# assign admin role
admin = auth.get_role_administrator(session)
user.roles.append(admin)
admin.notes = ("users in this role may \"become root\".\n\n"
"it's recommended not to grant other perms to this role.")
# ensure all built-in roles exist
auth.get_role_authenticated(session)
auth.get_role_anonymous(session)
# initialize built-in roles
authed = auth.get_role_authenticated(session)
authed.notes = ("this role represents any user who *is* logged in.\n\n"
"you may grant any perms you like to it.")
anon = auth.get_role_anonymous(session)
anon.notes = ("this role represents any user who is *not* logged in.\n\n"
"you may grant any perms you like to it.")
# also make "Site Admin" role
site_admin_perms = [
'appinfo.list',
'appinfo.configure',
'people.list',
'people.create',
'people.view',
'people.edit',
'people.delete',
'roles.list',
'roles.create',
'roles.view',
'roles.edit',
'roles.edit_builtin',
'roles.delete',
'settings.list',
'settings.create',
'settings.view',
'settings.edit',
'settings.delete',
'users.list',
'users.create',
'users.view',
'users.edit',
'users.delete',
]
admin2 = model.Role(name="Site Admin")
admin2.notes = ("this is the \"daily driver\" admin role.\n\n"
"you may grant any perms you like to it.")
session.add(admin2)
user.roles.append(admin2)
for perm in site_admin_perms:
auth.grant_permission(admin2, perm)
# maybe make person
if data['first_name'] or data['last_name']:

View file

@ -198,6 +198,8 @@ class MasterView(View):
i.e. it should have an :meth:`edit()` view. Default value is
``True``.
See also :meth:`is_editable()`.
.. attribute:: deletable
Boolean indicating whether the view model supports "deleting" -
@ -802,6 +804,43 @@ class MasterView(View):
# support methods
##############################
def has_perm(self, name):
"""
Shortcut to check if current user has the given permission.
This will automatically add the :attr:`permission_prefix` to
``name`` before passing it on to
:func:`~wuttaweb.subscribers.request.has_perm()`.
For instance within the
:class:`~wuttaweb.views.users.UserView` these give the same
result::
self.request.has_perm('users.edit')
self.has_perm('edit')
So this shortcut only applies to permissions defined for the
current master view. The first example above must still be
used to check for "foreign" permissions (i.e. any needing a
different prefix).
"""
permission_prefix = self.get_permission_prefix()
return self.request.has_perm(f'{permission_prefix}.{name}')
def has_any_perm(self, *names):
"""
Shortcut to check if current user has any of the given
permissions.
This calls :meth:`has_perm()` until one returns ``True``. If
none do, returns ``False``.
"""
for name in names:
if self.has_perm(name):
return True
return False
def render_to_response(self, template, context):
"""
Locate and render an appropriate template, with the given
@ -937,15 +976,15 @@ class MasterView(View):
# TODO: should split this off into index_get_grid_actions() ?
if self.viewable:
if self.viewable and self.has_perm('view'):
actions.append(self.make_grid_action('view', icon='eye',
url=self.get_action_url_view))
if self.editable:
if self.editable and self.has_perm('edit'):
actions.append(self.make_grid_action('edit', icon='edit',
url=self.get_action_url_edit))
if self.deletable:
if self.deletable and self.has_perm('delete'):
actions.append(self.make_grid_action('delete', icon='trash',
url=self.get_action_url_delete,
link_class='has-text-danger'))
@ -1137,14 +1176,19 @@ class MasterView(View):
def get_action_url_edit(self, obj, i):
"""
Returns the "edit" grid action URL for the given object.
Returns the "edit" grid action URL for the given object, if
applicable.
Most typically this is like ``/widgets/XXX/edit`` where
``XXX`` represents the object's key/ID.
Calls :meth:`get_action_url()` under the hood.
This first calls :meth:`is_editable()` and if that is false,
this method will return ``None``.
Calls :meth:`get_action_url()` to generate the true URL.
"""
return self.get_action_url('edit', obj)
if self.is_editable(obj):
return self.get_action_url('edit', obj)
def get_action_url_delete(self, obj, i):
"""
@ -1162,6 +1206,19 @@ class MasterView(View):
if self.is_deletable(obj):
return self.get_action_url('delete', obj)
def is_editable(self, obj):
"""
Returns a boolean indicating whether "edit" should be allowed
for the given model instance (and for current user).
By default this always return ``True``; subclass can override
if needed.
Note that the use of this method implies :attr:`editable` is
true, so the method does not need to check that flag.
"""
return True
def is_deletable(self, obj):
"""
Returns a boolean indicating whether "delete" should be
@ -1634,7 +1691,8 @@ class MasterView(View):
if cls.listable:
config.add_route(route_prefix, f'{url_prefix}/')
config.add_view(cls, attr='index',
route_name=route_prefix)
route_name=route_prefix,
permission=f'{permission_prefix}.list')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.list',
f"Browse / search {model_title_plural}")
@ -1644,7 +1702,8 @@ class MasterView(View):
config.add_route(f'{route_prefix}.create',
f'{url_prefix}/new')
config.add_view(cls, attr='create',
route_name=f'{route_prefix}.create')
route_name=f'{route_prefix}.create',
permission=f'{permission_prefix}.create')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.create',
f"Create new {model_title}")
@ -1654,7 +1713,8 @@ class MasterView(View):
instance_url_prefix = cls.get_instance_url_prefix()
config.add_route(f'{route_prefix}.view', instance_url_prefix)
config.add_view(cls, attr='view',
route_name=f'{route_prefix}.view')
route_name=f'{route_prefix}.view',
permission=f'{permission_prefix}.view')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.view',
f"View {model_title}")
@ -1665,7 +1725,8 @@ class MasterView(View):
config.add_route(f'{route_prefix}.edit',
f'{instance_url_prefix}/edit')
config.add_view(cls, attr='edit',
route_name=f'{route_prefix}.edit')
route_name=f'{route_prefix}.edit',
permission=f'{permission_prefix}.edit')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.edit',
f"Edit {model_title}")
@ -1676,7 +1737,8 @@ class MasterView(View):
config.add_route(f'{route_prefix}.delete',
f'{instance_url_prefix}/delete')
config.add_view(cls, attr='delete',
route_name=f'{route_prefix}.delete')
route_name=f'{route_prefix}.delete',
permission=f'{permission_prefix}.delete')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.delete',
f"Delete {model_title}")
@ -1686,7 +1748,8 @@ class MasterView(View):
config.add_route(f'{route_prefix}.configure',
f'{url_prefix}/configure')
config.add_view(cls, attr='configure',
route_name=f'{route_prefix}.configure')
route_name=f'{route_prefix}.configure',
permission=f'{permission_prefix}.configure')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.configure',
f"Configure {model_title_plural}")

View file

@ -69,6 +69,22 @@ class RoleView(MasterView):
# notes
g.set_renderer('notes', self.grid_render_notes)
def is_editable(self, role):
""" """
session = self.app.get_session(role)
auth = self.app.get_auth_handler()
# only "root" can edit admin role
if role is auth.get_role_administrator(session):
return self.request.is_root
# other built-in roles require special perm
if role in (auth.get_role_authenticated(session),
auth.get_role_anonymous(session)):
return self.has_perm('edit_builtin')
return True
def is_deletable(self, role):
""" """
session = self.app.get_session(role)
@ -228,6 +244,21 @@ class RoleView(MasterView):
else:
auth.revoke_permission(role, pkey)
@classmethod
def defaults(cls, config):
cls._defaults(config)
cls._role_defaults(config)
@classmethod
def _role_defaults(cls, config):
permission_prefix = cls.get_permission_prefix()
model_title_plural = cls.get_model_title_plural()
# perm to edit built-in roles
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.edit_builtin',
f"Edit the Built-in {model_title_plural}")
def defaults(config, **kwargs):
base = globals()