Skip to content

Auth & Permissions

Overview

The Auth plugin provides a composable permission system for AMSDAL Server. It operates in one of two modes, controlled by the AMSDAL_REQUIRE_DEFAULT_AUTHORIZATION setting:

Mode Setting Default permission Behavior
Public API False AllowAny Everything is allowed by default. You selectively protect specific resources.
Protected API True RequireAuth Everything requires authentication. You selectively make specific resources public or add granular permission checks.

Permission Classes

Permission classes are composable objects that determine access. They can be combined with & (AND) and | (OR) operators.

Class Description
AllowAny Always grants access
RequireAuth Requires an authenticated user
RequirePermissions('...') Requires specific permission strings (all must match)
from amsdal.contrib.auth.permissions import AllowAny, RequireAuth, RequirePermissions

Composition

# Both conditions must pass (AND)
RequireAuth & RequirePermissions('models.Post:read')

# At least one condition must pass (OR)
RequirePermissions('models.Post:read') | RequirePermissions('models.Post:create')

# Complex composition
RequireAuth & (RequirePermissions('models.Post:read') | RequirePermissions('models.Post:create'))

Permission Strings

Permission strings follow the format:

{resource_type}.{resource_name}:{action}
Part Values Description
resource_type models, transactions Type of resource
resource_name e.g. Post, CreateOrder, * Resource name or wildcard
action read, create, update, delete, execute, * Action or wildcard

Wildcard matching

Use * as a wildcard for resource names or actions:

models.Post:read                 # read Post
models.Post:*                    # all actions on Post
models.*:read                    # read any model
models.*:*                       # all actions on all models
transactions.CreateOrder:execute # execute CreateOrder
transactions.*:*                 # everything with transactions
*:*                              # superuser — full access

Use Cases

Public API with selective protection

Set AMSDAL_REQUIRE_DEFAULT_AUTHORIZATION = False. Everything is open by default — add permissions only where you need restrictions.

Transaction that requires authentication only

from amsdal_data.transactions.decorators import transaction
from amsdal.contrib.auth.decorators import require_auth

@require_auth
@transaction()
def change_password(old_password: str, new_password: str) -> ChangePasswordResponse:
    """Any authenticated user can change their password."""
    ...

Transaction that requires specific permissions

from amsdal_data.transactions.decorators import transaction
from amsdal.contrib.auth.decorators import permissions
from amsdal.contrib.auth.permissions import RequirePermissions

@permissions(RequirePermissions('transactions.delete_user:execute'))
@transaction()
def delete_user(user_id: str) -> DeleteUserResponse:
    """Only users with the delete_user:execute permission can call this."""
    ...

Model that requires specific permissions

from amsdal.contrib.auth.permissions import RequirePermissions

class AuditLog(Model):
    __permissions__ = RequirePermissions('models.AuditLog:read')

    action: str
    details: str

Only users whose permissions match will have access. All other models remain public.


Protected API with selective public access

Set AMSDAL_REQUIRE_DEFAULT_AUTHORIZATION = True. Everything requires authentication by default — explicitly mark public resources.

Public model

from amsdal.contrib.auth.decorators import allow_any

@allow_any
class LoginSession(Model):
    """Login must be accessible without authentication."""
    email: str
    password: str
    token: str | None = None

Or using the __permissions__ attribute directly:

from amsdal.contrib.auth.permissions import AllowAny

class LoginSession(Model):
    __permissions__ = AllowAny
    ...

Public transaction

from amsdal_data.transactions.decorators import transaction
from amsdal.contrib.auth.decorators import allow_any

@allow_any
@transaction()
def health_check() -> HealthCheckResponse:
    """Available to anyone, even without authentication."""
    return HealthCheckResponse(status="ok")

Per-action permissions

Use the @permissions decorator with keyword arguments to set different permissions per action:

from amsdal.contrib.auth.decorators import permissions
from amsdal.contrib.auth.permissions import AllowAny, RequireAuth, RequirePermissions

@permissions(
    read=AllowAny,
    create=RequireAuth,
    update=RequireAuth & RequirePermissions('models.Post:update'),
    delete=RequirePermissions('models.Post:delete'),
)
class Post(Model):
    title: str
    author_email: str

Unspecified actions fall back to the default permission (first positional argument), or None (which means "use the global default from AMSDAL_REQUIRE_DEFAULT_AUTHORIZATION").

# All actions require auth, but execute also requires a specific permission
@permissions(RequireAuth, execute=RequireAuth & RequirePermissions('transactions.admin_action:execute'))
@transaction()
def admin_action() -> AdminActionResponse:
    ...

Composing permissions with & and |

AND — require authentication AND specific permissions

from amsdal.contrib.auth.decorators import permissions
from amsdal.contrib.auth.permissions import RequireAuth, RequirePermissions

@permissions(RequireAuth & RequirePermissions('transactions.create_post:execute'))
@transaction()
def create_post(title: str) -> CreatePostResponse:
    """User must be authenticated AND have the execute permission."""
    ...

OR — any permission is enough

@permissions(RequirePermissions('transactions.manage_post:execute') | RequirePermissions('transactions.create_post:execute'))
@transaction()
def create_post(title: str) -> CreatePostResponse:
    """Either permission group is sufficient."""
    ...

Object-level Permissions

Class-level permissions control access to a resource type (e.g. "can this user read Posts at all?"). Object-level permissions control access to a specific instance (e.g. "can this user edit this particular Post?").

Define a has_object_permission method on your model:

class MyModel(Model):
    def has_object_permission(
        self,
        user: 'BaseUser',
        action: 'Action',
        update_data: dict[str, Any] | None = None,
        auth: 'AuthCredentials | None' = None,
    ) -> bool:
        ...
Parameter Description
user Current user (request.user)
action Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE
update_data Data being updated (for UPDATE). Can be mutated to filter out fields.
auth Auth credentials with auth.scopes

Example: users can only access their own profile

class User(Model):
    email: str
    name: str

    def has_object_permission(self, user, action, update_data=None, auth=None) -> bool:
        from amsdal.contrib.auth.permissions import has_admin_permissions

        if has_admin_permissions(auth):
            return True

        # Users can only see/edit their own profile
        if user.is_authenticated:
            return self.email == user.identity

        return False

Example: filtering sensitive fields on update

Use the mutable update_data dict to strip fields the user should not modify:

class User(Model):
    email: str
    name: str
    permissions: list[str]

    def has_object_permission(self, user, action, update_data=None, auth=None) -> bool:
        from amsdal_server.apps.common.permissions.enums import Action as ActionEnum

        if action == ActionEnum.UPDATE and update_data is not None:
            update_data.pop('permissions', None)  # users cannot change their own permissions

        if user.is_authenticated:
            return self.email == user.identity

        return False

Example: public create, restricted read/update/delete

class LoginSession(Model):
    email: str
    token: str | None = None

    def has_object_permission(self, user, action, update_data=None, auth=None) -> bool:
        from amsdal_server.apps.common.permissions.enums import Action as ActionEnum

        # Anyone can create a login session (login)
        if action == ActionEnum.CREATE:
            return True

        # Other actions — only own sessions
        if user.is_authenticated:
            return self.email == user.identity

        return False

Example: admin-only resource

class Permission(Model):
    resource_type: str
    model: str
    action: str

    def has_object_permission(self, user, action, update_data=None, auth=None) -> bool:
        from amsdal.contrib.auth.permissions import has_admin_permissions, has_permissions

        if has_admin_permissions(auth):
            return True

        return has_permissions(f'models.Permission:{action.value}', auth)

API Managers (row-level filtering)

For row-level security at the query level, define a custom API manager. This filters the queryset before any results are returned — users only see rows they are allowed to access.

from typing import ClassVar
from amsdal_models.managers.model_manager import Manager
from amsdal_models.querysets.base_queryset import QuerySet, ModelType


class PostApiManager(Manager):
    def get_queryset(self) -> QuerySet[ModelType]:
        from amsdal.context.manager import AmsdalContextManager
        from amsdal.contrib.auth.utils.scopes import is_super_admin

        qs = super().get_queryset()

        request = AmsdalContextManager().get_context().get('request', None)
        if not request or not request.auth:
            return qs.none()

        user_scopes = set(getattr(request.auth, 'scopes', []))

        # Super admin sees everything
        if is_super_admin(user_scopes):
            return qs

        # Regular users see only their own posts
        auth_user = getattr(request, 'user', None)
        if auth_user and auth_user.is_authenticated:
            return qs.filter(author_email=auth_user.identity)

        return qs.none()


class Post(Model):
    api_objects: ClassVar[PostApiManager] = PostApiManager()

    title: str
    author_email: str

Permissions and Roles

User permissions are stored as Permission records and converted to scopes during authentication.

Permission model

Each Permission record has three fields:

Field Description Example
resource_type models or transactions models
model Resource name or * Post
action Action or * read

Each record produces a scope string: {resource_type}.{model}:{action}.

Role examples

Superuser — full access:

*:*

Content administrator — manage posts and comments, read categories, view transactions:

models.Post:*
models.Comment:*
models.Category:read
transactions.*:read

Reader — read-only access to content:

models.Post:read
models.Comment:read

API integration — execute specific transactions and read orders:

transactions.CreateOrder:execute
transactions.GetOrderStatus:execute
models.Order:read

Moderator — read everything, delete comments, update posts:

models.*:read
models.Comment:delete
models.Post:update

Decorators Reference

Decorator Applies to What it does
@permissions(perm) Models, Transactions Sets blanket permission for all actions.
@permissions(read=..., create=..., ...) Models, Transactions Sets per-action permissions.
@allow_any Models, Transactions Alias for @permissions(AllowAny).
@require_auth Models, Transactions Alias for @permissions(RequireAuth).
from amsdal.contrib.auth.decorators import permissions, allow_any, require_auth
from amsdal.contrib.auth.permissions import AllowAny, RequireAuth, RequirePermissions

Important: Auth decorators must be placed above @transaction. This ensures that the __permissions__ attribute is visible to the authorization system.

# Correct
@require_auth
@transaction()
def my_transaction(): ...

# Wrong — auth attributes may not be visible
@transaction()
@require_auth
def my_transaction(): ...

Utility functions

For use inside has_object_permission and custom managers:

Function What it does
has_admin_permissions(auth) Checks if auth credentials have super admin (*:*) access
has_permissions(required, auth) Checks if auth credentials have a specific permission (supports wildcards)
from amsdal.contrib.auth.permissions import has_admin_permissions, has_permissions

# In has_object_permission:
def has_object_permission(self, user, action, update_data=None, auth=None) -> bool:
    if has_admin_permissions(auth):
        return True

    if not has_permissions(f'models.MyModel:{action.value}', auth):
        return False

    # ... custom logic