1
0
Fork 0

feat: add basic "auth" data models: user/role/perm

not really tested yet though, other than unit tests
This commit is contained in:
Lance Edgar 2024-07-13 23:25:20 -05:00
parent 7442047d0e
commit 639b0de8b1
11 changed files with 378 additions and 6 deletions

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`.

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

@ -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):

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)