Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/db/model/auth.py: 100%
67 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-17 14:16 -0600
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-17 14:16 -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, Person
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 back_populates="users",
208 cascade_backrefs=False,
209 doc="""
210 Reference to the :class:`~wuttjamaican.db.model.base.Person`
211 whose user account this is.
212 """,
213 )
215 # TODO: these may or may not be good ideas? i added them mostly
216 # for sake of testing association proxy behavior in wuttaweb, b/c
217 # i was lazy and didn't want to write proper fixtures. so if
218 # they are a problem then doing that should fix it..
219 first_name = association_proxy(
220 "person",
221 "first_name",
222 creator=lambda n: Person(first_name=n, full_name=n),
223 )
224 last_name = association_proxy(
225 "person",
226 "last_name",
227 creator=lambda n: Person(last_name=n, full_name=n),
228 )
230 active = sa.Column(
231 sa.Boolean(),
232 nullable=False,
233 default=True,
234 doc="""
235 Flag indicating whether the user account is "active" - it is
236 ``True`` by default.
238 The default auth logic will prevent login for "inactive" user accounts.
239 """,
240 )
242 prevent_edit = sa.Column(
243 sa.Boolean(),
244 nullable=True,
245 doc="""
246 If set, this user account can only be edited by root. User cannot
247 change their own password.
248 """,
249 )
251 role_refs = orm.relationship(
252 "UserRole",
253 back_populates="user",
254 cascade="all, delete-orphan",
255 cascade_backrefs=False,
256 doc="""
257 List of :class:`UserRole` instances belonging to the user.
259 See also :attr:`roles`.
260 """,
261 )
263 roles = association_proxy(
264 "role_refs",
265 "role",
266 creator=lambda r: UserRole(role=r),
267 # TODO
268 # getset_factory=getset_factory,
269 )
271 api_tokens = orm.relationship(
272 "UserAPIToken",
273 back_populates="user",
274 order_by="UserAPIToken.created",
275 cascade="all, delete-orphan",
276 cascade_backrefs=False,
277 doc="""
278 List of :class:`UserAPIToken` instances belonging to the user.
279 """,
280 )
282 def __str__(self):
283 if self.person:
284 name = str(self.person)
285 if name:
286 return name
287 return self.username or ""
290class UserRole(Base): # pylint: disable=too-few-public-methods
291 """
292 Represents the association between a user and a role; i.e. the
293 user "belongs" or "is assigned" to the role.
294 """
296 __tablename__ = "user_x_role"
297 __versioned__ = {}
298 __wutta_hint__ = {
299 "model_title": "User Role",
300 "model_title_plural": "User Roles",
301 }
303 uuid = uuid_column()
305 user_uuid = uuid_fk_column("user.uuid", nullable=False)
306 user = orm.relationship(
307 User,
308 back_populates="role_refs",
309 cascade_backrefs=False,
310 doc="""
311 Reference to the :class:`User` involved.
312 """,
313 )
315 role_uuid = uuid_fk_column("role.uuid", nullable=False)
316 role = orm.relationship(
317 Role,
318 back_populates="user_refs",
319 cascade_backrefs=False,
320 doc="""
321 Reference to the :class:`Role` involved.
322 """,
323 )
326class UserAPIToken(Base): # pylint: disable=too-few-public-methods
327 """
328 User authentication token for use with HTTP API
329 """
331 __tablename__ = "user_api_token"
332 __wutta_hint__ = {
333 "model_title": "User API Token",
334 "model_title_plural": "User API Tokens",
335 }
337 uuid = uuid_column()
339 user_uuid = uuid_fk_column("user.uuid", nullable=False)
340 user = orm.relationship(
341 User,
342 back_populates="api_tokens",
343 cascade_backrefs=False,
344 doc="""
345 Reference to the :class:`User` whose token this is.
346 """,
347 )
349 description = sa.Column(
350 sa.String(length=255),
351 nullable=False,
352 doc="""
353 Description of the token.
354 """,
355 )
357 token_string = sa.Column(
358 sa.String(length=255),
359 nullable=False,
360 doc="""
361 Raw token string, to be used by API clients.
362 """,
363 )
365 created = sa.Column(
366 sa.DateTime(),
367 nullable=False,
368 default=make_utc,
369 doc="""
370 Date/time when the token was created.
371 """,
372 )
374 def __str__(self):
375 return self.description or ""