Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / views / roles.py: 100%
137 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 15:23 -0600
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 15:23 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024-2025 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Views for roles
25"""
27from wuttjamaican.db.model import Role, Permission
28from wuttaweb.views import MasterView
29from wuttaweb.db import Session
30from wuttaweb.forms import widgets
31from wuttaweb.forms.schema import Permissions, RoleRef
32from wuttaweb.util import make_users_grid
35class RoleView(MasterView): # pylint: disable=abstract-method
36 """
37 Master view for roles.
39 Default route prefix is ``roles``.
41 Notable URLs provided by this class:
43 * ``/roles/``
44 * ``/roles/new``
45 * ``/roles/XXX``
46 * ``/roles/XXX/edit``
47 * ``/roles/XXX/delete``
48 """
50 model_class = Role
52 grid_columns = [
53 "name",
54 "notes",
55 ]
57 filter_defaults = {
58 "name": {"active": True},
59 }
60 sort_defaults = "name"
62 wutta_permissions = None
64 # TODO: master should handle this, possibly via configure_form()
65 def get_query(self, session=None): # pylint: disable=empty-docstring
66 """ """
67 model = self.app.model
68 query = super().get_query(session=session)
69 return query.order_by(model.Role.name)
71 def configure_grid(self, grid): # pylint: disable=empty-docstring
72 """ """
73 g = grid
74 super().configure_grid(g)
76 # name
77 g.set_link("name")
79 # notes
80 g.set_renderer("notes", self.grid_render_notes)
82 def is_editable(self, obj): # pylint: disable=empty-docstring
83 """ """
84 role = obj
85 session = self.app.get_session(role)
86 auth = self.app.get_auth_handler()
88 # only "root" can edit admin role
89 if role is auth.get_role_administrator(session):
90 return self.request.is_root
92 # other built-in roles require special perm
93 if role in (
94 auth.get_role_authenticated(session),
95 auth.get_role_anonymous(session),
96 ):
97 return self.has_perm("edit_builtin")
99 return True
101 def is_deletable(self, obj): # pylint: disable=empty-docstring
102 """ """
103 role = obj
104 session = self.app.get_session(role)
105 auth = self.app.get_auth_handler()
107 # prevent delete for built-in roles
108 if role is auth.get_role_authenticated(session):
109 return False
110 if role is auth.get_role_anonymous(session):
111 return False
112 if role is auth.get_role_administrator(session):
113 return False
115 return True
117 def configure_form(self, form): # pylint: disable=empty-docstring
118 """ """
119 f = form
120 super().configure_form(f)
121 role = f.model_instance
123 # never show these
124 f.remove("permission_refs", "user_refs")
126 # name
127 f.set_validator("name", self.unique_name)
129 # notes
130 f.set_widget("notes", widgets.NotesWidget())
132 # users
133 if not (self.creating or self.editing):
134 f.append("users")
135 f.set_grid("users", self.make_users_grid(role))
137 # permissions
138 f.append("permissions")
139 self.wutta_permissions = self.get_available_permissions()
140 f.set_node(
141 "permissions", Permissions(self.request, permissions=self.wutta_permissions)
142 )
143 if not self.creating:
144 f.set_default("permissions", list(role.permissions))
146 def make_users_grid(self, role):
147 """
148 Make and return the grid for the Users field.
150 This grid is shown for the Users field when viewing a Role.
152 :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
153 instance.
154 """
155 return make_users_grid(
156 self.request,
157 route_prefix=self.get_route_prefix(),
158 data=role.users,
159 columns=[
160 "username",
161 "person",
162 "active",
163 ],
164 )
166 def unique_name(self, node, value): # pylint: disable=empty-docstring
167 """ """
168 model = self.app.model
169 session = Session()
171 query = session.query(model.Role).filter(model.Role.name == value)
173 if self.editing:
174 uuid = self.request.matchdict["uuid"]
175 query = query.filter(model.Role.uuid != uuid)
177 if query.count():
178 node.raise_invalid("Name must be unique")
180 def get_available_permissions(self):
181 """
182 Returns all "available" permissions. This is used when
183 viewing or editing a role; the result is passed into the
184 :class:`~wuttaweb.forms.schema.Permissions` field schema.
186 The app itself must be made aware of each permission, in order
187 for them to found by this method. This is done via
188 :func:`~wuttaweb.auth.add_permission_group()` and
189 :func:`~wuttaweb.auth.add_permission()`.
191 When in "view" (readonly) mode, this method will return the
192 full set of known permissions.
194 However in "edit" mode, it will prune the set to remove any
195 permissions which the current user does not also have. The
196 idea here is to allow "many" users to manage roles, but ensure
197 they cannot "break out" of their own role by assigning extra
198 permissions to it.
200 The permissions returned will also be grouped, and each single
201 permission is also represented as a simple dict, e.g.::
203 {
204 'books': {
205 'key': 'books',
206 'label': "Books",
207 'perms': {
208 'books.list': {
209 'key': 'books.list',
210 'label': "Browse / search Books",
211 },
212 'books.view': {
213 'key': 'books.view',
214 'label': "View Book",
215 },
216 },
217 },
218 'widgets': {
219 'key': 'widgets',
220 'label': "Widgets",
221 'perms': {
222 'widgets.list': {
223 'key': 'widgets.list',
224 'label': "Browse / search Widgets",
225 },
226 'widgets.view': {
227 'key': 'widgets.view',
228 'label': "View Widget",
229 },
230 },
231 },
232 }
233 """
235 # get all known permissions from settings cache
236 permissions = self.request.registry.settings.get("wutta_permissions", {})
238 # when viewing, we allow all permissions to be exposed for all users
239 if self.viewing:
240 return permissions
242 # admin user gets to manage all permissions
243 if self.request.is_admin:
244 return permissions
246 # non-admin user can only see permissions they're granted
247 available = {}
248 for gkey, group in permissions.items():
249 for pkey, perm in group["perms"].items():
250 if self.request.has_perm(pkey):
251 if gkey not in available:
252 available[gkey] = {
253 "key": gkey,
254 "label": group["label"],
255 "perms": {},
256 }
257 available[gkey]["perms"][pkey] = perm
259 return available
261 def objectify(self, form): # pylint: disable=empty-docstring
262 """ """
263 # normal logic first
264 role = super().objectify(form)
266 # update permissions for role
267 self.update_permissions(role, form)
269 return role
271 def update_permissions(self, role, form): # pylint: disable=empty-docstring
272 """ """
273 if "permissions" not in form.validated:
274 return
276 auth = self.app.get_auth_handler()
277 available = self.wutta_permissions
278 permissions = form.validated["permissions"]
280 for group in available.values():
281 for pkey in group["perms"]:
282 if pkey in permissions:
283 auth.grant_permission(role, pkey)
284 else:
285 auth.revoke_permission(role, pkey)
287 @classmethod
288 def defaults(cls, config): # pylint: disable=empty-docstring
289 """ """
290 cls._defaults(config)
291 cls._role_defaults(config)
293 @classmethod
294 def _role_defaults(cls, config):
295 permission_prefix = cls.get_permission_prefix()
296 model_title_plural = cls.get_model_title_plural()
298 # perm to edit built-in roles
299 config.add_wutta_permission(
300 permission_prefix,
301 f"{permission_prefix}.edit_builtin",
302 f"Edit the Built-in {model_title_plural}",
303 )
306class PermissionView(MasterView): # pylint: disable=abstract-method
307 """
308 Master view for permissions.
310 Default route prefix is ``permissions``.
312 Notable URLs provided by this class:
314 * ``/permissions/``
315 * ``/permissions/XXX``
316 * ``/permissions/XXX/delete``
317 """
319 model_class = Permission
320 creatable = False
321 editable = False
323 grid_columns = [
324 "role",
325 "permission",
326 ]
328 sort_defaults = "role"
330 form_fields = [
331 "role",
332 "permission",
333 ]
335 def get_query(self, **kwargs): # pylint: disable=empty-docstring,arguments-differ
336 """ """
337 query = super().get_query(**kwargs)
338 model = self.app.model
340 # always join on Role
341 query = query.join(model.Role)
343 return query
345 def configure_grid(self, grid): # pylint: disable=empty-docstring
346 """ """
347 g = grid
348 super().configure_grid(g)
349 model = self.app.model
351 # role
352 g.set_sorter("role", model.Role.name)
353 g.set_filter("role", model.Role.name, label="Role Name")
354 g.set_link("role")
356 # permission
357 g.set_link("permission")
359 def configure_form(self, form): # pylint: disable=empty-docstring
360 """ """
361 f = form
362 super().configure_form(f)
364 # role
365 f.set_node("role", RoleRef(self.request))
368def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
369 base = globals()
371 RoleView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
372 "RoleView", base["RoleView"]
373 )
374 RoleView.defaults(config)
376 PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
377 "PermissionView", base["PermissionView"]
378 )
379 PermissionView.defaults(config)
382def includeme(config): # pylint: disable=missing-function-docstring
383 defaults(config)