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

  1. Elevate the user's system_role to a role that holds the permission (e.g. admin)
  2. Grant the missing resource:action permission to the role in the permission map
  3. 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

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


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/4a950df2adc07835. Report an issue: GitHub.