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

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""" 

26 

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 

33 

34 

35class RoleView(MasterView): # pylint: disable=abstract-method 

36 """ 

37 Master view for roles. 

38 

39 Default route prefix is ``roles``. 

40 

41 Notable URLs provided by this class: 

42 

43 * ``/roles/`` 

44 * ``/roles/new`` 

45 * ``/roles/XXX`` 

46 * ``/roles/XXX/edit`` 

47 * ``/roles/XXX/delete`` 

48 """ 

49 

50 model_class = Role 

51 

52 grid_columns = [ 

53 "name", 

54 "notes", 

55 ] 

56 

57 filter_defaults = { 

58 "name": {"active": True}, 

59 } 

60 sort_defaults = "name" 

61 

62 wutta_permissions = None 

63 

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) 

70 

71 def configure_grid(self, grid): # pylint: disable=empty-docstring 

72 """ """ 

73 g = grid 

74 super().configure_grid(g) 

75 

76 # name 

77 g.set_link("name") 

78 

79 # notes 

80 g.set_renderer("notes", self.grid_render_notes) 

81 

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() 

87 

88 # only "root" can edit admin role 

89 if role is auth.get_role_administrator(session): 

90 return self.request.is_root 

91 

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") 

98 

99 return True 

100 

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() 

106 

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 

114 

115 return True 

116 

117 def configure_form(self, form): # pylint: disable=empty-docstring 

118 """ """ 

119 f = form 

120 super().configure_form(f) 

121 role = f.model_instance 

122 

123 # never show these 

124 f.remove("permission_refs", "user_refs") 

125 

126 # name 

127 f.set_validator("name", self.unique_name) 

128 

129 # notes 

130 f.set_widget("notes", widgets.NotesWidget()) 

131 

132 # users 

133 if not (self.creating or self.editing): 

134 f.append("users") 

135 f.set_grid("users", self.make_users_grid(role)) 

136 

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)) 

145 

146 def make_users_grid(self, role): 

147 """ 

148 Make and return the grid for the Users field. 

149 

150 This grid is shown for the Users field when viewing a Role. 

151 

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 ) 

165 

166 def unique_name(self, node, value): # pylint: disable=empty-docstring 

167 """ """ 

168 model = self.app.model 

169 session = Session() 

170 

171 query = session.query(model.Role).filter(model.Role.name == value) 

172 

173 if self.editing: 

174 uuid = self.request.matchdict["uuid"] 

175 query = query.filter(model.Role.uuid != uuid) 

176 

177 if query.count(): 

178 node.raise_invalid("Name must be unique") 

179 

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. 

185 

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()`. 

190 

191 When in "view" (readonly) mode, this method will return the 

192 full set of known permissions. 

193 

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. 

199 

200 The permissions returned will also be grouped, and each single 

201 permission is also represented as a simple dict, e.g.:: 

202 

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 """ 

234 

235 # get all known permissions from settings cache 

236 permissions = self.request.registry.settings.get("wutta_permissions", {}) 

237 

238 # when viewing, we allow all permissions to be exposed for all users 

239 if self.viewing: 

240 return permissions 

241 

242 # admin user gets to manage all permissions 

243 if self.request.is_admin: 

244 return permissions 

245 

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 

258 

259 return available 

260 

261 def objectify(self, form): # pylint: disable=empty-docstring 

262 """ """ 

263 # normal logic first 

264 role = super().objectify(form) 

265 

266 # update permissions for role 

267 self.update_permissions(role, form) 

268 

269 return role 

270 

271 def update_permissions(self, role, form): # pylint: disable=empty-docstring 

272 """ """ 

273 if "permissions" not in form.validated: 

274 return 

275 

276 auth = self.app.get_auth_handler() 

277 available = self.wutta_permissions 

278 permissions = form.validated["permissions"] 

279 

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) 

286 

287 @classmethod 

288 def defaults(cls, config): # pylint: disable=empty-docstring 

289 """ """ 

290 cls._defaults(config) 

291 cls._role_defaults(config) 

292 

293 @classmethod 

294 def _role_defaults(cls, config): 

295 permission_prefix = cls.get_permission_prefix() 

296 model_title_plural = cls.get_model_title_plural() 

297 

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 ) 

304 

305 

306class PermissionView(MasterView): # pylint: disable=abstract-method 

307 """ 

308 Master view for permissions. 

309 

310 Default route prefix is ``permissions``. 

311 

312 Notable URLs provided by this class: 

313 

314 * ``/permissions/`` 

315 * ``/permissions/XXX`` 

316 * ``/permissions/XXX/delete`` 

317 """ 

318 

319 model_class = Permission 

320 creatable = False 

321 editable = False 

322 

323 grid_columns = [ 

324 "role", 

325 "permission", 

326 ] 

327 

328 sort_defaults = "role" 

329 

330 form_fields = [ 

331 "role", 

332 "permission", 

333 ] 

334 

335 def get_query(self, **kwargs): # pylint: disable=empty-docstring,arguments-differ 

336 """ """ 

337 query = super().get_query(**kwargs) 

338 model = self.app.model 

339 

340 # always join on Role 

341 query = query.join(model.Role) 

342 

343 return query 

344 

345 def configure_grid(self, grid): # pylint: disable=empty-docstring 

346 """ """ 

347 g = grid 

348 super().configure_grid(g) 

349 model = self.app.model 

350 

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") 

355 

356 # permission 

357 g.set_link("permission") 

358 

359 def configure_form(self, form): # pylint: disable=empty-docstring 

360 """ """ 

361 f = form 

362 super().configure_form(f) 

363 

364 # role 

365 f.set_node("role", RoleRef(self.request)) 

366 

367 

368def defaults(config, **kwargs): # pylint: disable=missing-function-docstring 

369 base = globals() 

370 

371 RoleView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name 

372 "RoleView", base["RoleView"] 

373 ) 

374 RoleView.defaults(config) 

375 

376 PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name 

377 "PermissionView", base["PermissionView"] 

378 ) 

379 PermissionView.defaults(config) 

380 

381 

382def includeme(config): # pylint: disable=missing-function-docstring 

383 defaults(config)