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