Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/db/model/auth.py: 100%
65 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-31 19:12 -0600
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-31 19:12 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-2025 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Auth Models
26The :term:`auth handler` is primarily responsible for managing the
27data for these models.
29Basic design/structure is as follows:
31* :class:`User` may be assigned to multiple roles
32* :class:`Role` may contain multiple users (cf. :class:`UserRole`)
33* :class:`Role` may be granted multiple permissions
34* :class:`Permission` is a permission granted to a role
35* roles are not nested/grouped; each is independent
36* a few roles are built-in, e.g. Administrators
38So a user's permissions are "inherited" from the role(s) to which they
39belong.
40"""
42import sqlalchemy as sa
43from sqlalchemy import orm
44from sqlalchemy.ext.associationproxy import association_proxy
46from wuttjamaican.db.util import uuid_column, uuid_fk_column
47from wuttjamaican.db.model.base import Base
48from wuttjamaican.util import make_utc
51class Role(Base): # pylint: disable=too-few-public-methods
52 """
53 Represents an authentication role within the system; used for
54 permission management.
56 .. attribute:: permissions
58 List of keys (string names) for permissions granted to this
59 role.
61 See also :attr:`permission_refs`.
63 .. attribute:: users
65 List of :class:`User` instances belonging to this role.
67 See also :attr:`user_refs`.
68 """
70 __tablename__ = "role"
71 __versioned__ = {}
73 uuid = uuid_column()
75 name = sa.Column(
76 sa.String(length=100),
77 nullable=False,
78 unique=True,
79 doc="""
80 Name for the role. Each role must have a name, which must be
81 unique.
82 """,
83 )
85 notes = sa.Column(
86 sa.Text(),
87 nullable=True,
88 doc="""
89 Arbitrary notes for the role.
90 """,
91 )
93 permission_refs = orm.relationship(
94 "Permission",
95 back_populates="role",
96 cascade="all, delete-orphan",
97 cascade_backrefs=False,
98 doc="""
99 List of :class:`Permission` references for the role.
101 See also :attr:`permissions`.
102 """,
103 )
105 permissions = association_proxy(
106 "permission_refs",
107 "permission",
108 creator=lambda p: Permission(permission=p),
109 # TODO
110 # getset_factory=getset_factory,
111 )
113 user_refs = orm.relationship(
114 "UserRole",
115 back_populates="role",
116 cascade="all, delete-orphan",
117 cascade_backrefs=False,
118 doc="""
119 List of :class:`UserRole` instances belonging to the role.
121 See also :attr:`users`.
122 """,
123 )
125 users = association_proxy(
126 "user_refs",
127 "user",
128 creator=lambda u: UserRole(user=u),
129 # TODO
130 # getset_factory=getset_factory,
131 )
133 def __str__(self):
134 return self.name or ""
137class Permission(Base): # pylint: disable=too-few-public-methods
138 """
139 Represents a permission granted to a role.
140 """
142 __tablename__ = "permission"
143 __versioned__ = {}
145 role_uuid = uuid_fk_column("role.uuid", primary_key=True, nullable=False)
146 role = orm.relationship(
147 Role,
148 back_populates="permission_refs",
149 cascade_backrefs=False,
150 doc="""
151 Reference to the :class:`Role` for which the permission is
152 granted.
153 """,
154 )
156 permission = sa.Column(
157 sa.String(length=254),
158 primary_key=True,
159 doc="""
160 Key (name) of the permission which is granted.
161 """,
162 )
164 def __str__(self):
165 return self.permission or ""
168class User(Base): # pylint: disable=too-few-public-methods
169 """
170 Represents a user of the system.
172 This may or may not correspond to a real person, i.e. some users
173 may exist solely for automated tasks.
175 .. attribute:: roles
177 List of :class:`Role` instances to which the user belongs.
179 See also :attr:`role_refs`.
180 """
182 __tablename__ = "user"
183 __versioned__ = {"exclude": ["password"]}
185 uuid = uuid_column()
187 username = sa.Column(
188 sa.String(length=25),
189 nullable=False,
190 unique=True,
191 doc="""
192 Account username. This is required and must be unique.
193 """,
194 )
196 password = sa.Column(
197 sa.String(length=60),
198 nullable=True,
199 doc="""
200 Hashed password for login. (The raw password is not stored.)
201 """,
202 )
204 person_uuid = uuid_fk_column("person.uuid", nullable=True)
205 person = orm.relationship(
206 "Person",
207 # TODO: seems like this is not needed?
208 # uselist=False,
209 back_populates="users",
210 cascade_backrefs=False,
211 doc="""
212 Reference to the :class:`~wuttjamaican.db.model.base.Person`
213 whose user account this is.
214 """,
215 )
217 active = sa.Column(
218 sa.Boolean(),
219 nullable=False,
220 default=True,
221 doc="""
222 Flag indicating whether the user account is "active" - it is
223 ``True`` by default.
225 The default auth logic will prevent login for "inactive" user accounts.
226 """,
227 )
229 prevent_edit = sa.Column(
230 sa.Boolean(),
231 nullable=True,
232 doc="""
233 If set, this user account can only be edited by root. User cannot
234 change their own password.
235 """,
236 )
238 role_refs = orm.relationship(
239 "UserRole",
240 back_populates="user",
241 cascade="all, delete-orphan",
242 cascade_backrefs=False,
243 doc="""
244 List of :class:`UserRole` instances belonging to the user.
246 See also :attr:`roles`.
247 """,
248 )
250 roles = association_proxy(
251 "role_refs",
252 "role",
253 creator=lambda r: UserRole(role=r),
254 # TODO
255 # getset_factory=getset_factory,
256 )
258 api_tokens = orm.relationship(
259 "UserAPIToken",
260 back_populates="user",
261 order_by="UserAPIToken.created",
262 cascade="all, delete-orphan",
263 cascade_backrefs=False,
264 doc="""
265 List of :class:`UserAPIToken` instances belonging to the user.
266 """,
267 )
269 def __str__(self):
270 if self.person:
271 name = str(self.person)
272 if name:
273 return name
274 return self.username or ""
277class UserRole(Base): # pylint: disable=too-few-public-methods
278 """
279 Represents the association between a user and a role; i.e. the
280 user "belongs" or "is assigned" to the role.
281 """
283 __tablename__ = "user_x_role"
284 __versioned__ = {}
285 __wutta_hint__ = {
286 "model_title": "User Role",
287 "model_title_plural": "User Roles",
288 }
290 uuid = uuid_column()
292 user_uuid = uuid_fk_column("user.uuid", nullable=False)
293 user = orm.relationship(
294 User,
295 back_populates="role_refs",
296 cascade_backrefs=False,
297 doc="""
298 Reference to the :class:`User` involved.
299 """,
300 )
302 role_uuid = uuid_fk_column("role.uuid", nullable=False)
303 role = orm.relationship(
304 Role,
305 back_populates="user_refs",
306 cascade_backrefs=False,
307 doc="""
308 Reference to the :class:`Role` involved.
309 """,
310 )
313class UserAPIToken(Base): # pylint: disable=too-few-public-methods
314 """
315 User authentication token for use with HTTP API
316 """
318 __tablename__ = "user_api_token"
319 __wutta_hint__ = {
320 "model_title": "User API Token",
321 "model_title_plural": "User API Tokens",
322 }
324 uuid = uuid_column()
326 user_uuid = uuid_fk_column("user.uuid", nullable=False)
327 user = orm.relationship(
328 User,
329 back_populates="api_tokens",
330 cascade_backrefs=False,
331 doc="""
332 Reference to the :class:`User` whose token this is.
333 """,
334 )
336 description = sa.Column(
337 sa.String(length=255),
338 nullable=False,
339 doc="""
340 Description of the token.
341 """,
342 )
344 token_string = sa.Column(
345 sa.String(length=255),
346 nullable=False,
347 doc="""
348 Raw token string, to be used by API clients.
349 """,
350 )
352 created = sa.Column(
353 sa.DateTime(),
354 nullable=False,
355 default=make_utc,
356 doc="""
357 Date/time when the token was created.
358 """,
359 )
361 def __str__(self):
362 return self.description or ""