Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/roles.py: 100%

129 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2024-12-28 21:19 -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""" 

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 UserRefs, Permissions, RoleRef 

32 

33 

34class RoleView(MasterView): 

35 """ 

36 Master view for roles. 

37 

38 Default route prefix is ``roles``. 

39 

40 Notable URLs provided by this class: 

41 

42 * ``/roles/`` 

43 * ``/roles/new`` 

44 * ``/roles/XXX`` 

45 * ``/roles/XXX/edit`` 

46 * ``/roles/XXX/delete`` 

47 """ 

48 model_class = Role 

49 

50 grid_columns = [ 

51 'name', 

52 'notes', 

53 ] 

54 

55 filter_defaults = { 

56 'name': {'active': True}, 

57 } 

58 sort_defaults = 'name' 

59 

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) 

66 

67 def configure_grid(self, g): 

68 """ """ 

69 super().configure_grid(g) 

70 

71 # name 

72 g.set_link('name') 

73 

74 # notes 

75 g.set_renderer('notes', self.grid_render_notes) 

76 

77 def is_editable(self, role): 

78 """ """ 

79 session = self.app.get_session(role) 

80 auth = self.app.get_auth_handler() 

81 

82 # only "root" can edit admin role 

83 if role is auth.get_role_administrator(session): 

84 return self.request.is_root 

85 

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

90 

91 return True 

92 

93 def is_deletable(self, role): 

94 """ """ 

95 session = self.app.get_session(role) 

96 auth = self.app.get_auth_handler() 

97 

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 

105 

106 return True 

107 

108 def configure_form(self, f): 

109 """ """ 

110 super().configure_form(f) 

111 role = f.model_instance 

112 

113 # never show these 

114 f.remove('permission_refs', 

115 'user_refs') 

116 

117 # name 

118 f.set_validator('name', self.unique_name) 

119 

120 # notes 

121 f.set_widget('notes', widgets.NotesWidget()) 

122 

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

129 

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

136 

137 def unique_name(self, node, value): 

138 """ """ 

139 model = self.app.model 

140 session = Session() 

141 

142 query = session.query(model.Role)\ 

143 .filter(model.Role.name == value) 

144 

145 if self.editing: 

146 uuid = self.request.matchdict['uuid'] 

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

148 

149 if query.count(): 

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

151 

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. 

157 

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

162 

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

164 full set of known permissions. 

165 

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. 

171 

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

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

174 

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

206 

207 # get all known permissions from settings cache 

208 permissions = self.request.registry.settings.get('wutta_permissions', {}) 

209 

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

211 if self.viewing: 

212 return permissions 

213 

214 # admin user gets to manage all permissions 

215 if self.request.is_admin: 

216 return permissions 

217 

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 

230 

231 return available 

232 

233 def objectify(self, form): 

234 """ """ 

235 # normal logic first 

236 role = super().objectify(form) 

237 

238 # update permissions for role 

239 self.update_permissions(role, form) 

240 

241 return role 

242 

243 def update_permissions(self, role, form): 

244 """ """ 

245 if 'permissions' not in form.validated: 

246 return 

247 

248 auth = self.app.get_auth_handler() 

249 available = self.wutta_permissions 

250 permissions = form.validated['permissions'] 

251 

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) 

258 

259 @classmethod 

260 def defaults(cls, config): 

261 """ """ 

262 cls._defaults(config) 

263 cls._role_defaults(config) 

264 

265 @classmethod 

266 def _role_defaults(cls, config): 

267 permission_prefix = cls.get_permission_prefix() 

268 model_title_plural = cls.get_model_title_plural() 

269 

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

274 

275 

276class PermissionView(MasterView): 

277 """ 

278 Master view for permissions. 

279 

280 Default route prefix is ``permissions``. 

281 

282 Notable URLs provided by this class: 

283 

284 * ``/permissions/`` 

285 * ``/permissions/XXX`` 

286 * ``/permissions/XXX/delete`` 

287 """ 

288 model_class = Permission 

289 creatable = False 

290 editable = False 

291 

292 grid_columns = [ 

293 'role', 

294 'permission', 

295 ] 

296 

297 sort_defaults = 'role' 

298 

299 form_fields = [ 

300 'role', 

301 'permission', 

302 ] 

303 

304 def get_query(self, **kwargs): 

305 """ """ 

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

307 model = self.app.model 

308 

309 # always join on Role 

310 query = query.join(model.Role) 

311 

312 return query 

313 

314 def configure_grid(self, g): 

315 """ """ 

316 super().configure_grid(g) 

317 model = self.app.model 

318 

319 # role 

320 g.set_sorter('role', model.Role.name) 

321 g.set_filter('role', model.Role.name, label="Role Name") 

322 g.set_link('role') 

323 

324 # permission 

325 g.set_link('permission') 

326 

327 def configure_form(self, f): 

328 """ """ 

329 super().configure_form(f) 

330 

331 # role 

332 f.set_node('role', RoleRef(self.request)) 

333 

334 

335def defaults(config, **kwargs): 

336 base = globals() 

337 

338 RoleView = kwargs.get('RoleView', base['RoleView']) 

339 RoleView.defaults(config) 

340 

341 PermissionView = kwargs.get('PermissionView', base['PermissionView']) 

342 PermissionView.defaults(config) 

343 

344 

345def includeme(config): 

346 defaults(config)