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
« 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"""
27from pyramid.authentication import SessionAuthenticationHelper
28from pyramid.request import RequestLocalCache
29from pyramid.security import remember, forget
31from wuttaweb.db import Session
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::
40 from pyramid.httpexceptions import HTTPFound
42 headers = login_user(request, user)
43 return HTTPFound(location='/', headers=headers)
45 .. warning::
47 This logic does not "authenticate" the user! It assumes caller
48 has already authenticated the user and they are safe to login.
50 See also :func:`logout_user()`.
51 """
52 headers = remember(request, user.uuid)
53 return headers
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::
62 from pyramid.httpexceptions import HTTPFound
64 headers = logout_user(request)
65 return HTTPFound(location='/', headers=headers)
67 See also :func:`login_user()`.
68 """
69 request.session.delete()
70 request.session.invalidate()
71 headers = forget(request)
72 return headers
75class WuttaSecurityPolicy:
76 """
77 Pyramid :term:`security policy` for WuttaWeb.
79 For more on the Pyramid details, see :doc:`pyramid:narr/security`.
81 But the idea here is that you should be able to just use this,
82 without thinking too hard::
84 from pyramid.config import Configurator
85 from wuttaweb.auth import WuttaSecurityPolicy
87 pyramid_config = Configurator()
88 pyramid_config.set_security_policy(WuttaSecurityPolicy())
90 This security policy will then do the following:
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
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 """
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()
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
114 # fetch user uuid from current session
115 uuid = self.session_helper.authenticated_userid(request)
116 if not uuid:
117 return None
119 # fetch user object from db
120 user = self.db_session.get(model.User, uuid)
121 if not user:
122 return None
124 return user
126 def identity(self, request): # pylint: disable=empty-docstring
127 """ """
128 return self.identity_cache.get_or_create(request)
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
137 def remember(self, request, userid, **kw): # pylint: disable=empty-docstring
138 """ """
139 return self.session_helper.remember(request, userid, **kw)
141 def forget(self, request, **kw): # pylint: disable=empty-docstring
142 """ """
143 return self.session_helper.forget(request, **kw)
145 def permits( # pylint: disable=unused-argument,empty-docstring
146 self, request, context, permission
147 ):
148 """ """
150 # nb. root user can do anything
151 if getattr(request, "is_root", False):
152 return True
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)
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.
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()`.
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.
176 A simple example of usage::
178 pyramid_config.add_permission_group('widgets', label="Widgets")
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`.
184 :param label: Optional label for the permission group. If not
185 specified, it is derived from ``groupkey``.
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``).
191 See also :func:`add_permission()`.
192 """
193 config = pyramid_config.get_settings()["wutta_config"]
194 app = config.get_app()
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})
203 pyramid_config.action(None, action)
206def add_permission(pyramid_config, groupkey, key, label=None):
207 """
208 Pyramid directive to add a single "permission" to the app's
209 awareness.
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()`.
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.).
223 A simple example of usage::
225 pyramid_config.add_permission('widgets', 'widgets.polish',
226 label="Polish all the widgets")
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`.
232 :param key: Unique key for the permission. This should be the
233 "complete" permission name which includes the permission
234 prefix.
236 :param label: Optional label for the permission. If not
237 specified, it is derived from ``key``.
239 See also :func:`add_permission_group()`.
240 """
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})
252 pyramid_config.action(None, action)