flask_more_smorest.perms

Permissions module for Flask-More-Smorest.

This module provides the permissions system including the Api with auth, BasePermsModel with permission checks, user models, and PermsBlueprintMixin.

Quick Start:

from flask_more_smorest.perms import init_fms from flask_more_smorest.perms.models.defaults import User

# Register user models init_fms(user=User)

# Use the UserBlueprint user_bp = UserBlueprint(register=False) api.register_blueprint(user_bp)

# Note: no global singleton user_bp is provided; create explicitly.

Functions

clear_registration()

Clear all user model registrations and helper functions.

class flask_more_smorest.perms.AbstractDomain(**kwargs)[source]

Abstract Domain model for multi-domain support.

This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration.

Domains represent distinct contexts within an application (e.g., organizations, tenants, or projects) where roles can be scoped.

Subclassing example:

from flask_more_smorest.perms import AbstractDomain

class CustomDomain(AbstractDomain):
    __tablename__ = "domain"

    name: Mapped[str] = mapped_column(db.String(255), nullable=False)
    display_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
    active: Mapped[bool] = mapped_column(db.Boolean, default=True, nullable=False)

    # Optional: custom fields
    organization_id: Mapped[str] = mapped_column(db.String(50))
    settings: Mapped[dict] = mapped_column(db.JSON, default={})
name: Mapped[str] = <sqlalchemy.orm.properties.MappedColumn object>
display_name: Mapped[str] = <sqlalchemy.orm.properties.MappedColumn object>
active: Mapped[bool] = <sqlalchemy.orm.properties.MappedColumn object>
class flask_more_smorest.perms.AbstractToken(**kwargs)[source]

Abstract Token model for API authentication.

This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration.

Permission checks are delegated to the owning user by default (via UserOwnershipMixin). Override _can_read/_can_write/_can_create to customize.

Subclassing example:

from flask_more_smorest.perms import AbstractToken

class CustomToken(AbstractToken):
    __tablename__ = "token"

    token: Mapped[str] = mapped_column(db.String(1024), nullable=False)
    description: Mapped[str | None] = mapped_column(db.String(64), nullable=True)
    expires_at: Mapped[sa.DateTime | None] = mapped_column(sa.DateTime(), nullable=True)
    revoked: Mapped[bool] = mapped_column(db.Boolean(), nullable=False, default=False)
    revoked_at: Mapped[sa.DateTime | None] = mapped_column(sa.DateTime(), nullable=True)

    # Optional: custom fields
    last_used_at: Mapped[sa.DateTime | None] = mapped_column(sa.DateTime(), nullable=True)
    ip_address: Mapped[str | None] = mapped_column(db.String(45), nullable=True)
token: Mapped[str] = <sqlalchemy.orm.properties.MappedColumn object>
description: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
expires_at: Mapped[dt.datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
revoked: Mapped[bool] = <sqlalchemy.orm.properties.MappedColumn object>
revoked_at: Mapped[dt.datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
class flask_more_smorest.perms.AbstractUser(**kwargs)[source]

Abstract User model with email/password auth, roles, and domain support.

This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration.

Features (all inherited): - Email/password authentication - Roles management via UserRole relationship - Settings management via UserSetting relationship - Token management via Token relationship - Permission checks (_can_read, _can_write, _can_create) - Admin properties (is_admin, is_superadmin) - Role checking (has_role, list_roles)

Subclassing example:

from flask_more_smorest.perms import AbstractUser, init_fms

class CustomUser(AbstractUser):
    # Optional: custom fields only
    bio: Mapped[str | None] = mapped_column(sa.String(500))

    def _can_write(self, user) -> bool:
        return super()._can_write(user)

# Register with the system
init_fms(user=CustomUser)
email: Mapped[str] = <sqlalchemy.orm.properties.MappedColumn object>
password: Mapped[bytes | None] = <sqlalchemy.orm.properties.MappedColumn object>
is_enabled: Mapped[bool] = <sqlalchemy.orm.properties.MappedColumn object>
roles = <_RelationshipDeclared at 0x7a2d88ffbc50; no key>
settings = <_RelationshipDeclared at 0x7a2d88ffb1b0; no key>
tokens = <_RelationshipDeclared at 0x7a2d88c35e50; no key>
__init__(**kwargs)[source]

Create new user with optional password hashing.

classmethod get_current_user()[source]

Get the current authenticated user of this User subclass.

This provides zero-boilerplate typed access to the current user. Uses the application’s configured authentication (JWT or custom getter).

Return type:

Optional[TypeVar(UserT, bound= AbstractUser)]

Returns:

Current user instance of this User subclass if authenticated, None otherwise

Example

>>> user = AbstractUser.get_current_user()
>>> user = MyCustomUser.get_current_user()
normalize_email(email)[source]

Normalize email to lowercase for case-insensitive lookups.

Emails are automatically converted to lowercase when set, ensuring: - Case-insensitive login (user@example.com == USER@EXAMPLE.COM) - Prevention of duplicate registrations with different cases - Efficient database queries using the email index - Consistent email storage throughout the application

Parameters:

email (str | None) – Email address to normalize

Return type:

str | None

Returns:

Lowercase email address, or None if email is None

set_password(password)[source]

Set password with secure hashing.

Return type:

None

is_password_correct(password)[source]

Check if provided password matches stored hash.

Return type:

bool

update(commit=True, **kwargs)[source]

Update user with password handling.

Return type:

None

property is_admin: bool

Check if user has admin privileges.

property is_superadmin: bool

Check if user has superadmin privileges.

has_role(role, domain_name=None)[source]

Check if user has specified role, optionally scoped to domain.

Parameters:
  • role (Union[Literal['ADMIN', 'SUPERADMIN'], str, Enum]) – Role to check (string or enum value)

  • domain_name (str | None) – Optional domain name to scope the check

Return type:

bool

Returns:

True if user has the role, False otherwise

Example

>>> user.has_role("ADMIN")
True
>>> user.has_role("ADMIN", domain_name="main")
True
list_roles()[source]

List user roles as strings.

Return type:

list[str]

property num_tokens: int

Get number of tokens for this user.

property domain_ids: set[UUID | str]

Return set of domain IDs the user has roles for.

has_domain_access(domain_id)[source]

Check if user has access to a specific domain.

Users have access to a domain if they have any role associated with that domain, or if they have a wildcard role (*). Superadmins automatically have access.

Parameters:

domain_id (UUID | None) – Domain UUID to check access for, or None for global access

Return type:

bool

Returns:

True if user has access to the domain, False otherwise

Example

>>> user.has_domain_access(domain_id)
True
>>> user.has_domain_access(None)  # Global access check
True
class flask_more_smorest.perms.AbstractUserRole(domain_id=None, role=None, **kwargs)[source]

Abstract UserRole model with domain scoping for multi-domain applications.

This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration.

Supports custom role enums by accepting any string/enum value:

from enum import Enum

class CustomRole(str, Enum):
    SUPERADMIN = "SUPERADMIN"
    ADMIN = "ADMIN"
    MANAGER = "MANAGER"
    USER = "USER"

class CustomUserRole(AbstractUserRole):
    __tablename__ = "user_role"

    user_id: Mapped[uuid.UUID] = mapped_column(
        sa.Uuid(as_uuid=True),
        db.ForeignKey("user.id"),
        nullable=False
    )
    domain_id: Mapped[uuid.UUID | None] = mapped_column(
        sa.Uuid(as_uuid=True),
        db.ForeignKey("domain.id"),
        nullable=True,
        default=None,
    )
    _role: Mapped[str] = mapped_column("role", sa.String(50), nullable=False)

# Create roles with custom enum values
role = CustomUserRole(user=user, role=CustomRole.MANAGER)
user_id: Mapped[uuid.UUID] = <sqlalchemy.orm.properties.MappedColumn object>
domain_id: Mapped[uuid.UUID | None] = <sqlalchemy.orm.properties.MappedColumn object>
user = <_RelationshipDeclared at 0x7a2d88c35f90; no key>
domain = <_RelationshipDeclared at 0x7a2d887d34d0; no key>
property role: str

Get role as string value.

Returns:

Role name as string

__init__(domain_id=None, role=None, **kwargs)[source]

Initialize role with domain and role handling.

Parameters:
  • domain_id (UUID | str | None) – Domain UUID, None for all domains, or ‘*’ string (converted to None)

  • role (str | Enum | None) – Role value (enum or string)

  • **kwargs (object) – Additional field values

class flask_more_smorest.perms.AbstractUserSetting(**kwargs)[source]

Abstract UserSetting model for key-value storage.

This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration.

Permission checks are delegated to the owning user by default (via UserOwnershipMixin). Override _can_read/_can_write/_can_create to customize.

Subclassing example:

from flask_more_smorest.perms import AbstractUserSetting

class CustomUserSetting(AbstractUserSetting):
    __tablename__ = "user_setting"

    key: Mapped[str] = mapped_column(db.String(80), nullable=False)
    value: Mapped[str | None] = mapped_column(db.String(1024), nullable=True)

    __table_args__ = (db.UniqueConstraint("user_id", "key"),)

    # Optional: custom fields
    metadata: Mapped[dict] = mapped_column(db.JSON, default={})
    encrypted: Mapped[bool] = mapped_column(db.Boolean, default=False)
key: Mapped[str] = <sqlalchemy.orm.properties.MappedColumn object>
value: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
class flask_more_smorest.perms.Api(app=None, *, spec_kwargs=None)[source]

Extended Api with JWT authentication and permission checking.

This class extends Flask-Smorest’s Api to automatically: - Configure JWT authentication in OpenAPI spec - Enforce authentication on non-public endpoints - Check admin permissions on admin-only endpoints - Customize schema naming for OpenAPI

Example

>>> from flask import Flask
>>> from flask_more_smorest.perms import Api
>>>
>>> app = Flask(__name__)
>>> api = Api(app)
__init__(app=None, *, spec_kwargs=None)[source]

Initialize the API with custom Marshmallow plugin.

Parameters:
  • app (Flask | None) – Optional Flask application

  • spec_kwargs (dict | None) – Optional keyword arguments for APISpec

init_app(app, *pargs, **kwargs)[source]

Initialize the API with a Flask application.

Sets up OpenAPI security schemes and before_request handler for authentication and authorization.

Parameters:
  • app (Flask) – Flask application to initialize

  • *pargs (Any) – Additional positional arguments

  • **kwargs (Any) – Additional keyword arguments

Return type:

None

class flask_more_smorest.perms.BasePermsModel(**kwargs)[source]

Base model with permission checking.

perms_disabled

Disable permission checks (default: False)

Example

>>> class Article(BasePermsModel):
...     title: Mapped[str] = mapped_column(sa.String(200))
...     def _can_write(self, user) -> bool:
...         return user is not None and self.user_id == user.id
perms_disabled = False
__init__(**kwargs)[source]

Initialize model after checking sub-fields can be created.

classmethod bypass_perms(cls)[source]

Temporarily disable permission checking for this model class.

Example

>>> with Article.bypass_perms():
...     article.delete()  # No permission check
Return type:

Iterator[None]

can_write(user=None)[source]

Check if current user has write permission.

Return type:

bool

can_read(user=None)[source]

Check if current user has read permission.

Return type:

bool

can_create(user=None)[source]

Check if current user can create objects.

Return type:

bool

save(commit=True)[source]

Extend BaseModel save with permission checks.

Return type:

Self

delete(commit=True)[source]

Extend BaseModel delete with permission checks.

Return type:

None

classmethod get_by(**kwargs)[source]

Get resource by field values with permission check.

Return type:

Optional[Self]

Returns:

Instance if found and can_read() is True None if not found None if found but can_read() is False and RETURN_404_ON_ACCESS_DENIED is True

Raises:

ForbiddenError – If found but can_read() is False

check_create(val, _visited=None)[source]

Recursively check that all BaseModel instances can be created.

Parameters:
  • val (list | set | tuple | object) – Value or collection of values to check

  • _visited (set[int] | None) – Internal set of visited object IDs to prevent infinite recursion

Raises:

ForbiddenError – If any nested object cannot be created

Return type:

None

class flask_more_smorest.perms.HasUserMixin[source]

Adds user_id foreign key and user relationship to a model.

Configuration:
  • __user_field_name__: Custom alias for user_id (default: “user_id”)

  • __user_relationship_name__: Custom alias for user (default: “user”)

  • __user_id_nullable__: Allow NULL owner IDs (default: False)

  • __user_backref_name__: Custom backref on User model
    • None (default): Auto-generate as {tablename}s (e.g., “articles”)

    • Custom string: Use specified name

    • "": Skip backref creation

Example

>>> class Article(BasePermsModel, HasUserMixin):
...     __user_backref_name__ = "written_articles"
...     title: Mapped[str] = mapped_column(sa.String(200))
>>> user.written_articles  # Custom backref
classmethod __init_subclass__(**kwargs)[source]

Configure user field and relationship aliases on subclass creation.

user_id = <sqlalchemy.orm.properties.MappedColumn object>
user

Relationship to the registered User model.

Uses lazy resolution via lambda to support custom User models registered through init_fms(). The lambda is evaluated during mapper configuration, allowing get_user_model() to return the correct registered User class.

class flask_more_smorest.perms.PermsBlueprint(name, import_name, model=None, schema=None, model_import_name=None, schema_import_name=None, res_id='id', res_id_param=None, methods=[CRUDMethod.INDEX, CRUDMethod.GET, CRUDMethod.POST, CRUDMethod.PATCH, CRUDMethod.DELETE], skip_methods=None, default_page_size=20, db_session=None, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, cli_group=None)[source]

CRUD Blueprint with permission annotations.

Combines CRUDBlueprint functionality with PermsBlueprintMixin to provide automatic CRUD operations with permission checking support.

class flask_more_smorest.perms.PermsBlueprintMixin[source]

Blueprint mixin with added annotations for public and admin endpoints.

This mixin extends Flask-Smorest’s Blueprint to provide additional decorators for marking endpoints with special access levels: - public_endpoint: Accessible without authentication - admin_endpoint: Requires admin privileges

Example

>>> class MyBlueprint(Blueprint, PermsBlueprintMixin):
...     pass
>>> bp = MyBlueprint('items', __name__)
>>> @bp.route('/')
>>> @bp.public_endpoint
>>> def list_items():
...     return []
public_endpoint(func)[source]

Decorator to mark an endpoint as public.

Public endpoints do not require authentication and can be accessed by anyone.

Parameters:

func (Callable) – The endpoint function to mark as public

Return type:

Callable

Returns:

The decorated function with public annotation

Example

>>> @bp.route('/health')
>>> @bp.public_endpoint
>>> def health_check():
...     return {'status': 'ok'}
admin_endpoint(func)[source]

Decorator to mark an endpoint as admin only.

Admin endpoints require the user to have admin privileges. The Api class enforces this during request handling.

Parameters:

func (Callable) – The endpoint function to mark as admin only

Return type:

Callable

Returns:

The decorated function with admin annotation

Example

>>> @bp.route('/users/<uuid:user_id>')
>>> @bp.admin_endpoint
>>> def delete_user(user_id):
...     # Only admins can delete users
...     pass
class flask_more_smorest.perms.ProfileMixin[source]

Adds profile fields: first_name, last_name, display_name, avatar_url.

Property: full_name returns combined first/last name.

first_name: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
last_name: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
display_name: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
avatar_url: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
property full_name: str

Get formatted full name.

Returns:

Full name as “first last”, or just first or last if one is missing

classmethod parse_full_name(full_name)[source]

Parse a full name into first and last name components.

Strips leading/trailing whitespace and splits on first space. Everything after the first space is considered the last name.

Parameters:

full_name (str) – The full name string

Return type:

dict[str, str]

Returns:

Dictionary with ‘first_name’ and ‘last_name’ keys

property avatar: str | None

Get avatar URL (alias for avatar_url).

Override this property to implement custom avatar logic (e.g., generating Gravatar or Initials avatar if avatar_url is missing).

class flask_more_smorest.perms.SoftDeleteMixin[source]

Soft delete with deleted_at timestamp and helper methods.

Methods: soft_delete() marks as deleted, restore() clears. Property: is_deleted returns True if deleted_at is not None.

deleted_at: Mapped[datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
property is_deleted: bool

Check if record is soft deleted.

Returns:

True if record has been soft deleted

soft_delete()[source]

Mark record as soft deleted.

Sets deleted_at to current time and optionally disables the record if is_enabled field exists.

Return type:

None

restore()[source]

Restore soft deleted record.

Clears deleted_at and optionally re-enables the record if is_enabled field exists.

Return type:

None

class flask_more_smorest.perms.TimestampMixin[source]

Adds authentication-related timestamps: last_login_at, email_verified_at.

last_login_at: Mapped[datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
email_verified_at: Mapped[datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
class flask_more_smorest.perms.UserBlueprint(name='users', import_name='flask_more_smorest.perms.user_blueprint', model=None, schema=None, url_prefix='/api/users/', methods=None, skip_methods=None, register=False, **kwargs)[source]

Blueprint for User CRUD operations with authentication endpoints.

This blueprint extends CRUDBlueprint to provide: - Standard CRUD operations for User model (GET, POST, PATCH, DELETE) - Public login endpoint (POST /login/) - Current user profile endpoint (GET /me/)

When the User model has PUBLIC_REGISTRATION=True, the POST endpoint is automatically made public to allow unauthenticated user registration.

Parameters:
  • name (str) – Blueprint name (default: “users”)

  • import_name (str) – Import name (default: __name__)

  • model (type[BaseModel] | str | None) – Model class or string (default: User from registry)

  • schema (type[Schema] | str | None) – Schema class or string (default: UserSchema)

  • url_prefix (str | None) – URL prefix for all routes (default: “/api/users/”)

  • methods (list[CRUDMethod] | Mapping[CRUDMethod, MethodConfig | bool] | None) – CRUD methods to enable (default: all methods)

  • skip_methods (list[CRUDMethod] | None) – CRUD methods to disable (default: None)

  • register (bool) – If True, register the model with init_fms (default: False)

  • **kwargs (Any) – Additional arguments passed to CRUDBlueprint

Example

>>> user_bp = UserBlueprint()
>>> app.register_blueprint(user_bp)
>>> # With custom configuration
>>> user_bp = UserBlueprint(
...     url_prefix="/api/v2/users/",
...     skip_methods=[CRUDMethod.DELETE]
... )
>>> # Register custom user model
>>> user_bp = UserBlueprint(model=MyUser, register=True)
>>> # Enable public registration
>>> class PublicUser(User):
...     PUBLIC_REGISTRATION = True
>>> public_bp = UserBlueprint(model=PublicUser)
__init__(name='users', import_name='flask_more_smorest.perms.user_blueprint', model=None, schema=None, url_prefix='/api/users/', methods=None, skip_methods=None, register=False, **kwargs)[source]

Initialize UserBlueprint with default User model and schema.

class flask_more_smorest.perms.UserOwnershipMixin[source]

User-owned resources with configurable permission delegation.

Two modes:

  1. Simple Ownership (default, __delegate_to_user__ = False): - Compares user_id == current_user.id - Use for: Notes, posts, comments

  2. Delegated Permissions (__delegate_to_user__ = True): - Calls self.user._can_write(current_user) - Use for: Tokens, settings, API keys

__delegate_to_user__

Delegate to user’s permission methods (default: False)

__user_id_nullable__

Allow NULL owner IDs (default: False)

Example

>>> class Token(UserOwnershipMixin, BasePermsModel):
...     __delegate_to_user__ = True
...     token: Mapped[str] = mapped_column(sa.String(500))
>>> # Delegates to user's permission methods
flask_more_smorest.perms.clear_registration()[source]

Clear all user model registrations and helper functions.

Return type:

None

flask_more_smorest.perms.get_current_user(user_type=None)[source]
Overloads:
  • AbstractUser | None

  • user_type (type[UserT]) → UserT | None

Get the current authenticated user.

Resolution order: 1. Registered custom getter (via init_fms) 2. Default: JWT-based authentication (built-in)

Parameters:

user_type (type[TypeVar(UserT, bound= AbstractUser)] | None) – Optional user class for typed return. If None, returns AbstractUser | None.

Returns:

Current user instance if authenticated, None otherwise

Return type:

UserT | object | None

flask_more_smorest.perms.get_current_user_func()[source]

Get the registered get_current_user function.

Return type:

Callable[[], AbstractUser | None] | None

Returns:

Registered function or None

flask_more_smorest.perms.get_current_user_id()[source]

Get the current authenticated user’s ID.

Return type:

UUID | None

flask_more_smorest.perms.init_fms(user=None, role=None, token=None, domain=None, setting=None, get_current_user=None)[source]

Initialize Flask-More-Smorest perms integration.

This is the primary integration point. It registers models and helper functions in a single call. Missing models are filled with defaults.

The first call initializes models + helpers. Later calls may only update helper functions (model changes are rejected).

Return type:

None

flask_more_smorest.perms.init_jwt(app)[source]

Initialize JWTManager with user lookup callbacks.

Parameters:

app (Flask) – Flask application to initialize JWT for

Raises:

RuntimeError – If JWT_SECRET_KEY is not set in production (when DEBUG and TESTING are both False)

Return type:

None

flask_more_smorest.perms.is_current_user_admin()[source]

Check if the current user is an admin.

Return type:

bool

flask_more_smorest.perms.is_current_user_superadmin()[source]

Check if the current user is a superadmin.

Return type:

bool

Modules

api

Extended Flask-Smorest API with authentication and permission support.

base_perms_model

Base permission-aware model for Flask-More-Smorest.

jwt

model_mixins

Reusable mixins for User models and other models in Flask-More-Smorest.

models

User-related models for Flask-More-Smorest.

perms_blueprint

Blueprint Mixin to support method annotation for access control.

user_blueprint

User Blueprint with authentication endpoints.

user_context

User context helpers for flask-more-smorest.

user_registry

User model registry for Flask-More-Smorest.