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

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

24Auth Utility Logic 

25""" 

26 

27from pyramid.authentication import SessionAuthenticationHelper 

28from pyramid.request import RequestLocalCache 

29from pyramid.security import remember, forget 

30 

31from wuttaweb.db import Session 

32 

33 

34def login_user(request, user): 

35 """ 

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

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

38 redirect, like so:: 

39 

40 from pyramid.httpexceptions import HTTPFound 

41 

42 headers = login_user(request, user) 

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

44 

45 .. warning:: 

46 

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

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

49 

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

51 """ 

52 headers = remember(request, user.uuid) 

53 return headers 

54 

55 

56def logout_user(request): 

57 """ 

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

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

60 so:: 

61 

62 from pyramid.httpexceptions import HTTPFound 

63 

64 headers = logout_user(request) 

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

66 

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

68 """ 

69 request.session.delete() 

70 request.session.invalidate() 

71 headers = forget(request) 

72 return headers 

73 

74 

75class WuttaSecurityPolicy: 

76 """ 

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

78 

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

80 

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

82 without thinking too hard:: 

83 

84 from pyramid.config import Configurator 

85 from wuttaweb.auth import WuttaSecurityPolicy 

86 

87 pyramid_config = Configurator() 

88 pyramid_config.set_security_policy(WuttaSecurityPolicy()) 

89 

90 This security policy will then do the following: 

91 

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

93 ``user.uuid``) 

94 * check permissions as needed, by calling 

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

96 for current user 

97 

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

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

100 tests. 

101 """ 

102 

103 def __init__(self, db_session=None): 

104 self.session_helper = SessionAuthenticationHelper() 

105 self.identity_cache = RequestLocalCache(self.load_identity) 

106 self.db_session = db_session or Session() 

107 

108 def load_identity(self, request): # pylint: disable=empty-docstring 

109 """ """ 

110 config = request.registry.settings["wutta_config"] 

111 app = config.get_app() 

112 model = app.model 

113 

114 # fetch user uuid from current session 

115 uuid = self.session_helper.authenticated_userid(request) 

116 if not uuid: 

117 return None 

118 

119 # fetch user object from db 

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

121 if not user: 

122 return None 

123 

124 return user 

125 

126 def identity(self, request): # pylint: disable=empty-docstring 

127 """ """ 

128 return self.identity_cache.get_or_create(request) 

129 

130 def authenticated_userid(self, request): # pylint: disable=empty-docstring 

131 """ """ 

132 user = self.identity(request) 

133 if user is not None: 

134 return user.uuid 

135 return None 

136 

137 def remember(self, request, userid, **kw): # pylint: disable=empty-docstring 

138 """ """ 

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

140 

141 def forget(self, request, **kw): # pylint: disable=empty-docstring 

142 """ """ 

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

144 

145 def permits( # pylint: disable=unused-argument,empty-docstring 

146 self, request, context, permission 

147 ): 

148 """ """ 

149 

150 # nb. root user can do anything 

151 if getattr(request, "is_root", False): 

152 return True 

153 

154 config = request.registry.settings["wutta_config"] 

155 app = config.get_app() 

156 auth = app.get_auth_handler() 

157 user = self.identity(request) 

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

159 

160 

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

162 """ 

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

164 awareness. 

165 

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

167 when editing a 

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

169 for discovering permissions is in 

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

171 

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

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

174 the permission group which applies to the view model. 

175 

176 A simple example of usage:: 

177 

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

179 

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

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

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

183 

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

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

186 

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

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

189 overwritten (with ``label``). 

190 

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

192 """ 

193 config = pyramid_config.get_settings()["wutta_config"] 

194 app = config.get_app() 

195 

196 def action(): 

197 perms = pyramid_config.get_settings().get("wutta_permissions", {}) 

198 if overwrite or groupkey not in perms: 

199 group = perms.setdefault(groupkey, {"key": groupkey}) 

200 group["label"] = label or app.make_title(groupkey) 

201 pyramid_config.add_settings({"wutta_permissions": perms}) 

202 

203 pyramid_config.action(None, action) 

204 

205 

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

207 """ 

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

209 awareness. 

210 

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

212 when editing a 

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

214 for discovering permissions is in 

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

216 

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

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

219 "known" permissions based on master view feature flags 

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

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

222 

223 A simple example of usage:: 

224 

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

226 label="Polish all the widgets") 

227 

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

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

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

231 

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

233 "complete" permission name which includes the permission 

234 prefix. 

235 

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

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

238 

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

240 """ 

241 

242 def action(): 

243 config = pyramid_config.get_settings()["wutta_config"] 

244 app = config.get_app() 

245 perms = pyramid_config.get_settings().get("wutta_permissions", {}) 

246 group = perms.setdefault(groupkey, {"key": groupkey}) 

247 group.setdefault("label", app.make_title(groupkey)) 

248 perm = group.setdefault("perms", {}).setdefault(key, {"key": key}) 

249 perm["label"] = label or app.make_title(key) 

250 pyramid_config.add_settings({"wutta_permissions": perms}) 

251 

252 pyramid_config.action(None, action)