Users¶
commons.users
¶
User management module providing authentication, authorization, and user identity classes.
This module implements a flexible user system with three levels of user identity and a role-based access control (RBAC) system for permission management.
User Hierarchy¶
The module defines three concrete user types, all implementing the User protocol:
-
AnonymousUser: Represents unauthenticated visitors. Has a fixed ID of
"__anonymous__"and no personal information. UseAnonymousUser.get()to obtain the singleton instance. -
AuthenticatedUser: Represents users authenticated via external systems (e.g., OAuth, SSO) who are not registered in the local database. Contains identity information (name, email) but no credentials. Created via
AuthenticatedUser.create(). -
RegisteredUser: Represents users with local accounts stored in the database. Has credentials (username/password) and can have role assignments. This is a SQLModel table class. Created via
RegisteredUser.create(). A registered user may have no password (password_hashisNone): they can't sign in with a password, only by other means such as an email link or code (commons.users.login_tokens).
Permission System¶
The RBAC system uses path-based scopes and permissions:
-
Permission: A capability like
"/read"or"/admin/delete". Permissions use hierarchical paths where"/admin"implies"/admin/delete". -
Role: A named collection of permissions (e.g., "editor" with
["/read", "/write"]). Roles are linked to permissions via theRolePermissionjunction table. -
Scope: A resource boundary like
"/projects/my-project". Scopes use hierarchical paths where"/projects"contains"/projects/my-project". -
RoleAssignment: Links a
RegisteredUserto aRolewithin aScope. This is the primary mechanism for granting permissions to users. A user has a role on a scope at most once; the same role on another scope is a separate assignment.list_assignments(scope)andremove_assignments(scope)work on a scope and every scope below it (e.g. all roles in a tenant).
A role on a scope applies to every scope below it, so a typical multi-tenant setup is
an admin role on "/app" for global admins and a tenant-admin role on
"/app/tenants/<tenant-id>" for the admins of one tenant.
Paths¶
Scope and permission paths are normalized to one spelling wherever they enter
(assigning, defining roles, checking): "/app/" and "app" are "/app",
"/app//x" is "/app/x", "/foo/Foo Bar" is "/foo/foo-bar"; letters are
lowercased, accents removed, other characters become -, and dashes (also double
ones, "/lib--python-commons") are kept. Paths whose meaning normalizing would
change raise ValueError: segments . and .., segments that are empty after
normalizing, and paths without segments other than the root "/" (e.g. "").
Because different spellings become one scope, ids that differ only in case or special
characters share their roles (Tenant_A and tenant-a): build scopes from
generated ids (commons.id_operators.new_id), not from names.
Default Roles¶
Roles passed as default_roles to the repository are defined in code. They are
synced into the database when the repository is created: stored with exactly the
permissions of the code and marked as default role (Role.is_default_role), so they
can be assigned like any other role; a role of the same name created by the app is
taken over. A default role removed from the code is deleted on the next start,
together with its assignments. Default roles can't be changed or deleted at runtime
(upsert_role() and delete_role() raise ValueError); apps should show them as
not editable.
Repository Pattern¶
User operations are performed through repository classes:
-
UsersRepository: Abstract base class defining the interface for user management, authentication, and permission checking.
-
SQLUsersRepository: Concrete implementation using SQLAlchemy/SQLModel for database persistence.
Command Line¶
python -m commons.users hash-password prints a bcrypt hash for a password, to be
used as password_hash of users defined in the settings (see
commons.settings.StandardUser).
Example Usage¶
::
from sqlalchemy import create_engine
from commons.users import SQLUsersRepository, RegisteredUser, Role
# Create repository
engine = create_engine("sqlite:///users.db")
repo = SQLUsersRepository(engine)
# Create a role with permissions
admin_role = Role.create("admin", ["/read", "/write", "/admin"])
repo.upsert_role(admin_role)
# Create a user
user = RegisteredUser.create(
username="john.doe",
password_hash=repo.hash_password("secret"),
display_name="John Doe",
firstname="John",
lastname="Doe",
email="john@example.com",
)
repo.upsert_user(user)
# Assign role to user
repo.add_assignment(user, "/projects/alpha", "admin")
# Check permission
if repo.has_permission(user, "/projects/alpha", "/write"):
print("User can write!")
Modules:
| Name | Description |
|---|---|
login_tokens |
Passwordless login with a link or code sent to a registered user. |
repository |
|
sql |
SQL-based implementation of the UsersRepository. |
user |
User identity and RBAC model classes. |
Classes:
| Name | Description |
|---|---|
UsersRepository |
Abstract base class for user repository implementations. |
SQLUsersRepository |
SQL database implementation of UsersRepository using SQLAlchemy/SQLModel. |
User |
Protocol defining the common interface for all user types. |
AnonymousUser |
Represents an unauthenticated user (visitor). |
AuthenticatedUser |
Represents a user authenticated via an external system without a local account. |
RegisteredUser |
A user with a local account stored in the database. |
Role |
A named collection of permissions that can be assigned to users. |
RoleAssignment |
Links a registered user to a role within a specific scope. |
RolePermission |
Junction table linking roles to permissions (many-to-many relationship). |
SystemUser |
Represents a system itself. |
__all__
module-attribute
¶
__all__ = [
"AnonymousUser",
"AuthenticatedUser",
"RegisteredUser",
"Role",
"RoleAssignment",
"RolePermission",
"SystemUser",
"SQLUsersRepository",
"User",
"UsersRepository",
]
UsersRepository
¶
UsersRepository(
default_roles: list[Role] | None,
anonymous_default_permissions: (
list[DefaultRoleAssignment] | None
),
authenticated_default_permissions: (
list[DefaultRoleAssignment] | None
),
registered_default_permissions: (
list[DefaultRoleAssignment] | None
),
password_hasher: PasswordHasher | None = None,
password_verifier: PasswordVerifier | None = None,
)
Bases: ABC
Abstract base class for user repository implementations.
Provides user management, authentication, and permission checking functionality. Subclasses must implement the abstract methods to provide database-specific operations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
default_roles
|
list[Role] | None
|
Roles defined in code; see "Default Roles" in |
required |
anonymous_default_permissions
|
list[DefaultRoleAssignment] | None
|
Role assignments applied to anonymous (unauthenticated) users. |
required |
authenticated_default_permissions
|
list[DefaultRoleAssignment] | None
|
Role assignments applied to authenticated but unregistered users. |
required |
registered_default_permissions
|
list[DefaultRoleAssignment] | None
|
Role assignments applied to all registered users. |
required |
password_hasher
|
PasswordHasher | None
|
Custom password hashing function. Defaults to bcrypt. |
None
|
password_verifier
|
PasswordVerifier | None
|
Custom password verification function. Defaults to bcrypt. |
None
|
Methods:
| Name | Description |
|---|---|
add_assignment |
Add a role assignment to a user. |
authenticate |
Verify credentials and return the user if valid. |
check_permission |
Check if a user has a permission, raising an error if not. |
delete_role |
Delete a role. |
delete_user |
Delete a user. |
find_role_by_name |
Find a role by its name. |
find_user_by_email |
Find a user by their email address. |
find_user_by_id |
Find a user by their unique ID. |
find_user_by_username |
Find a user by their username. |
get_role_by_name |
Get a role by its name, raising an error if not found. |
get_role_assignments |
Fetch the role assignments assigned to a user. |
get_user_by_email |
Get a user by their email address, raising an error if not found. |
get_user_by_id |
Get a user by their unique ID, raising an error if not found. |
get_user_by_username |
Get a user by their username, raising an error if not found. |
has_permission |
Check whether a user has a specific permission in a scope. |
hash_password |
Hash a plain-text password using the configured hasher. |
init_from_settings |
Initialize users from settings configuration. |
list_roles |
List all roles from the database. |
registered_user_has_permission |
Check whether a registered user has a specific permission in a scope. |
remove_assignment |
Remove a role assignment. |
role_has_permission |
Check whether a role grants a specific permission within a scope. |
update_password |
Update a user's password. |
upsert_role |
Create or update a role. |
upsert_user |
Create or update a user. |
list_assignments |
List the role assignments on a scope and on every scope below it. |
remove_assignments |
Remove the role assignments on a scope and on every scope below it, e.g. when a tenant is deleted. |
Attributes:
| Name | Type | Description |
|---|---|---|
default_roles |
|
|
anonymous_default_permissions |
|
|
authenticated_default_permissions |
|
|
registered_default_permissions |
|
Source code in src/commons/users/repository.py
anonymous_default_permissions
instance-attribute
¶
authenticated_default_permissions
instance-attribute
¶
registered_default_permissions
instance-attribute
¶
add_assignment
abstractmethod
¶
add_assignment(
user: RegisteredUser | str, scope: str, role: str
) -> RoleAssignment
Add a role assignment to a user.
A user has a role on a scope at most once: if the assignment exists already, it is returned and nothing is written. The same role on another scope (also one below it) is a separate assignment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
RegisteredUser | str
|
The user instance or username to assign the role to. |
required |
scope
|
str
|
The scope path (e.g., '/projects/my-project') where the role applies. |
required |
role
|
str
|
The role name to assign. |
required |
Returns:
| Type | Description |
|---|---|
RoleAssignment
|
The created (or existing) RoleAssignment. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the user or the role doesn't exist. |
ValueError
|
If the scope is invalid. |
Source code in src/commons/users/repository.py
authenticate
¶
authenticate(
username: str, password: str
) -> RegisteredUser | None
Verify credentials and return the user if valid.
A user without a password (password_hash is None or empty) is never
authenticated, whatever password is given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
The username to authenticate. |
required |
password
|
str
|
The plain-text password to verify. |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser | None
|
The authenticated RegisteredUser if credentials are valid, None otherwise. |
Source code in src/commons/users/repository.py
check_permission
¶
check_permission(
user: User, scope: str, permission: str
) -> None
Check if a user has a permission, raising an error if not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
User
|
The user to check permissions for. |
required |
scope
|
str
|
The scope path to check the permission in. |
required |
permission
|
str
|
The permission path to check. |
required |
Raises:
| Type | Description |
|---|---|
LookupError
|
If the user does not have the required permission. |
Source code in src/commons/users/repository.py
delete_role
abstractmethod
¶
delete_role(role: Role | str) -> None
Delete a role.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
role
|
Role | str
|
The role instance or role name to delete. |
required |
delete_user
abstractmethod
¶
delete_user(user: RegisteredUser | str) -> None
Delete a user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
RegisteredUser | str
|
The user instance or username to delete. |
required |
find_role_by_name
¶
find_role_by_name(name: str) -> Role | None
Find a role by its name.
Searches both default_roles and the database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The role name to search for. |
required |
Returns:
| Type | Description |
|---|---|
Role | None
|
The Role if found, None otherwise. |
Source code in src/commons/users/repository.py
find_user_by_email
abstractmethod
¶
find_user_by_email(email: str) -> RegisteredUser | None
Find a user by their email address.
The comparison is case-insensitive, as email addresses are commonly entered with varying case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
email
|
str
|
The user's email address. |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser | None
|
The RegisteredUser if found, None otherwise. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If more than one user is registered with the given email address. |
Source code in src/commons/users/repository.py
find_user_by_id
abstractmethod
¶
find_user_by_id(user_id: str) -> RegisteredUser | None
Find a user by their unique ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_id
|
str
|
The user's unique identifier (UUID). |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser | None
|
The RegisteredUser if found, None otherwise. |
Source code in src/commons/users/repository.py
find_user_by_username
abstractmethod
¶
find_user_by_username(
username: str,
) -> RegisteredUser | None
Find a user by their username.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
The user's unique username. |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser | None
|
The RegisteredUser if found, None otherwise. |
Source code in src/commons/users/repository.py
get_role_by_name
¶
get_role_by_name(name: str) -> Role
Get a role by its name, raising an error if not found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The role name to search for. |
required |
Returns:
| Type | Description |
|---|---|
Role
|
The Role. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no role exists with the given name. |
Source code in src/commons/users/repository.py
get_role_assignments
abstractmethod
¶
get_role_assignments(
user: RegisteredUser | str,
) -> list[RoleAssignment]
Fetch the role assignments assigned to a user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
RegisteredUser | str
|
The user instance or username to get assignments for. |
required |
Returns:
| Type | Description |
|---|---|
list[RoleAssignment]
|
List of RoleAssignments for the user. |
Source code in src/commons/users/repository.py
get_user_by_email
¶
get_user_by_email(email: str) -> RegisteredUser
Get a user by their email address, raising an error if not found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
email
|
str
|
The user's email address (compared case-insensitively). |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser
|
The RegisteredUser. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no user exists with the given email address. |
ValueError
|
If more than one user is registered with the given email address. |
Source code in src/commons/users/repository.py
get_user_by_id
¶
get_user_by_id(user_id: str) -> RegisteredUser
Get a user by their unique ID, raising an error if not found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_id
|
str
|
The user's unique identifier (UUID). |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser
|
The RegisteredUser. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no user exists with the given ID. |
Source code in src/commons/users/repository.py
get_user_by_username
¶
get_user_by_username(username: str) -> RegisteredUser
Get a user by their username, raising an error if not found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
The user's unique username. |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser
|
The RegisteredUser. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no user exists with the given username. |
Source code in src/commons/users/repository.py
has_permission
¶
has_permission(
user: User, scope: str, permission: str
) -> bool
Check whether a user has a specific permission in a scope.
An entry (default permission or role assignment) whose role doesn't exist grants nothing: it is skipped with a warning in the log, and the other entries still count.
Permission checking is based on the user type: - AnonymousUser: Checks anonymous_default_permissions - AuthenticatedUser: Checks authenticated_default_permissions - RegisteredUser: Checks registered_default_permissions and user-specific assignments
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
User
|
The user to check permissions for. |
required |
scope
|
str
|
The scope path to check the permission in (e.g., '/projects/my-project'). |
required |
permission
|
str
|
The permission path to check (e.g., '/read', '/admin/delete'). |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the user has the permission, False otherwise. |
Source code in src/commons/users/repository.py
hash_password
¶
Hash a plain-text password using the configured hasher.
For hashes to put into the settings, use the command line helper
python -m commons.users hash-password (default bcrypt hasher).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
password
|
str
|
The plain-text password to hash. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The hashed password string. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the password is empty. |
Source code in src/commons/users/repository.py
init_from_settings
¶
init_from_settings(settings: StandardUserSettings) -> None
Initialize users from settings configuration.
Creates users defined in the settings which do not exist yet and assigns their
configured roles. A configured password_hash is stored as-is; a plain
password is hashed with the configured hasher; a user with neither is
created without a password.
Users which already exist in the repository are left untouched (no password,
profile or role changes), so e.g. passwords changed via update_password()
are not reset on the next start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
StandardUserSettings
|
The user settings containing user definitions. |
required |
Source code in src/commons/users/repository.py
list_roles
abstractmethod
¶
list_roles() -> list[Role]
List all roles from the database.
Note: This does not include default_roles. Use default_roles attribute to access predefined roles.
Returns:
| Type | Description |
|---|---|
list[Role]
|
List of all Role objects in the database. |
Source code in src/commons/users/repository.py
registered_user_has_permission
¶
registered_user_has_permission(
user: RegisteredUser, scope: str, permission: str
) -> bool
Check whether a registered user has a specific permission in a scope.
Checks both registered_default_permissions (applied to all registered users) and the user's individual role assignments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
RegisteredUser
|
The registered user to check permissions for. |
required |
scope
|
str
|
The scope path to check the permission in. |
required |
permission
|
str
|
The permission path to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the user has the permission, False otherwise. |
Source code in src/commons/users/repository.py
remove_assignment
abstractmethod
¶
remove_assignment(assignment: RoleAssignment | str) -> None
Remove a role assignment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
assignment
|
RoleAssignment | str
|
The RoleAssignment instance or assignment ID (UUID) to remove. |
required |
role_has_permission
¶
role_has_permission(
scope: str,
role: Role | str,
scope_to_check: str,
permission_to_check: str,
) -> bool
Check whether a role grants a specific permission within a scope.
Uses path-based matching: a role with permission '/admin' also grants '/admin/delete', and a role assigned to scope '/projects' also applies to '/projects/my-project'.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
str
|
The scope path where the role is assigned. |
required |
role
|
Role | str
|
The role instance or role name to check. |
required |
scope_to_check
|
str
|
The scope path to verify access for. |
required |
permission_to_check
|
str
|
The permission path to verify. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the role grants the permission in the scope, False otherwise. |
Source code in src/commons/users/repository.py
update_password
¶
update_password(
user: RegisteredUser | str, new_password: str
) -> RegisteredUser
Update a user's password.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
RegisteredUser | str
|
The user instance or username whose password should be updated. |
required |
new_password
|
str
|
The new plain-text password (will be hashed before storage); must not be empty. |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser
|
The updated RegisteredUser with the new password hash. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a username is provided and no user exists with that username. |
ValueError
|
If the new password is empty. |
Source code in src/commons/users/repository.py
upsert_role
abstractmethod
¶
Create or update a role.
If a role with the same name exists, it is updated. Otherwise, a new role is created.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
role
|
Role
|
The role to create or update. |
required |
Returns:
| Type | Description |
|---|---|
Role
|
The created or updated Role. |
Source code in src/commons/users/repository.py
upsert_user
abstractmethod
¶
upsert_user(user: RegisteredUser) -> RegisteredUser
Create or update a user.
If a user with the same ID exists, it is updated. Otherwise, a new user is created.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
RegisteredUser
|
The user to create or update. |
required |
Returns:
| Type | Description |
|---|---|
RegisteredUser
|
The created or updated RegisteredUser. |
Source code in src/commons/users/repository.py
list_assignments
abstractmethod
¶
list_assignments(scope: str) -> list[RoleAssignment]
List the role assignments on a scope and on every scope below it.
Assignments on scopes above it (which also apply to it) are not included; list the parent scopes to find them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
str
|
The scope path, e.g. |
required |
Returns:
| Type | Description |
|---|---|
list[RoleAssignment]
|
The assignments, each with its |
Source code in src/commons/users/repository.py
remove_assignments
abstractmethod
¶
Remove the role assignments on a scope and on every scope below it, e.g. when a tenant is deleted.
Scope entries left without assignments are removed, too. Assignments on scopes above it are kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
str
|
The scope path. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of removed assignments. |
Source code in src/commons/users/repository.py
SQLUsersRepository
¶
SQLUsersRepository(
engine: Engine,
default_roles: list[Role] | None = None,
anonymous_default_permissions: (
list[DefaultRoleAssignment] | None
) = None,
authenticated_default_permissions: (
list[DefaultRoleAssignment] | None
) = None,
registered_default_permissions: (
list[DefaultRoleAssignment] | None
) = None,
password_hasher: PasswordHasher | None = None,
password_verifier: PasswordVerifier | None = None,
)
Bases: UsersRepository
SQL database implementation of UsersRepository using SQLAlchemy/SQLModel.
This repository persists users, roles, permissions, and role assignments to a SQL database. It automatically creates the required tables on initialization if they don't exist.
Existing tables are not migrated: a database created before commons 1.4 (without
registered_users.locale) or before commons 1.2 (without roles.is_default_role
and the unique role assignments) must be recreated.
Supported databases include SQLite, PostgreSQL, MySQL, and any other database supported by SQLAlchemy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
SQLAlchemy Engine connected to the database. |
required |
default_roles
|
list[Role] | None
|
Roles defined in code. They are synced into the database on
creation (marked as default roles, with exactly these permissions); default
roles no longer listed are deleted with their assignments. They can't be
changed or deleted at runtime. See "Default Roles" in |
None
|
anonymous_default_permissions
|
list[DefaultRoleAssignment] | None
|
Role assignments automatically applied to anonymous (unauthenticated) users. |
None
|
authenticated_default_permissions
|
list[DefaultRoleAssignment] | None
|
Role assignments automatically applied to authenticated users without local accounts. |
None
|
registered_default_permissions
|
list[DefaultRoleAssignment] | None
|
Role assignments automatically applied to all registered users (in addition to their individual assignments). |
None
|
password_hasher
|
PasswordHasher | None
|
Custom password hashing function. Defaults to bcrypt. |
None
|
password_verifier
|
PasswordVerifier | None
|
Custom password verification function. Defaults to bcrypt. |
None
|
Example::
from sqlalchemy import create_engine
from commons.users import SQLUsersRepository, Role
# Create with SQLite
engine = create_engine("sqlite:///users.db")
repo = SQLUsersRepository(engine)
# Create with PostgreSQL and default roles
engine = create_engine("postgresql://user:pass@localhost/db")
viewer_role = Role.create("viewer", ["/read"])
repo = SQLUsersRepository(
engine,
default_roles=[viewer_role],
registered_default_permissions=[DefaultRoleAssignment(scope="/", role="viewer")],
)
See Also
UsersRepository: Abstract base class defining the interface.
Methods:
Source code in src/commons/users/sql.py
add_assignment
¶
add_assignment(
user: RegisteredUser | str, scope: str, role: str
) -> RoleAssignment
Source code in src/commons/users/sql.py
delete_role
¶
delete_role(role: Role | str) -> None
Source code in src/commons/users/sql.py
delete_user
¶
delete_user(user: RegisteredUser | str) -> None
Source code in src/commons/users/sql.py
find_user_by_email
¶
find_user_by_email(email: str) -> RegisteredUser | None
Source code in src/commons/users/sql.py
find_user_by_id
¶
find_user_by_id(user_id: str) -> RegisteredUser | None
Source code in src/commons/users/sql.py
find_user_by_username
¶
find_user_by_username(
username: str,
) -> RegisteredUser | None
Source code in src/commons/users/sql.py
get_role_assignments
¶
get_role_assignments(
user: RegisteredUser | str,
) -> list[RoleAssignment]
Source code in src/commons/users/sql.py
list_assignments
¶
list_assignments(scope: str) -> list[RoleAssignment]
Source code in src/commons/users/sql.py
remove_assignment
¶
remove_assignment(assignment: RoleAssignment | str) -> None
Source code in src/commons/users/sql.py
remove_assignments
¶
Source code in src/commons/users/sql.py
upsert_role
¶
Source code in src/commons/users/sql.py
upsert_user
¶
upsert_user(user: RegisteredUser) -> RegisteredUser
User
¶
Bases: Protocol
Protocol defining the common interface for all user types.
This protocol enables polymorphic handling of different user types (AnonymousUser, AuthenticatedUser, RegisteredUser) in permission checks and other operations.
All concrete user classes implement this protocol, allowing code to work with any user type without knowing the specific implementation.
Example::
def greet_user(user: User) -> str:
return f"Hello, {user.name('casual')}!"
# Works with any user type
greet_user(AnonymousUser.get())
greet_user(AuthenticatedUser.create("John Doe"))
greet_user(registered_user)
Methods:
| Name | Description |
|---|---|
name |
Get a formatted name for the user. |
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique identifier for the user. |
display_name |
str
|
Human-readable display name for the user. |
firstname |
str | None
|
User's first name, if available. |
lastname |
str | None
|
User's last name, if available. |
email |
str | None
|
User's email address, if available. |
name
¶
Get a formatted name for the user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type
|
UserNameType
|
The name format to return. See |
'casual'
|
Returns:
| Type | Description |
|---|---|
str
|
The formatted name string. |
AnonymousUser
¶
Bases: BaseModel
Represents an unauthenticated user (visitor).
Anonymous users have no personal information and a fixed identity. They are
used to represent visitors who have not logged in. Permission checks for
anonymous users use the repository's anonymous_default_permissions.
This class implements the User protocol.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Always |
display_name |
str
|
Defaults to |
firstname |
str | None
|
Always |
lastname |
str | None
|
Always |
email |
str | None
|
Always |
Example::
# Get the anonymous user instance
anon = AnonymousUser.get()
# Check permissions
if repo.has_permission(anon, "/public", "/read"):
show_public_content()
Methods:
| Name | Description |
|---|---|
get |
Get an AnonymousUser instance. |
name |
Get a formatted name for the anonymous user. |
id_
class-attribute
instance-attribute
¶
id_: Literal["__anonymous__"] = Field(
default="__anonymous__",
validation_alias="id",
serialization_alias="id",
)
display_name_
class-attribute
instance-attribute
¶
display_name_: str = Field(
default="Anonymous User",
validation_alias="display_name",
serialization_alias="display_name",
)
get
staticmethod
¶
get() -> AnonymousUser
Get an AnonymousUser instance.
Returns:
| Type | Description |
|---|---|
AnonymousUser
|
An AnonymousUser with default values. |
name
¶
Get a formatted name for the anonymous user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type
|
UserNameType
|
The name format (ignored for anonymous users). |
'casual'
|
Returns:
| Type | Description |
|---|---|
str
|
Always returns |
Source code in src/commons/users/user.py
AuthenticatedUser
¶
Bases: BaseModel
Represents a user authenticated via an external system without a local account.
Authenticated users are typically created from external identity providers
(OAuth, SSO, SAML, etc.). They have identity information but no local
credentials. Permission checks use the repository's authenticated_default_permissions.
This class implements the User protocol.
Use AuthenticatedUser.create() to construct instances with proper defaults.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique identifier (UUID), auto-generated if not provided. |
display_name |
str
|
Human-readable name (required). |
firstname |
str | None
|
First name (optional). |
lastname |
str | None
|
Last name (optional). |
email |
str | None
|
Email address (optional). |
Example::
# Create from OAuth callback data
user = AuthenticatedUser.create(
display_name="Jane Smith",
id=oauth_user_id,
firstname="Jane",
lastname="Smith",
email="jane@example.com",
)
# Check permissions
if repo.has_permission(user, "/documents", "/read"):
show_documents()
See Also
RegisteredUser: For users with local accounts and credentials.
Methods:
| Name | Description |
|---|---|
name |
Get a formatted name for the user. |
create |
Create an AuthenticatedUser with the given attributes. |
model_config
class-attribute
instance-attribute
¶
id_
class-attribute
instance-attribute
¶
id_: str = Field(
default_factory=new_id,
validation_alias="id",
serialization_alias="id",
)
display_name_
class-attribute
instance-attribute
¶
firstname_
class-attribute
instance-attribute
¶
firstname_: str | None = Field(
default=None,
validation_alias="firstname",
serialization_alias="firstname",
)
lastname_
class-attribute
instance-attribute
¶
lastname_: str | None = Field(
default=None,
validation_alias="lastname",
serialization_alias="lastname",
)
email_
class-attribute
instance-attribute
¶
name
¶
Get a formatted name for the user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type
|
UserNameType
|
The name format to return. |
'casual'
|
Returns:
| Type | Description |
|---|---|
str
|
|
str
|
|
str
|
|
str
|
|
Source code in src/commons/users/user.py
create
staticmethod
¶
create(
display_name: str,
*,
id: str | None = None,
firstname: str | None = None,
lastname: str | None = None,
email: str | None = None
) -> AuthenticatedUser
Create an AuthenticatedUser with the given attributes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
display_name
|
str
|
Human-readable display name (required). |
required |
id
|
str | None
|
Unique identifier. Auto-generated UUID if not provided. |
None
|
firstname
|
str | None
|
First name (optional). |
None
|
lastname
|
str | None
|
Last name (optional). |
None
|
email
|
str | None
|
Email address (optional). |
None
|
Returns:
| Type | Description |
|---|---|
AuthenticatedUser
|
A new AuthenticatedUser instance. |
Source code in src/commons/users/user.py
RegisteredUser
¶
Bases: SQLModel
A user with a local account stored in the database.
Registered users have credentials (username and password hash) and can have
role assignments granting specific permissions. They are authenticated via
UsersRepository.authenticate() and can be assigned roles via
UsersRepository.add_assignment().
This class implements the User protocol and is a SQLModel table class.
Use RegisteredUser.create() to construct instances with proper defaults.
Use UsersRepository.hash_password() to generate password hashes in code,
or python -m commons.users hash-password on the command line (e.g. for
password_hash of users configured in the settings).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique identifier (UUID), auto-generated if not provided. |
username |
str
|
Unique login username. |
password_hash |
str | None
|
Bcrypt-hashed password (never store plain passwords), or |
display_name |
str
|
Human-readable name for display. |
firstname |
str | None
|
First name (optional). |
lastname |
str | None
|
Last name (optional). |
email |
str | None
|
Email address (optional). |
locale |
str | None
|
The language the user chose (a language tag such as |
Example::
# Create a new user
user = RegisteredUser.create(
username="john.doe",
password_hash=repo.hash_password("secret123"),
display_name="John Doe",
firstname="John",
lastname="Doe",
email="john@example.com",
)
repo.upsert_user(user)
# Authenticate
authenticated = repo.authenticate("john.doe", "secret123")
# Assign roles
repo.add_assignment(user, "/projects/alpha", "admin")
# Check permissions
if repo.has_permission(user, "/projects/alpha", "/admin/delete"):
delete_project()
See Also
AnonymousUser: For unauthenticated visitors. AuthenticatedUser: For externally authenticated users without local accounts. RoleAssignment: For granting permissions to registered users.
Methods:
| Name | Description |
|---|---|
name |
Get a formatted name for the user. |
create |
Create a RegisteredUser with the given attributes. |
id_
class-attribute
instance-attribute
¶
id_: str = Field(
default_factory=new_id,
validation_alias="id",
serialization_alias="id",
sa_column=Column("id", String(64), primary_key=True),
description="Unique id of the user.",
)
username_
class-attribute
instance-attribute
¶
username_: str = Field(
validation_alias="username",
serialization_alias="username",
sa_column=Column("username", String(64), unique=True),
)
password_hash_
class-attribute
instance-attribute
¶
password_hash_: str | None = Field(
default=None,
validation_alias="password_hash",
serialization_alias="password_hash",
sa_column=Column("password_hash", String(256)),
)
display_name_
class-attribute
instance-attribute
¶
display_name_: str = Field(
validation_alias="display_name",
serialization_alias="display_name",
sa_column=Column("display_name", Unicode(128)),
)
firstname_
class-attribute
instance-attribute
¶
firstname_: str | None = Field(
default=None,
validation_alias="firstname",
serialization_alias="firstname",
sa_column=Column("firstname", Unicode(64)),
)
lastname_
class-attribute
instance-attribute
¶
lastname_: str | None = Field(
default=None,
validation_alias="lastname",
serialization_alias="lastname",
sa_column=Column("lastname", Unicode(64)),
)
email_
class-attribute
instance-attribute
¶
email_: str | None = Field(
default=None,
validation_alias="email",
serialization_alias="email",
sa_column=Column("email", String(128)),
)
locale_
class-attribute
instance-attribute
¶
locale_: str | None = Field(
default=None,
validation_alias="locale",
serialization_alias="locale",
sa_column=Column("locale", String(35)),
)
name
¶
Get a formatted name for the user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type
|
UserNameType
|
The name format to return. |
'casual'
|
Returns:
| Type | Description |
|---|---|
str
|
|
str
|
|
str
|
|
str
|
|
Source code in src/commons/users/user.py
create
staticmethod
¶
create(
username: str,
password_hash: str | None,
display_name: str,
*,
id: str | None = None,
firstname: str | None = None,
lastname: str | None = None,
email: str | None = None,
locale: str | None = None
) -> RegisteredUser
Create a RegisteredUser with the given attributes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
Unique login username (required). |
required |
password_hash
|
str | None
|
Pre-hashed password, or |
required |
display_name
|
str
|
Human-readable display name (required). |
required |
id
|
str | None
|
Unique identifier. Auto-generated UUID if not provided. |
None
|
firstname
|
str | None
|
First name (optional). |
None
|
lastname
|
str | None
|
Last name (optional). |
None
|
email
|
str | None
|
Email address (optional). |
None
|
locale
|
str | None
|
The language the user chose, e.g. |
None
|
Returns:
| Type | Description |
|---|---|
RegisteredUser
|
A new RegisteredUser instance. |
Example::
user = RegisteredUser.create(
username="jane.doe",
password_hash=repo.hash_password("password123"),
display_name="Jane Doe",
firstname="Jane",
lastname="Doe",
)
Source code in src/commons/users/user.py
Role
¶
Bases: SQLModel
A named collection of permissions that can be assigned to users.
Roles group related permissions together for easier management. For example,
an "editor" role might include "/read" and "/write" permissions,
while an "admin" role might include "/admin" (covering all admin sub-permissions).
Roles are assigned to users within a scope via RoleAssignment.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Unique role name (e.g., |
permissions |
list[Permission]
|
List of permissions granted by this role. |
is_default_role |
bool
|
Whether the role is a default role, defined in code
( |
Example::
# Create a role with permissions
admin_role = Role.create("admin", ["/read", "/write", "/admin"])
# Save to database
repo.upsert_role(admin_role)
# Assign to a user within a scope
repo.add_assignment(user, "/projects/alpha", "admin")
See Also
Permission: Individual capabilities granted by roles. RoleAssignment: Links users to roles within scopes. Scope: Resource boundaries where roles apply.
Methods:
| Name | Description |
|---|---|
create |
Create a Role with the given permissions. |
name
class-attribute
instance-attribute
¶
is_default_role
class-attribute
instance-attribute
¶
is_default_role: bool = Field(
default=False,
sa_column=Column(
Boolean, nullable=False, default=False
),
)
permissions
class-attribute
instance-attribute
¶
permissions: list[Permission] = Relationship(
back_populates="roles",
link_model=RolePermission,
sa_relationship_kwargs={"lazy": "selectin"},
)
create
staticmethod
¶
create(name: str, permissions: list[str]) -> Role
Create a Role with the given permissions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Unique role name. |
required |
permissions
|
list[str]
|
List of permission paths (e.g., |
required |
Returns:
| Type | Description |
|---|---|
Role
|
A new Role instance with Permission objects created for each path. |
Source code in src/commons/users/user.py
RoleAssignment
¶
Bases: SQLModel
Links a registered user to a role within a specific scope.
Role assignments are the primary mechanism for granting permissions to users. Each assignment grants a user all permissions defined in the role, but only within the specified scope (and its child scopes).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique identifier for the assignment (UUID). |
scope_name |
str
|
The scope path where the role applies. |
role_name |
str
|
The name of the assigned role. |
user_id |
str
|
The ID of the user receiving the assignment. |
scope |
Scope
|
The Scope object (relationship). |
role |
Role
|
The Role object (relationship). |
user |
RegisteredUser
|
The RegisteredUser object (relationship). |
Example::
# Assign a role to a user (via repository)
assignment = repo.add_assignment(user, "/projects/alpha", "editor")
# The user now has all "editor" permissions within /projects/alpha
repo.has_permission(user, "/projects/alpha", "/write") # True
repo.has_permission(user, "/projects/beta", "/write") # False (different scope)
# Remove the assignment
repo.remove_assignment(assignment)
See Also
Role: Defines what permissions the assignment grants. Scope: Defines where the assignment applies. RegisteredUser: The user receiving the assignment.
__table_args__
class-attribute
instance-attribute
¶
id
class-attribute
instance-attribute
¶
id: str = Field(
default_factory=new_id,
sa_column=Column("id", String(64), primary_key=True),
description="Unique id of the role assignment.",
)
scope_name
class-attribute
instance-attribute
¶
scope_name: str = Field(
sa_column=Column(
String(128), ForeignKey("scopes.name")
),
description="The scope on which the role has been assigned.",
)
role_name
class-attribute
instance-attribute
¶
role_name: str = Field(
sa_column=Column(String(64), ForeignKey("roles.name")),
description="The role which has been assigned to the user.",
)
user_id
class-attribute
instance-attribute
¶
user_id: str = Field(
sa_column=Column(
String(64), ForeignKey("registered_users.id")
),
description="The user to whom the role has been assigned.",
)
scope
class-attribute
instance-attribute
¶
RolePermission
¶
Bases: SQLModel
Junction table linking roles to permissions (many-to-many relationship).
This is an internal model used by SQLAlchemy to represent the relationship
between Role and Permission. You typically don't interact with this
class directly; instead, use Role.permissions to access a role's permissions.
Attributes:
| Name | Type | Description |
|---|---|---|
permission_name |
str
|
Foreign key to the permission. |
role_name |
str
|
Foreign key to the role. |
See Also
Role: The role that has the permission. Permission: The permission granted to the role.
SystemUser
¶
Bases: BaseModel
Represents a system itself.
System users should be used when an applications does something autonomously (e.g., seeding, running background processes), which are not on behalf of other users.
This class implements the User protocol.
Methods:
| Name | Description |
|---|---|
get |
Get n SystemUser instance. |
name |
Get a formatted name for the anonymous user. |
Attributes:
| Name | Type | Description |
|---|---|---|
id_ |
Literal['__system__']
|
|
display_name_ |
str
|
|
id |
str
|
|
display_name |
str
|
|
firstname |
str | None
|
|
lastname |
str | None
|
|
email |
str | None
|
|
id_
class-attribute
instance-attribute
¶
id_: Literal["__system__"] = Field(
default="__system__",
validation_alias="id",
serialization_alias="id",
)
display_name_
class-attribute
instance-attribute
¶
display_name_: str = Field(
default="System",
validation_alias="display_name",
serialization_alias="display_name",
)
get
staticmethod
¶
get() -> SystemUser
name
¶
Get a formatted name for the anonymous user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type
|
UserNameType
|
The name format (ignored for anonymous users). |
'casual'
|
Returns:
| Type | Description |
|---|---|
str
|
Always returns |