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

105 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-23 14:51 -0500

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 

28from wuttaweb.views import MasterView 

29from wuttaweb.db import Session 

30from wuttaweb.forms import widgets 

31from wuttaweb.forms.schema import UserRefs, Permissions 

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 

276def defaults(config, **kwargs): 

277 base = globals() 

278 

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

280 RoleView.defaults(config) 

281 

282 

283def includeme(config): 

284 defaults(config)