{"record":{"id":"4a950df2adc07835","repo":"bytedance/deer-flow","slug":"permission-denied-resource-action","errorCode":null,"errorMessage":"Permission denied: {resource}:{action}","messagePattern":"Permission denied: (.+?):(.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"backend/app/gateway/authz.py","lineNumber":478,"sourceCode":"                    kwargs[\"request\"] = _make_test_request_stub()\n                else:\n                    return await func(*args, **kwargs)\n                request = kwargs[\"request\"]\n\n            if getattr(request, \"_deerflow_test_bypass_auth\", False):\n                return await func(*args, **kwargs)\n\n            auth: AuthContext = getattr(request.state, \"auth\", None)\n            if auth is None:\n                auth = await _authenticate(request)\n                request.state.auth = auth\n\n            if not auth.is_authenticated:\n                raise HTTPException(status_code=401, detail=\"Authentication required\")\n\n            # Check permission\n            if not auth.has_permission(resource, action):\n                raise HTTPException(\n                    status_code=403,\n                    detail=f\"Permission denied: {resource}:{action}\",\n                )\n\n            # Owner check for thread-specific resources.\n            #\n            # 2.0-rc moved thread metadata into the SQL persistence layer\n            # (``threads_meta`` table). We verify ownership via\n            # ``ThreadMetaStore.check_access``: it returns True for\n            # missing rows (untracked legacy thread) and for rows whose\n            # ``user_id`` is NULL (shared / pre-auth data), so this is\n            # strict-deny rather than strict-allow — only an *existing*\n            # row with a *different* user_id triggers 404.\n            if owner_check:\n                from app.gateway.internal_auth import INTERNAL_OWNER_USER_ID_HEADER_NAME, INTERNAL_SYSTEM_ROLE\n\n                thread_id = kwargs.get(\"thread_id\")\n                if thread_id is None:","sourceCodeStart":460,"sourceCodeEnd":496,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/authz.py#L460-L496","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"# Hide/disable UI actions the role cannot perform\nperms = await fetch_my_permissions()  # e.g. from /api/me\nif \"threads:delete\" not in perms:\n    disable_delete_button()","typeGuard":"def can(ctx: AuthContext, resource: str, action: str) -> bool:\n    \"\"\"True when require_permission(resource, action) would pass its 403 check.\"\"\"\n    return ctx.is_authenticated and ctx.has_permission(resource, action)","tryCatchPattern":"try:\n    await client.post(admin_url, headers=headers)\nexcept HTTPStatusError as e:\n    if e.response.status_code == 403 and \"Permission denied\" in e.response.text:\n        show_forbidden(e.response.text)  # tells the user exactly which resource:action\n    else:\n        raise","preventionTips":["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"],"tags":["auth","http-403","authorization","permissions","rbac","gateway"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}