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
|
:maxdepth: 1
|
||||||
|
|
||||||
app
|
app
|
||||||
|
auth
|
||||||
conf
|
conf
|
||||||
db
|
db
|
||||||
db.conf
|
db.conf
|
||||||
db.model
|
db.model
|
||||||
|
db.model.auth
|
||||||
db.model.base
|
db.model.base
|
||||||
db.sess
|
db.sess
|
||||||
exc
|
exc
|
||||||
|
|
|
@ -55,6 +55,12 @@ Glossary
|
||||||
|
|
||||||
See also the code-friendly :term:`app name`.
|
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
|
command
|
||||||
A top-level command line interface for the app. Note that
|
A top-level command line interface for the app. Note that
|
||||||
top-level commands don't usually "do" anything per se, and are
|
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
|
happens is, a config object is created and then extended by each
|
||||||
of the registered config extensions.
|
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
|
config file
|
||||||
A file which contains :term:`config settings<config setting>`.
|
A file which contains :term:`config settings<config setting>`.
|
||||||
See also :doc:`narr/config/files`.
|
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
|
This is the default :term:`app model` module.
|
||||||
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.Base`
|
||||||
* :class:`~wuttjamaican.db.model.base.Setting`
|
* :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
|
.. class:: Base
|
||||||
|
|
||||||
|
@ -47,6 +47,13 @@ def uuid_column(*args, **kwargs):
|
||||||
return sa.Column(sa.String(length=32), *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):
|
class Setting(Base):
|
||||||
"""
|
"""
|
||||||
Represents a :term:`config setting`.
|
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):
|
def test_basic(self):
|
||||||
column = model.uuid_column()
|
column = model.uuid_column()
|
||||||
self.assertIsInstance(column, sa.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):
|
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