2
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 :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

View file

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

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

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