Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/roles.py: 100%
105 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-24 10:35 -0600
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-24 10:35 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024 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
28from wuttaweb.views import MasterView
29from wuttaweb.db import Session
30from wuttaweb.forms import widgets
31from wuttaweb.forms.schema import UserRefs, Permissions
34class RoleView(MasterView):
35 """
36 Master view for roles.
38 Default route prefix is ``roles``.
40 Notable URLs provided by this class:
42 * ``/roles/``
43 * ``/roles/new``
44 * ``/roles/XXX``
45 * ``/roles/XXX/edit``
46 * ``/roles/XXX/delete``
47 """
48 model_class = Role
50 grid_columns = [
51 'name',
52 'notes',
53 ]
55 filter_defaults = {
56 'name': {'active': True},
57 }
58 sort_defaults = 'name'
60 # TODO: master should handle this, possibly via configure_form()
61 def get_query(self, session=None):
62 """ """
63 model = self.app.model
64 query = super().get_query(session=session)
65 return query.order_by(model.Role.name)
67 def configure_grid(self, g):
68 """ """
69 super().configure_grid(g)
71 # name
72 g.set_link('name')
74 # notes
75 g.set_renderer('notes', self.grid_render_notes)
77 def is_editable(self, role):
78 """ """
79 session = self.app.get_session(role)
80 auth = self.app.get_auth_handler()
82 # only "root" can edit admin role
83 if role is auth.get_role_administrator(session):
84 return self.request.is_root
86 # other built-in roles require special perm
87 if role in (auth.get_role_authenticated(session),
88 auth.get_role_anonymous(session)):
89 return self.has_perm('edit_builtin')
91 return True
93 def is_deletable(self, role):
94 """ """
95 session = self.app.get_session(role)
96 auth = self.app.get_auth_handler()
98 # prevent delete for built-in roles
99 if role is auth.get_role_authenticated(session):
100 return False
101 if role is auth.get_role_anonymous(session):
102 return False
103 if role is auth.get_role_administrator(session):
104 return False
106 return True
108 def configure_form(self, f):
109 """ """
110 super().configure_form(f)
111 role = f.model_instance
113 # never show these
114 f.remove('permission_refs',
115 'user_refs')
117 # name
118 f.set_validator('name', self.unique_name)
120 # notes
121 f.set_widget('notes', widgets.NotesWidget())
123 # users
124 if not (self.creating or self.editing):
125 f.append('users')
126 f.set_readonly('users')
127 f.set_node('users', UserRefs(self.request))
128 f.set_default('users', [u.uuid for u in role.users])
130 # permissions
131 f.append('permissions')
132 self.wutta_permissions = self.get_available_permissions()
133 f.set_node('permissions', Permissions(self.request, permissions=self.wutta_permissions))
134 if not self.creating:
135 f.set_default('permissions', list(role.permissions))
137 def unique_name(self, node, value):
138 """ """
139 model = self.app.model
140 session = Session()
142 query = session.query(model.Role)\
143 .filter(model.Role.name == value)
145 if self.editing:
146 uuid = self.request.matchdict['uuid']
147 query = query.filter(model.Role.uuid != uuid)
149 if query.count():
150 node.raise_invalid("Name must be unique")
152 def get_available_permissions(self):
153 """
154 Returns all "available" permissions. This is used when
155 viewing or editing a role; the result is passed into the
156 :class:`~wuttaweb.forms.schema.Permissions` field schema.
158 The app itself must be made aware of each permission, in order
159 for them to found by this method. This is done via
160 :func:`~wuttaweb.auth.add_permission_group()` and
161 :func:`~wuttaweb.auth.add_permission()`.
163 When in "view" (readonly) mode, this method will return the
164 full set of known permissions.
166 However in "edit" mode, it will prune the set to remove any
167 permissions which the current user does not also have. The
168 idea here is to allow "many" users to manage roles, but ensure
169 they cannot "break out" of their own role by assigning extra
170 permissions to it.
172 The permissions returned will also be grouped, and each single
173 permission is also represented as a simple dict, e.g.::
175 {
176 'books': {
177 'key': 'books',
178 'label': "Books",
179 'perms': {
180 'books.list': {
181 'key': 'books.list',
182 'label': "Browse / search Books",
183 },
184 'books.view': {
185 'key': 'books.view',
186 'label': "View Book",
187 },
188 },
189 },
190 'widgets': {
191 'key': 'widgets',
192 'label': "Widgets",
193 'perms': {
194 'widgets.list': {
195 'key': 'widgets.list',
196 'label': "Browse / search Widgets",
197 },
198 'widgets.view': {
199 'key': 'widgets.view',
200 'label': "View Widget",
201 },
202 },
203 },
204 }
205 """
207 # get all known permissions from settings cache
208 permissions = self.request.registry.settings.get('wutta_permissions', {})
210 # when viewing, we allow all permissions to be exposed for all users
211 if self.viewing:
212 return permissions
214 # admin user gets to manage all permissions
215 if self.request.is_admin:
216 return permissions
218 # non-admin user can only see permissions they're granted
219 available = {}
220 for gkey, group in permissions.items():
221 for pkey, perm in group['perms'].items():
222 if self.request.has_perm(pkey):
223 if gkey not in available:
224 available[gkey] = {
225 'key': gkey,
226 'label': group['label'],
227 'perms': {},
228 }
229 available[gkey]['perms'][pkey] = perm
231 return available
233 def objectify(self, form):
234 """ """
235 # normal logic first
236 role = super().objectify(form)
238 # update permissions for role
239 self.update_permissions(role, form)
241 return role
243 def update_permissions(self, role, form):
244 """ """
245 if 'permissions' not in form.validated:
246 return
248 auth = self.app.get_auth_handler()
249 available = self.wutta_permissions
250 permissions = form.validated['permissions']
252 for gkey, group in available.items():
253 for pkey, perm in group['perms'].items():
254 if pkey in permissions:
255 auth.grant_permission(role, pkey)
256 else:
257 auth.revoke_permission(role, pkey)
259 @classmethod
260 def defaults(cls, config):
261 """ """
262 cls._defaults(config)
263 cls._role_defaults(config)
265 @classmethod
266 def _role_defaults(cls, config):
267 permission_prefix = cls.get_permission_prefix()
268 model_title_plural = cls.get_model_title_plural()
270 # perm to edit built-in roles
271 config.add_wutta_permission(permission_prefix,
272 f'{permission_prefix}.edit_builtin',
273 f"Edit the Built-in {model_title_plural}")
276def defaults(config, **kwargs):
277 base = globals()
279 RoleView = kwargs.get('RoleView', base['RoleView'])
280 RoleView.defaults(config)
283def includeme(config):
284 defaults(config)