# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Auth Utility Logic
"""
import re
from pyramid.authentication import SessionAuthenticationHelper
from pyramid.request import RequestLocalCache
from pyramid.security import remember, forget
from wuttaweb.db import Session
[docs]
def login_user(request, user):
"""
Perform the steps necessary to "login" the given user. This
returns a ``headers`` dict which you should pass to the final
redirect, like so::
from pyramid.httpexceptions import HTTPFound
headers = login_user(request, user)
return HTTPFound(location='/', headers=headers)
.. warning::
This logic does not "authenticate" the user! It assumes caller
has already authenticated the user and they are safe to login.
See also :func:`logout_user()`.
"""
headers = remember(request, user.uuid)
return headers
[docs]
def logout_user(request):
"""
Perform the logout action for the given request. This returns a
``headers`` dict which you should pass to the final redirect, like
so::
from pyramid.httpexceptions import HTTPFound
headers = logout_user(request)
return HTTPFound(location='/', headers=headers)
See also :func:`login_user()`.
"""
request.session.delete()
request.session.invalidate()
headers = forget(request)
return headers
[docs]
class WuttaSecurityPolicy:
"""
Pyramid :term:`security policy` for WuttaWeb.
For more on the Pyramid details, see :doc:`pyramid:narr/security`.
But the idea here is that you should be able to just use this,
without thinking too hard::
from pyramid.config import Configurator
from wuttaweb.auth import WuttaSecurityPolicy
pyramid_config = Configurator()
pyramid_config.set_security_policy(WuttaSecurityPolicy())
This security policy will then do the following:
* use the request "web session" for auth storage (e.g. current
``user.uuid``)
* check permissions as needed, by calling
:meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.has_permission()`
for current user
:param db_session: Optional :term:`db session` to use, instead of
:class:`wuttaweb.db.sess.Session`. Probably only useful for
tests.
"""
def __init__(self, db_session=None):
self.session_helper = SessionAuthenticationHelper()
self.identity_cache = RequestLocalCache(self.load_identity)
self.db_session = db_session or Session()
def load_identity(self, request):
config = request.registry.settings['wutta_config']
app = config.get_app()
model = app.model
# fetch user uuid from current session
uuid = self.session_helper.authenticated_userid(request)
if not uuid:
return
# fetch user object from db
user = self.db_session.get(model.User, uuid)
if not user:
return
return user
def identity(self, request):
return self.identity_cache.get_or_create(request)
def authenticated_userid(self, request):
user = self.identity(request)
if user is not None:
return user.uuid
def remember(self, request, userid, **kw):
return self.session_helper.remember(request, userid, **kw)
def forget(self, request, **kw):
return self.session_helper.forget(request, **kw)
def permits(self, request, context, permission):
# nb. root user can do anything
if getattr(request, 'is_root', False):
return True
config = request.registry.settings['wutta_config']
app = config.get_app()
auth = app.get_auth_handler()
user = self.identity(request)
return auth.has_permission(self.db_session, user, permission)
[docs]
def add_permission_group(pyramid_config, groupkey, label=None, overwrite=True):
"""
Pyramid directive to add a "permission group" to the app's
awareness.
The app must be made aware of all permissions, so they are exposed
when editing a
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
for discovering permissions is in
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
This is usually called from within a master view's
:meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
the permission group which applies to the view model.
A simple example of usage::
pyramid_config.add_permission_group('widgets', label="Widgets")
:param groupkey: Unique key for the permission group. In the
context of a master view, this will be the same as
:attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
:param label: Optional label for the permission group. If not
specified, it is derived from ``groupkey``.
:param overwrite: If the permission group was already established,
this flag controls whether the group's label should be
overwritten (with ``label``).
See also :func:`add_permission()`.
"""
config = pyramid_config.get_settings()['wutta_config']
app = config.get_app()
def action():
perms = pyramid_config.get_settings().get('wutta_permissions', {})
if overwrite or groupkey not in perms:
group = perms.setdefault(groupkey, {'key': groupkey})
group['label'] = label or app.make_title(groupkey)
pyramid_config.add_settings({'wutta_permissions': perms})
pyramid_config.action(None, action)
[docs]
def add_permission(pyramid_config, groupkey, key, label=None):
"""
Pyramid directive to add a single "permission" to the app's
awareness.
The app must be made aware of all permissions, so they are exposed
when editing a
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
for discovering permissions is in
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
This is usually called from within a master view's
:meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
"known" permissions based on master view feature flags
(:attr:`~wuttaweb.views.master.MasterView.viewable`,
:attr:`~wuttaweb.views.master.MasterView.editable`, etc.).
A simple example of usage::
pyramid_config.add_permission('widgets', 'widgets.polish',
label="Polish all the widgets")
:param groupkey: Unique key for the permission group. In the
context of a master view, this will be the same as
:attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
:param key: Unique key for the permission. This should be the
"complete" permission name which includes the permission
prefix.
:param label: Optional label for the permission. If not
specified, it is derived from ``key``.
See also :func:`add_permission_group()`.
"""
def action():
config = pyramid_config.get_settings()['wutta_config']
app = config.get_app()
perms = pyramid_config.get_settings().get('wutta_permissions', {})
group = perms.setdefault(groupkey, {'key': groupkey})
group.setdefault('label', app.make_title(groupkey))
perm = group.setdefault('perms', {}).setdefault(key, {'key': key})
perm['label'] = label or app.make_title(key)
pyramid_config.add_settings({'wutta_permissions': perms})
pyramid_config.action(None, action)