feat: add basic "auth" data models: user/role/perm
not really tested yet though, other than unit tests
This commit is contained in:
parent
7442047d0e
commit
639b0de8b1
6
docs/api/wuttjamaican/auth.rst
Normal file
6
docs/api/wuttjamaican/auth.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.auth``
|
||||
=====================
|
||||
|
||||
.. automodule:: wuttjamaican.auth
|
||||
:members:
|
6
docs/api/wuttjamaican/db.model.auth.rst
Normal file
6
docs/api/wuttjamaican/db.model.auth.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.db.model.auth``
|
||||
==============================
|
||||
|
||||
.. automodule:: wuttjamaican.db.model.auth
|
||||
:members:
|
|
@ -8,10 +8,12 @@
|
|||
:maxdepth: 1
|
||||
|
||||
app
|
||||
auth
|
||||
conf
|
||||
db
|
||||
db.conf
|
||||
db.model
|
||||
db.model.auth
|
||||
db.model.base
|
||||
db.sess
|
||||
exc
|
||||
|
|
|
@ -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`.
|
||||
|
|
44
src/wuttjamaican/auth.py
Normal file
44
src/wuttjamaican/auth.py
Normal 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
|
||||
"""
|
|
@ -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
|
||||
|
|
229
src/wuttjamaican/db/model/auth.py
Normal file
229
src/wuttjamaican/db/model/auth.py
Normal 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.
|
||||
""")
|
|
@ -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`.
|
||||
|
|
36
tests/db/model/test_auth.py
Normal file
36
tests/db/model/test_auth.py
Normal 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")
|
|
@ -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):
|
||||
|
||||
|
|
17
tests/test_auth.py
Normal file
17
tests/test_auth.py
Normal 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)
|
Loading…
Reference in a new issue