Compare commits
No commits in common. "19950956272ec1cc6f5d2fa874692a4166adbad0" and "7442047d0e026cce9d49c7701ca41fcb5dbbd4e5" have entirely different histories.
1995095627
...
7442047d0e
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,16 +5,6 @@ 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
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``wuttjamaican.auth``
|
||||
=====================
|
||||
|
||||
.. automodule:: wuttjamaican.auth
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``wuttjamaican.db.model.auth``
|
||||
==============================
|
||||
|
||||
.. automodule:: wuttjamaican.db.model.auth
|
||||
:members:
|
|
@ -8,12 +8,10 @@
|
|||
:maxdepth: 1
|
||||
|
||||
app
|
||||
auth
|
||||
conf
|
||||
db
|
||||
db.conf
|
||||
db.model
|
||||
db.model.auth
|
||||
db.model.base
|
||||
db.sess
|
||||
exc
|
||||
|
|
|
@ -55,12 +55,6 @@ 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
|
||||
|
@ -77,9 +71,6 @@ 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`.
|
||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttJamaican"
|
||||
version = "0.7.0"
|
||||
version = "0.6.1"
|
||||
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://wuttaproject.org/"
|
||||
Repository = "https://forgejo.wuttaproject.org/wutta/wuttjamaican"
|
||||
Changelog = "https://forgejo.wuttaproject.org/wutta/wuttjamaican/src/branch/master/CHANGELOG.md"
|
||||
Homepage = "https://rattailproject.org/"
|
||||
Repository = "https://kallithea.rattailproject.org/rattail-project/wuttjamaican"
|
||||
Changelog = "https://kallithea.rattailproject.org/rattail-project/wuttjamaican/files/master/CHANGELOG.md"
|
||||
|
||||
|
||||
[tool.commitizen]
|
||||
|
|
|
@ -105,6 +105,9 @@ 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)
|
||||
|
@ -118,22 +121,10 @@ 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*.
|
||||
"""
|
||||
# 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')
|
||||
providers = load_entry_points(f'{self.appname}.providers')
|
||||
for key in list(providers):
|
||||
providers[key] = providers[key](self.config)
|
||||
return providers
|
||||
|
@ -287,7 +278,8 @@ class AppProvider:
|
|||
These can add arbitrary extra functionality to the main :term:`app
|
||||
handler`. See also :doc:`/narr/providers/app`.
|
||||
|
||||
:param config: The app :term:`config object`.
|
||||
:param config: Config object for the app. This should be an
|
||||
instance of :class:`~wuttjamaican.conf.WuttaConfig`.
|
||||
|
||||
Instances have the following attributes:
|
||||
|
||||
|
@ -309,16 +301,7 @@ class AppProvider:
|
|||
config = config.config
|
||||
|
||||
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
|
||||
self.app = config.get_app()
|
||||
|
||||
|
||||
class GenericHandler:
|
||||
|
@ -335,12 +318,3 @@ 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
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
# -*- 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
|
||||
"""
|
|
@ -21,21 +21,14 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
Data Models
|
||||
WuttJamaican - database model
|
||||
|
||||
This is the default :term:`app model` module.
|
||||
For convenience, from this ``wuttjamaican.db.model`` namespace you can
|
||||
access the following:
|
||||
|
||||
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`
|
||||
* :class:`~wuttjamaican.db.model.auth.Role`
|
||||
* :class:`~wuttjamaican.db.model.auth.Permission`
|
||||
* :class:`~wuttjamaican.db.model.auth.User`
|
||||
* :class:`~wuttjamaican.db.model.auth.UserRole`
|
||||
* :func:`~wuttjamaican.db.model.base.uuid_column()`
|
||||
"""
|
||||
|
||||
from .base import Base, Setting, uuid_column, uuid_fk_column
|
||||
from .auth import Role, Permission, User, UserRole
|
||||
from .base import Base, uuid_column, Setting
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
# -*- 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.
|
||||
""")
|
|
@ -21,7 +21,7 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
Base Models
|
||||
WuttJamaican - base models
|
||||
|
||||
.. class:: Base
|
||||
|
||||
|
@ -47,13 +47,6 @@ 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`.
|
||||
|
|
2
tasks.py
2
tasks.py
|
@ -30,7 +30,7 @@ def release(c, skip_tests=False):
|
|||
Release a new version of WuttJamaican
|
||||
"""
|
||||
if not skip_tests:
|
||||
c.run('pytest')
|
||||
c.run('tox')
|
||||
|
||||
# rebuild local tar.gz file for distribution
|
||||
if os.path.exists('WuttJamaican.egg-info'):
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# -*- 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")
|
|
@ -14,16 +14,7 @@ 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):
|
||||
|
||||
|
|
|
@ -134,7 +134,6 @@ 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():
|
||||
|
@ -156,7 +155,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('wutta.app.providers')
|
||||
load_entry_points.assert_called_once_with('wuttatest.providers')
|
||||
self.assertEqual(len(providers), 1)
|
||||
self.assertIn('fake', providers)
|
||||
self.assertIsInstance(providers['fake'], FakeProvider)
|
||||
|
@ -213,4 +212,3 @@ 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')
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
# -*- 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)
|
Loading…
Reference in a new issue