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

68 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-27 21:18 -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""" 

24Auth Utility Logic 

25""" 

26 

27import re 

28 

29from pyramid.authentication import SessionAuthenticationHelper 

30from pyramid.request import RequestLocalCache 

31from pyramid.security import remember, forget 

32 

33from wuttaweb.db import Session 

34 

35 

36def login_user(request, user): 

37 """ 

38 Perform the steps necessary to "login" the given user. This 

39 returns a ``headers`` dict which you should pass to the final 

40 redirect, like so:: 

41 

42 from pyramid.httpexceptions import HTTPFound 

43 

44 headers = login_user(request, user) 

45 return HTTPFound(location='/', headers=headers) 

46 

47 .. warning:: 

48 

49 This logic does not "authenticate" the user! It assumes caller 

50 has already authenticated the user and they are safe to login. 

51 

52 See also :func:`logout_user()`. 

53 """ 

54 headers = remember(request, user.uuid) 

55 return headers 

56 

57 

58def logout_user(request): 

59 """ 

60 Perform the logout action for the given request. This returns a 

61 ``headers`` dict which you should pass to the final redirect, like 

62 so:: 

63 

64 from pyramid.httpexceptions import HTTPFound 

65 

66 headers = logout_user(request) 

67 return HTTPFound(location='/', headers=headers) 

68 

69 See also :func:`login_user()`. 

70 """ 

71 request.session.delete() 

72 request.session.invalidate() 

73 headers = forget(request) 

74 return headers 

75 

76 

77class WuttaSecurityPolicy: 

78 """ 

79 Pyramid :term:`security policy` for WuttaWeb. 

80 

81 For more on the Pyramid details, see :doc:`pyramid:narr/security`. 

82 

83 But the idea here is that you should be able to just use this, 

84 without thinking too hard:: 

85 

86 from pyramid.config import Configurator 

87 from wuttaweb.auth import WuttaSecurityPolicy 

88 

89 pyramid_config = Configurator() 

90 pyramid_config.set_security_policy(WuttaSecurityPolicy()) 

91 

92 This security policy will then do the following: 

93 

94 * use the request "web session" for auth storage (e.g. current 

95 ``user.uuid``) 

96 * check permissions as needed, by calling 

97 :meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.has_permission()` 

98 for current user 

99 

100 :param db_session: Optional :term:`db session` to use, instead of 

101 :class:`wuttaweb.db.sess.Session`. Probably only useful for 

102 tests. 

103 """ 

104 

105 def __init__(self, db_session=None): 

106 self.session_helper = SessionAuthenticationHelper() 

107 self.identity_cache = RequestLocalCache(self.load_identity) 

108 self.db_session = db_session or Session() 

109 

110 def load_identity(self, request): 

111 config = request.registry.settings['wutta_config'] 

112 app = config.get_app() 

113 model = app.model 

114 

115 # fetch user uuid from current session 

116 uuid = self.session_helper.authenticated_userid(request) 

117 if not uuid: 

118 return 

119 

120 # fetch user object from db 

121 user = self.db_session.get(model.User, uuid) 

122 if not user: 

123 return 

124 

125 return user 

126 

127 def identity(self, request): 

128 return self.identity_cache.get_or_create(request) 

129 

130 def authenticated_userid(self, request): 

131 user = self.identity(request) 

132 if user is not None: 

133 return user.uuid 

134 

135 def remember(self, request, userid, **kw): 

136 return self.session_helper.remember(request, userid, **kw) 

137 

138 def forget(self, request, **kw): 

139 return self.session_helper.forget(request, **kw) 

140 

141 def permits(self, request, context, permission): 

142 

143 # nb. root user can do anything 

144 if getattr(request, 'is_root', False): 

145 return True 

146 

147 config = request.registry.settings['wutta_config'] 

148 app = config.get_app() 

149 auth = app.get_auth_handler() 

150 user = self.identity(request) 

151 return auth.has_permission(self.db_session, user, permission) 

152 

153 

154def add_permission_group(pyramid_config, groupkey, label=None, overwrite=True): 

155 """ 

156 Pyramid directive to add a "permission group" to the app's 

157 awareness. 

158 

159 The app must be made aware of all permissions, so they are exposed 

160 when editing a 

161 :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic 

162 for discovering permissions is in 

163 :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`. 

164 

165 This is usually called from within a master view's 

166 :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish 

167 the permission group which applies to the view model. 

168 

169 A simple example of usage:: 

170 

171 pyramid_config.add_permission_group('widgets', label="Widgets") 

172 

173 :param groupkey: Unique key for the permission group. In the 

174 context of a master view, this will be the same as 

175 :attr:`~wuttaweb.views.master.MasterView.permission_prefix`. 

176 

177 :param label: Optional label for the permission group. If not 

178 specified, it is derived from ``groupkey``. 

179 

180 :param overwrite: If the permission group was already established, 

181 this flag controls whether the group's label should be 

182 overwritten (with ``label``). 

183 

184 See also :func:`add_permission()`. 

185 """ 

186 config = pyramid_config.get_settings()['wutta_config'] 

187 app = config.get_app() 

188 def action(): 

189 perms = pyramid_config.get_settings().get('wutta_permissions', {}) 

190 if overwrite or groupkey not in perms: 

191 group = perms.setdefault(groupkey, {'key': groupkey}) 

192 group['label'] = label or app.make_title(groupkey) 

193 pyramid_config.add_settings({'wutta_permissions': perms}) 

194 pyramid_config.action(None, action) 

195 

196 

197def add_permission(pyramid_config, groupkey, key, label=None): 

198 """ 

199 Pyramid directive to add a single "permission" to the app's 

200 awareness. 

201 

202 The app must be made aware of all permissions, so they are exposed 

203 when editing a 

204 :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic 

205 for discovering permissions is in 

206 :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`. 

207 

208 This is usually called from within a master view's 

209 :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish 

210 "known" permissions based on master view feature flags 

211 (:attr:`~wuttaweb.views.master.MasterView.viewable`, 

212 :attr:`~wuttaweb.views.master.MasterView.editable`, etc.). 

213 

214 A simple example of usage:: 

215 

216 pyramid_config.add_permission('widgets', 'widgets.polish', 

217 label="Polish all the widgets") 

218 

219 :param groupkey: Unique key for the permission group. In the 

220 context of a master view, this will be the same as 

221 :attr:`~wuttaweb.views.master.MasterView.permission_prefix`. 

222 

223 :param key: Unique key for the permission. This should be the 

224 "complete" permission name which includes the permission 

225 prefix. 

226 

227 :param label: Optional label for the permission. If not 

228 specified, it is derived from ``key``. 

229 

230 See also :func:`add_permission_group()`. 

231 """ 

232 def action(): 

233 config = pyramid_config.get_settings()['wutta_config'] 

234 app = config.get_app() 

235 perms = pyramid_config.get_settings().get('wutta_permissions', {}) 

236 group = perms.setdefault(groupkey, {'key': groupkey}) 

237 group.setdefault('label', app.make_title(groupkey)) 

238 perm = group.setdefault('perms', {}).setdefault(key, {'key': key}) 

239 perm['label'] = label or app.make_title(key) 

240 pyramid_config.add_settings({'wutta_permissions': perms}) 

241 pyramid_config.action(None, action)