Permissions
How the SDK gates operations: the Operation enum, permission domains, the built-in classes, and how to write your own. Every service (AgentService, ThreadService, MemoryService, WorkflowService, as_view(), history()) checks permissions before acting and raises PermissionDenied on failure.
Operations and Domains
An operation is the unit of access control. A domain is the default permission chain for a group of operations:
from django_ai_sdk.permissions import Operation, PermissionDomain
Operation.CHAT # "chat"
PermissionDomain.MEMORY # "memory"| Domain | Default class |
|---|---|
agent |
AgentDefaultPermission |
thread |
ThreadDefaultPermission |
memory |
MemoryDefaultPermission |
integrations |
IntegrationDefaultPermission |
workflow |
WorkflowDefaultPermission |
Override a domain globally via AI_SDK_PERMISSIONS:
AI_SDK_PERMISSIONS = {
"memory": ["apps.memories.permissions.AllowAnonymousMemoryPermission"],
"thread": ["apps.agents.permissions.DemoThreadPermission"],
}The Operation Enum
# Agent
VIEW_AGENT, CREATE_AGENT, UPDATE_AGENT, DELETE_AGENT
# Threads
LIST_THREADS, CREATE_THREAD, VIEW_THREAD, UPDATE_THREAD, DELETE_THREAD,
DELETE_ALL_THREADS, VIEW_HISTORY
# Messages
VIEW_MESSAGES, SEND_MESSAGE, RATE_MESSAGE, DELETE_MESSAGE, RESTORE_MESSAGE
# Files
UPLOAD_FILE, DELETE_FILE, VIEW_FILE
# Memories
VIEW_MEMORY, CREATE_MEMORY, UPDATE_MEMORY, DELETE_MEMORY,
VIEW_DOCUMENT, LIST_DOCUMENTS, UPLOAD_DOCUMENT, DELETE_DOCUMENT,
LIST_THREAD_MEMORIES, LINK_MEMORY, UNLINK_MEMORY
# Integrations
USE_INTEGRATION, MANAGE_INTEGRATION
# Workflows
VIEW_WORKFLOW, RUN_WORKFLOW, MANAGE_WORKFLOW
# Misc
CHAT, REINDEXBuilt-in Permission Classes
| Class | Behavior |
|---|---|
AllowAll |
Grants everything. |
DenyAll |
Denies everything. |
IsAuthenticated |
Allows any authenticated user. |
IsAdminUser |
Allows staff or superusers. |
IsInAllowedGroups(groups=[...]) |
Allows authenticated users in one of the named auth groups. |
IsOwner(field="user_id") |
Allows the object’s owner; field selects the ownership attribute. |
Tiered default permissions
Four built-ins expose READ / WRITE / MANAGE operation tiers:
ThreadDefaultPermission: authenticated users get full access to their own threads; anonymous users get none.MemoryDefaultPermission:can_managemembers get everything;can_manage=Falsemembers get read + write entries; public read-only is gated byis_public; anonymous is always blocked.AgentDefaultPermission:can_managemembers manage the agent; other members view and use it;is_publicgrants read and use to anyone; anonymous is otherwise blocked for agent operations and passes through everything else. Chat, thread and file operations have no object to check, so they are gated on the agent passed as theagentkeyword.WorkflowDefaultPermission: any authenticated user may author a workflow and run an active one; a stored definition is managed only by itscreated_by; a run is read only by the principal it ran as, since its outputs are that person’s content. Staff manage and read every one of both. It implementsget_queryset_perms, so list endpoints are scoped in the database rather than filtered afterwards — see Workflows.
MemoryService.get_object_permissions_map() and agent_permissions() auto-discover these READ/WRITE/MANAGE tiers and return an ObjectPermissions (can_read / can_write / can_manage) for the frontend.
The BasePermission API
class BasePermission(ABC):
async def has_permission(self, user, operation, **kwargs) -> bool: ...
async def has_object_permission(self, user, operation, obj, **kwargs) -> bool: ...
def get_queryset_perms(self, user, operation, queryset) -> QuerySet: ...has_permission gates the operation itself; has_object_permission gates it on a specific object. Implementing get_queryset_perms lets list endpoints filter at the database level instead of post-checking every row: MemoryDefaultPermission uses it for VIEW_MEMORY.
Writing a Custom Permission
from django_ai_sdk.permissions import BasePermission, Operation
class IsSalesTeam(BasePermission):
async def has_permission(self, user, operation, **kwargs) -> bool:
if user is None or not user.is_authenticated:
return False
return await user.groups.filter(name="Sales").aexists()Parameterized permissions are instances, not classes:
class IsInAllowedGroups(BasePermission):
def __init__(self, groups=None):
self.groups = list(groups or [])
async def has_permission(self, user, operation, **kwargs) -> bool:
return user.is_authenticated and await user.groups.filter(name__in=self.groups).aexists()Declare them on agents (permissions = [IsInAllowedGroups(groups=["eng"])]), on integrations (service.permissions), or as a domain override in AI_SDK_PERMISSIONS. Both class and instance forms are accepted anywhere.
PermissionsMixin and Helpers
Services subclass PermissionsMixin and set a domain:
class MyService(PermissionsMixin):
domain = PermissionDomain.THREAD
async def do_thing(self, *, user):
await self.has_perms(user, Operation.VIEW_THREAD, obj, raise_on_deny=True)has_perms(user, operation, obj=None): the check used by the built-in services;raise_on_deny=Falsereturns a boolean instead of raising.get_object_permissions_map(user, obj): returns{"read": ..., "write": ..., "manage": ...}from the tiered classes.has_queryset_perms(user, operation, queryset): runsget_queryset_permsacross the domain chain.
The low-level check_permissions() / check_object_permissions() / has_perms() free functions power all of the above and are what you’d use outside a mixin.