Source code for wuttaweb.views.roles

# -*- coding: utf-8; -*-
################################################################################
#
#  wuttaweb -- Web App for Wutta Framework
#  Copyright © 2024-2026 Lance Edgar
#
#  This file is part of Wutta Framework.
#
#  Wutta Framework is free software: you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  Wutta Framework is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#  more details.
#
#  You should have received a copy of the GNU General Public License along with
#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for roles
"""

from wuttjamaican.db.model import Role, Permission
from wuttaweb.views import MasterView
from wuttaweb.db import Session
from wuttaweb.forms import widgets
from wuttaweb.forms.schema import Permissions, RoleRef
from wuttaweb.util import make_users_grid


[docs] class RoleView(MasterView): # pylint: disable=abstract-method """ Master view for roles. Default route prefix is ``roles``. Notable URLs provided by this class: * ``/roles/`` * ``/roles/new`` * ``/roles/XXX`` * ``/roles/XXX/edit`` * ``/roles/XXX/delete`` """ model_class = Role grid_columns = [ "name", "notes", ] filter_defaults = { "name": {"active": True}, } sort_defaults = "name" mergeable = True merge_additive_fields = ["permission_count", "user_count"] wutta_permissions = None # TODO: master should handle this, possibly via configure_form() def get_query(self, session=None): # pylint: disable=empty-docstring """ """ model = self.app.model query = super().get_query(session=session) return query.order_by(model.Role.name) def configure_grid(self, grid): # pylint: disable=empty-docstring """ """ g = grid super().configure_grid(g) # name g.set_link("name") # notes g.set_renderer("notes", self.grid_render_notes) def is_editable(self, obj): # pylint: disable=empty-docstring """ """ role = obj 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, obj): # pylint: disable=empty-docstring """ """ role = obj session = self.app.get_session(role) auth = self.app.get_auth_handler() # prevent delete for built-in roles if role is auth.get_role_authenticated(session): return False if role is auth.get_role_anonymous(session): return False if role is auth.get_role_administrator(session): return False return True def configure_form(self, form): # pylint: disable=empty-docstring """ """ f = form super().configure_form(f) role = f.model_instance # never show these f.remove("permission_refs", "user_refs") # name f.set_validator("name", self.unique_name) # notes f.set_widget("notes", widgets.NotesWidget()) # users if not (self.creating or self.editing): f.append("users") f.set_grid("users", self.make_users_grid(role)) # permissions f.append("permissions") self.wutta_permissions = self.get_available_permissions() f.set_node( "permissions", Permissions(self.request, permissions=self.wutta_permissions) ) if not self.creating: f.set_default("permissions", list(role.permissions))
[docs] def make_users_grid(self, role): """ Make and return the grid for the Users field. This grid is shown for the Users field when viewing a Role. :returns: Fully configured :class:`~wuttaweb.grids.base.Grid` instance. """ return make_users_grid( self.request, route_prefix=self.get_route_prefix(), data=role.users, columns=[ "username", "person", "active", ], )
def unique_name(self, node, value): # pylint: disable=empty-docstring """ """ model = self.app.model session = Session() query = session.query(model.Role).filter(model.Role.name == value) if self.editing: uuid = self.request.matchdict["uuid"] query = query.filter(model.Role.uuid != uuid) if query.count(): node.raise_invalid("Name must be unique")
[docs] def get_available_permissions(self): """ Returns all "available" permissions. This is used when viewing or editing a role; the result is passed into the :class:`~wuttaweb.forms.schema.Permissions` field schema. The app itself must be made aware of each permission, in order for them to found by this method. This is done via :func:`~wuttaweb.auth.add_permission_group()` and :func:`~wuttaweb.auth.add_permission()`. When in "view" (readonly) mode, this method will return the full set of known permissions. However in "edit" mode, it will prune the set to remove any permissions which the current user does not also have. The idea here is to allow "many" users to manage roles, but ensure they cannot "break out" of their own role by assigning extra permissions to it. The permissions returned will also be grouped, and each single permission is also represented as a simple dict, e.g.:: { 'books': { 'key': 'books', 'label': "Books", 'perms': { 'books.list': { 'key': 'books.list', 'label': "Browse / search Books", }, 'books.view': { 'key': 'books.view', 'label': "View Book", }, }, }, 'widgets': { 'key': 'widgets', 'label': "Widgets", 'perms': { 'widgets.list': { 'key': 'widgets.list', 'label': "Browse / search Widgets", }, 'widgets.view': { 'key': 'widgets.view', 'label': "View Widget", }, }, }, } """ # get all known permissions from settings cache permissions = self.request.registry.settings.get("wutta_permissions", {}) # when viewing, we allow all permissions to be exposed for all users if self.viewing: return permissions # admin user gets to manage all permissions if self.request.is_admin: return permissions # non-admin user can only see permissions they're granted available = {} for gkey, group in permissions.items(): for pkey, perm in group["perms"].items(): 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 objectify(self, form): # pylint: disable=empty-docstring """ """ # normal logic first role = super().objectify(form) # update permissions for role self.update_permissions(role, form) return role def update_permissions(self, role, form): # pylint: disable=empty-docstring """ """ if "permissions" not in form.validated: return auth = self.app.get_auth_handler() available = self.wutta_permissions permissions = form.validated["permissions"] for group in available.values(): for pkey in group["perms"]: if pkey in permissions: auth.grant_permission(role, pkey) else: auth.revoke_permission(role, pkey) def merge_get_data(self, obj): # pylint: disable=empty-docstring """ """ data = super().merge_get_data(obj) role = obj data["permissions"] = role.permissions data["permission_count"] = len(data["permissions"]) data["usernames"] = [user.username for user in role.users] data["user_count"] = len(data["usernames"]) return data def merge_get_final_data( self, removing, keeping ): # pylint: disable=empty-docstring """ """ final = super().merge_get_final_data(removing, keeping) permissions = set(removing["permissions"] + keeping["permissions"]) final["permission_count"] = len(permissions) usernames = set(removing["usernames"] + keeping["usernames"]) final["user_count"] = len(usernames) return final
[docs] def merge_why_not(self, removing, keeping): """ This checks to ensure the "removing" role is not one of the special built-in roles (Administrator, Authenticated, Anonymous). See also parent method: :meth:`~wuttaweb.views.master.MasterView.merge_why_not()` """ auth = self.app.get_auth_handler() session = self.Session() if removing is auth.get_role_administrator(session): return "Cannot remove the Administrator role." if removing is auth.get_role_anonymous(session): return "Cannot remove the Anonymous role." if removing is auth.get_role_authenticated(session): return "Cannot remove the Authenticated role." return None
[docs] def merge_execute(self, removing, keeping): """ The logic to merge 2 roles is extended as follows: Any users belonging to the "removing" role will be added to the "keeping" role (if not already present). Any permissions belonging to the "removing" role will be added to the "keeping" role (if not already present). See also parent method: :meth:`~wuttaweb.views.master.MasterView.merge_execute()` """ # transfer permissions for perm in list(removing.permissions): if perm not in keeping.permissions: keeping.permissions.append(perm) # transfer users for user in list(removing.users): if user not in keeping.users: keeping.users.append(user) # continue default merge super().merge_execute(removing, keeping)
@classmethod def defaults(cls, config): # pylint: disable=empty-docstring """ """ 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}", )
[docs] class PermissionView(MasterView): # pylint: disable=abstract-method """ Master view for permissions. Default route prefix is ``permissions``. Notable URLs provided by this class: * ``/permissions/`` * ``/permissions/XXX`` * ``/permissions/XXX/delete`` """ model_class = Permission creatable = False editable = False grid_columns = [ "role", "permission", ] sort_defaults = "role" form_fields = [ "role", "permission", ] def get_query(self, **kwargs): # pylint: disable=empty-docstring,arguments-differ """ """ query = super().get_query(**kwargs) model = self.app.model # always join on Role query = query.join(model.Role) return query def configure_grid(self, grid): # pylint: disable=empty-docstring """ """ g = grid super().configure_grid(g) model = self.app.model # role g.set_sorter("role", model.Role.name) g.set_filter("role", model.Role.name, label="Role Name") g.set_link("role") # permission g.set_link("permission") def configure_form(self, form): # pylint: disable=empty-docstring """ """ f = form super().configure_form(f) # role f.set_node("role", RoleRef(self.request))
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring base = globals() RoleView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name "RoleView", base["RoleView"] ) RoleView.defaults(config) PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name "PermissionView", base["PermissionView"] ) PermissionView.defaults(config) def includeme(config): # pylint: disable=missing-function-docstring defaults(config)