1
0
Fork 0

Compare commits

...

5 commits

Author SHA1 Message Date
Lance Edgar 1995095627 build: just run pytest, avoid tox when making release 2024-07-14 11:08:26 -05:00
Lance Edgar 5d15ef9720 bump: version 0.6.1 → 0.7.0 2024-07-14 11:07:03 -05:00
Lance Edgar b4d6cfb0ed fix: always use 'wutta' prefix for provider entry points
otherwise not all providers will be discoverable, for custom appname

also add `appname` prop for GenericHandler, AppProvider
2024-07-14 10:45:13 -05:00
Lance Edgar f6d0912c03 docs: update project urls to wuttaproject.org 2024-07-14 09:28:58 -05:00
Lance Edgar 639b0de8b1 feat: add basic "auth" data models: user/role/perm
not really tested yet though, other than unit tests
2024-07-13 23:29:09 -05:00
16 changed files with 430 additions and 20 deletions

View file

@ -5,6 +5,16 @@ All notable changes to WuttJamaican will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.7.0 (2024-07-14)
### Feat
- add basic "auth" data models: user/role/perm
### Fix
- always use 'wutta' prefix for provider entry points
## v0.6.1 (2024-07-12)
### Fix

View file

@ -0,0 +1,6 @@
``wuttjamaican.auth``
=====================
.. automodule:: wuttjamaican.auth
:members:

View file

@ -0,0 +1,6 @@
``wuttjamaican.db.model.auth``
==============================
.. automodule:: wuttjamaican.db.model.auth
:members:

View file

@ -8,10 +8,12 @@
:maxdepth: 1
app
auth
conf
db
db.conf
db.model
db.model.auth
db.model.base
db.sess
exc

View file

@ -55,6 +55,12 @@ Glossary
See also the code-friendly :term:`app name`.
auth handler
A :term:`handler` responsible for user authentication and
authorization (login, permissions) and related things.
See also :class:`~wuttjamaican.auth.AuthHandler`.
command
A top-level command line interface for the app. Note that
top-level commands don't usually "do" anything per se, and are
@ -71,6 +77,9 @@ Glossary
happens is, a config object is created and then extended by each
of the registered config extensions.
The intention is that all config extensions will have been
applied before the :term:`app handler` is created.
config file
A file which contains :term:`config settings<config setting>`.
See also :doc:`narr/config/files`.

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttJamaican"
version = "0.6.1"
version = "0.7.0"
description = "Base package for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@ -38,9 +38,9 @@ tests = ["pytest-cov", "tox"]
[project.urls]
Homepage = "https://rattailproject.org/"
Repository = "https://kallithea.rattailproject.org/rattail-project/wuttjamaican"
Changelog = "https://kallithea.rattailproject.org/rattail-project/wuttjamaican/files/master/CHANGELOG.md"
Homepage = "https://wuttaproject.org/"
Repository = "https://forgejo.wuttaproject.org/wutta/wuttjamaican"
Changelog = "https://forgejo.wuttaproject.org/wutta/wuttjamaican/src/branch/master/CHANGELOG.md"
[tool.commitizen]

View file

@ -105,9 +105,6 @@ class AppHandler:
self.providers = self.get_all_providers()
return self.providers
# if 'providers' not in self.__dict__:
# self.__dict__['providers'] = self.get_all_providers()
for provider in self.providers.values():
if hasattr(provider, name):
return getattr(provider, name)
@ -121,10 +118,22 @@ class AppHandler:
Note that you do not need to call this directly; instead just
use :attr:`providers`.
The discovery logic is based on :term:`entry points<entry
point>` using the ``wutta.app.providers`` group. For instance
here is a sample entry point used by WuttaWeb (in its
``pyproject.toml``):
.. code-block:: toml
[project.entry-points."wutta.app.providers"]
wuttaweb = "wuttaweb.app:WebAppProvider"
:returns: Dictionary keyed by entry point name; values are
:class:`AppProvider` *instances*.
:class:`AppProvider` instances.
"""
providers = load_entry_points(f'{self.appname}.providers')
# nb. must use 'wutta' and not self.appname prefix here, or
# else we can't find all providers with custom appname
providers = load_entry_points('wutta.app.providers')
for key in list(providers):
providers[key] = providers[key](self.config)
return providers
@ -278,8 +287,7 @@ class AppProvider:
These can add arbitrary extra functionality to the main :term:`app
handler`. See also :doc:`/narr/providers/app`.
:param config: Config object for the app. This should be an
instance of :class:`~wuttjamaican.conf.WuttaConfig`.
:param config: The app :term:`config object`.
Instances have the following attributes:
@ -301,7 +309,16 @@ class AppProvider:
config = config.config
self.config = config
self.app = config.get_app()
self.app = self.config.get_app()
@property
def appname(self):
"""
The :term:`app name` for the current app.
See also :attr:`AppHandler.appname`.
"""
return self.app.appname
class GenericHandler:
@ -318,3 +335,12 @@ class GenericHandler:
def __init__(self, config, **kwargs):
self.config = config
self.app = self.config.get_app()
@property
def appname(self):
"""
The :term:`app name` for the current app.
See also :attr:`AppHandler.appname`.
"""
return self.app.appname

44
src/wuttjamaican/auth.py Normal file
View file

@ -0,0 +1,44 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-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 Handler
This defines the default :term:`auth handler`.
"""
from wuttjamaican.app import GenericHandler
class AuthHandler(GenericHandler):
"""
Base class and default implementation for the :term:`auth
handler`.
This is responsible for "authentication and authorization" - for
instance:
* create new users, roles
* grant/revoke role permissions
* determine which permissions a user has
* identify user from login credentials
"""

View file

@ -21,14 +21,21 @@
#
################################################################################
"""
WuttJamaican - database model
Data Models
For convenience, from this ``wuttjamaican.db.model`` namespace you can
access the following:
This is the default :term:`app model` module.
The ``wuttjamaican.db.model`` namespace contains the following:
* :func:`~wuttjamaican.db.model.base.uuid_column()`
* :func:`~wuttjamaican.db.model.base.uuid_fk_column()`
* :class:`~wuttjamaican.db.model.base.Base`
* :class:`~wuttjamaican.db.model.base.Setting`
* :func:`~wuttjamaican.db.model.base.uuid_column()`
* :class:`~wuttjamaican.db.model.auth.Role`
* :class:`~wuttjamaican.db.model.auth.Permission`
* :class:`~wuttjamaican.db.model.auth.User`
* :class:`~wuttjamaican.db.model.auth.UserRole`
"""
from .base import Base, uuid_column, Setting
from .base import Base, Setting, uuid_column, uuid_fk_column
from .auth import Role, Permission, User, UserRole

View file

@ -0,0 +1,229 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-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 Models
The :term:`auth handler` is primarily responsible for managing the
data for these models.
Basic design/structure is as follows:
* :class:`User` may be assigned to multiple roles
* :class:`Role` may contain multiple users (cf. :class:`UserRole`)
* :class:`Role` may be granted multiple permissions
* :class:`Permission` is a permission granted to a role
* roles are not nested/grouped; each is independent
* a few roles are built-in, e.g. Administrators
So a user's permissions are "inherited" from the role(s) to which they
belong.
"""
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy
from .base import Base, uuid_column, uuid_fk_column
class Role(Base):
"""
Represents an authentication role within the system; used for
permission management.
.. attribute:: permissions
List of keys (string names) for permissions granted to this
role.
See also :attr:`permission_refs`.
.. attribute:: users
List of :class:`User` instances belonging to this role.
See also :attr:`user_refs`.
"""
__tablename__ = 'role'
__table_args__ = (
sa.UniqueConstraint('name',
# TODO
# name='role_uq_name',
),
)
uuid = uuid_column()
name = sa.Column(sa.String(length=100), nullable=False, doc="""
Name for the role. Each role must have a name, which must be
unique.
""")
notes = sa.Column(sa.Text(), nullable=True, doc="""
Arbitrary notes for the role.
""")
permission_refs = orm.relationship(
'Permission',
back_populates='role',
doc="""
List of :class:`Permission` references for the role.
See also :attr:`permissions`.
""")
permissions = association_proxy(
'permission_refs', 'permission',
creator=lambda p: Permission(permission=p),
# TODO
# getset_factory=getset_factory,
)
user_refs = orm.relationship(
'UserRole',
# TODO
# cascade='all, delete-orphan',
# cascade_backrefs=False,
back_populates='role',
doc="""
List of :class:`UserRole` instances belonging to the role.
See also :attr:`users`.
""")
users = association_proxy(
'user_refs', 'user',
creator=lambda u: UserRole(user=u),
# TODO
# getset_factory=getset_factory,
)
def __str__(self):
return self.name or ""
class Permission(Base):
"""
Represents a permission granted to a role.
"""
__tablename__ = 'permission'
__table_args__ = (
sa.ForeignKeyConstraint(['role_uuid'], ['role.uuid'],
# TODO
# name='permission_fk_role',
),
)
role_uuid = uuid_fk_column(primary_key=True, nullable=False)
role = orm.relationship(
Role,
back_populates='permission_refs',
doc="""
Reference to the :class:`Role` for which the permission is
granted.
""")
permission = sa.Column(sa.String(length=254), primary_key=True, doc="""
Key (name) of the permission which is granted.
""")
def __str__(self):
return self.permission or ""
class User(Base):
"""
Represents a user of the system.
This may or may not correspond to a real person, i.e. some users
may exist solely for automated tasks.
"""
__tablename__ = 'user'
__table_args__ = (
sa.UniqueConstraint('username',
# TODO
# name='user_uq_username',
),
)
uuid = uuid_column()
username = sa.Column(sa.String(length=25), nullable=False, doc="""
Account username. This is required and must be unique.
""")
password = sa.Column(sa.String(length=60), nullable=True, doc="""
Hashed password for login. (The raw password is not stored.)
""")
active = sa.Column(sa.Boolean(), nullable=False, default=True, doc="""
Flag indicating whether the user account is "active" - it is
``True`` by default.
The default auth logic will prevent login for "inactive" user accounts.
""")
role_refs = orm.relationship(
'UserRole',
back_populates='user',
doc="""
List of :class:`UserRole` records.
""")
def __str__(self):
return self.username or ""
class UserRole(Base):
"""
Represents the association between a user and a role.
"""
__tablename__ = 'user_x_role'
__table_args__ = (
sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'],
# TODO
# name='user_x_role_fk_user',
),
sa.ForeignKeyConstraint(['role_uuid'], ['role.uuid'],
# TODO
# name='user_x_role_fk_role',
),
)
uuid = uuid_column()
user_uuid = uuid_fk_column(nullable=False)
user = orm.relationship(
User,
back_populates='role_refs',
doc="""
Reference to the :class:`User` involved.
""")
role_uuid = uuid_fk_column(nullable=False)
role = orm.relationship(
Role,
back_populates='user_refs',
doc="""
Reference to the :class:`Role` involved.
""")

View file

@ -21,7 +21,7 @@
#
################################################################################
"""
WuttJamaican - base models
Base Models
.. class:: Base
@ -47,6 +47,13 @@ def uuid_column(*args, **kwargs):
return sa.Column(sa.String(length=32), *args, **kwargs)
def uuid_fk_column(*args, **kwargs):
"""
Returns a UUID column for use as a foreign key to another table.
"""
return sa.Column(sa.String(length=32), *args, **kwargs)
class Setting(Base):
"""
Represents a :term:`config setting`.

View file

@ -30,7 +30,7 @@ def release(c, skip_tests=False):
Release a new version of WuttJamaican
"""
if not skip_tests:
c.run('tox')
c.run('pytest')
# rebuild local tar.gz file for distribution
if os.path.exists('WuttJamaican.egg-info'):

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
try:
import sqlalchemy as sa
from wuttjamaican.db.model import auth as model
except ImportError:
pass
else:
class TestRole(TestCase):
def test_basic(self):
role = model.Role()
self.assertEqual(str(role), "")
role.name = "Managers"
self.assertEqual(str(role), "Managers")
class TestPermission(TestCase):
def test_basic(self):
perm = model.Permission()
self.assertEqual(str(perm), "")
perm.permission = 'users.create'
self.assertEqual(str(perm), "users.create")
class TestUser(TestCase):
def test_basic(self):
user = model.User()
self.assertEqual(str(user), "")
user.username = 'barney'
self.assertEqual(str(user), "barney")

View file

@ -14,7 +14,16 @@ else:
def test_basic(self):
column = model.uuid_column()
self.assertIsInstance(column, sa.Column)
self.assertIsInstance(column.type, sa.String)
self.assertEqual(column.type.length, 32)
class TestUUIDFKColumn(TestCase):
def test_basic(self):
column = model.uuid_column()
self.assertIsInstance(column, sa.Column)
self.assertIsInstance(column.type, sa.String)
self.assertEqual(column.type.length, 32)
class TestSetting(TestCase):

View file

@ -134,6 +134,7 @@ class TestAppProvider(TestCase):
provider = app.AppProvider(self.config)
self.assertIs(provider.config, self.config)
self.assertIs(provider.app, self.app)
self.assertEqual(provider.appname, 'wuttatest')
# but can pass app handler instead
with warnings.catch_warnings():
@ -155,7 +156,7 @@ class TestAppProvider(TestCase):
# sanity check, we get *instances* back from this
providers = self.app.get_all_providers()
load_entry_points.assert_called_once_with('wuttatest.providers')
load_entry_points.assert_called_once_with('wutta.app.providers')
self.assertEqual(len(providers), 1)
self.assertIn('fake', providers)
self.assertIsInstance(providers['fake'], FakeProvider)
@ -212,3 +213,4 @@ class TestGenericHandler(TestCase):
handler = app.GenericHandler(self.config)
self.assertIs(handler.config, self.config)
self.assertIs(handler.app, self.app)
self.assertEqual(handler.appname, 'wuttatest')

17
tests/test_auth.py Normal file
View file

@ -0,0 +1,17 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from wuttjamaican import auth as mod
from wuttjamaican.conf import WuttaConfig
class TestAuthHandler(TestCase):
def setUp(self):
self.config = WuttaConfig()
self.app = self.config.get_app()
def test_basic(self):
handler = mod.AuthHandler(self.config)
self.assertIs(handler.app, self.app)