bytedance/deer-flow · error · HTTPException
Permission denied: {resource}:{action}
Error message
Permission denied: {resource}:{action} What it means
HTTP 403 from the require_permission decorator when the caller is authenticated but lacks the '{resource}:{action}' permission in their role's permission set. Permissions are additive strings on AuthContext (authz.py's has_permission does a set membership test), derived from the user's system role. This is authorization failure, not authentication failure.
Source
Thrown at backend/app/gateway/authz.py:478
kwargs["request"] = _make_test_request_stub()
else:
return await func(*args, **kwargs)
request = kwargs["request"]
if getattr(request, "_deerflow_test_bypass_auth", False):
return await func(*args, **kwargs)
auth: AuthContext = getattr(request.state, "auth", None)
if auth is None:
auth = await _authenticate(request)
request.state.auth = auth
if not auth.is_authenticated:
raise HTTPException(status_code=401, detail="Authentication required")
# Check permission
if not auth.has_permission(resource, action):
raise HTTPException(
status_code=403,
detail=f"Permission denied: {resource}:{action}",
)
# Owner check for thread-specific resources.
#
# 2.0-rc moved thread metadata into the SQL persistence layer
# (``threads_meta`` table). We verify ownership via
# ``ThreadMetaStore.check_access``: it returns True for
# missing rows (untracked legacy thread) and for rows whose
# ``user_id`` is NULL (shared / pre-auth data), so this is
# strict-deny rather than strict-allow — only an *existing*
# row with a *different* user_id triggers 404.
if owner_check:
from app.gateway.internal_auth import INTERNAL_OWNER_USER_ID_HEADER_NAME, INTERNAL_SYSTEM_ROLE
thread_id = kwargs.get("thread_id")
if thread_id is None:View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Elevate the user's system_role to a role that holds the permission (e.g. admin)
- Grant the missing resource:action permission to the role in the permission map
- Have the client stop calling the endpoint it is not authorized for
Defensive patterns
Strategy: validation
Validate before calling
# Hide/disable UI actions the role cannot perform
perms = await fetch_my_permissions() # e.g. from /api/me
if "threads:delete" not in perms:
disable_delete_button() Type guard
def can(ctx: AuthContext, resource: str, action: str) -> bool:
"""True when require_permission(resource, action) would pass its 403 check."""
return ctx.is_authenticated and ctx.has_permission(resource, action) Try / catch
try:
await client.post(admin_url, headers=headers)
except HTTPStatusError as e:
if e.response.status_code == 403 and "Permission denied" in e.response.text:
show_forbidden(e.response.text) # tells the user exactly which resource:action
else:
raise Prevention
- Mirror the server permission map in the frontend to hide impossible actions
- When adding endpoints, grant the permission to the roles that need it in the same change
- Include the resource:action string in bug reports — it names the missing grant
When it happens
Trigger: A 'user'-role account calling an admin-only endpoint (e.g. users:write, admin resource); a role whose _ALL_PERMISSIONS entry was never granted; disabling a permission in the role map.
Common situations: Non-admin users hitting admin UI actions; role definitions edited in config; new endpoints shipped with a permission the default role does not include; tests running with a token minted for a low-privilege user.
Related errors
- Authentication required
- Thread {thread_id} not found
- {detail}
- Failed to start Lark authorization.
- Failed to read JWT secret from {secret_file}. Set AUTH_JWT_S
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/4a950df2adc07835.
Report an issue: GitHub.