{"record":{"id":"481ae864638852b7","repo":"odysseus-dev/odysseus","slug":"action-action-requires-admin-privileges","errorCode":null,"errorMessage":"Action '{action}' requires admin privileges","messagePattern":"Action '(.+?)' requires admin privileges","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"routes/task_routes.py","lineNumber":435,"sourceCode":"                        )\n                    resumed += 1\n                db.commit()\n            finally:\n                db.close()\n        return {\"ok\": True, \"opened\": True, \"enabled\": bool(prefs.get(\"tasks_enabled\")), \"resumed\": resumed}\n\n    # Actions that execute shell/SSH commands or cross into admin-only\n    # Cookbook serving surfaces — restricted to admins.\n    # Non-admin users cannot create tasks with these action types via the\n    # API. See review CRIT-C.\n    _ADMIN_ONLY_ACTIONS = ADMIN_ONLY_TASK_ACTIONS\n\n    def _is_admin(user: str | None) -> bool:\n        return owner_has_admin_task_privileges(user)\n\n    def _require_admin_for_task_action(user: str | None, task_type: str | None, action: str | None) -> None:\n        if is_admin_only_task_action(task_type, action) and not _is_admin(user):\n            raise HTTPException(403, f\"Action '{action}' requires admin privileges\")\n\n    def _validate_then_task_id(db, then_task_id: Optional[str], user: Optional[str], current_task_id: Optional[str] = None) -> Optional[str]:\n        target_id = (then_task_id or \"\").strip()\n        if not target_id:\n            return None\n        if current_task_id and target_id == current_task_id:\n            raise HTTPException(400, \"Task cannot chain to itself\")\n        q = db.query(ScheduledTask).filter(ScheduledTask.id == target_id)\n        if user:\n            q = q.filter(ScheduledTask.owner == user)\n        target = q.first()\n        if not target:\n            raise HTTPException(404, \"Chained task not found\")\n        return target.id\n\n    @router.post(\"\")\n    async def create_task(request: Request, req: TaskCreate):\n        user = _owner(request)","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L417-L453","documentation":"Raised by the task-creation API when a task of type 'action' names an action classified in ADMIN_ONLY_TASK_ACTIONS (actions that run shell/SSH commands or serve cookbook surfaces) and the requesting owner does not pass owner_has_admin_task_privileges(user). It is a deliberate authorization boundary (review item CRIT-C) returning HTTP 403.","triggerScenarios":"POST /api/tasks (or PUT update) with task_type='action' and action in the admin-only set (e.g. action_run_local, ssh_command, run_script) while authenticated as a non-admin owner; the check _require_admin_for_task_action fires before schedule validation.","commonSituations":"Self-hosted multi-user deployment where a regular user clones an admin's task; frontend offers all BUILTIN_ACTION_INFO entries in the picker without filtering by role; upgrading to a version that newly classifies an action as admin-only and previously-created task edits now fail.","solutions":["Run the task under an owner that has admin task privileges, or ask the instance admin to create/own the task.","Filter the client-side action list to exclude ADMIN_ONLY_TASK_ACTIONS for non-admin users so the option is never submitted.","If the deployment is single-user and the owner was wrongly detected, verify how _owner(request) derives the user and that the admin privilege list (owner_has_admin_task_privileges) includes that owner.","If policy allows it, have the admin implement the capability as a safer non-admin action instead of relaxing the check."],"exampleFix":"# before\nactions = list(BUILTIN_ACTION_INFO.keys())\n\n# after\nfrom src.task_actions import ADMIN_ONLY_TASK_ACTIONS\nis_admin = current_user_has_admin_privileges\nactions = [a for a in BUILTIN_ACTION_INFO\n           if is_admin or a not in ADMIN_ONLY_TASK_ACTIONS]","handlingStrategy":"validation","validationCode":"const isAdmin = await api.getMe().then(u => u?.has_admin_task_privileges ?? false);\nfunction canCreateAction(action, isAdmin) {\n  const ADMIN_ONLY = new Set(['action_run_local', 'ssh_command', 'run_script']); // keep in sync with ADMIN_ONLY_TASK_ACTIONS\n  return isAdmin || !ADMIN_ONLY.has(action);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.createTask(payload);\n} catch (e) {\n  if (e.status === 403 && /admin privileges/.test(e.message)) {\n    showInfo('This action type is restricted to admins.');\n    return;\n  }\n  throw e;\n}","preventionTips":["Fetch ADMIN_ONLY_TASK_ACTIONS from the server (or a /me privileges flag) and filter the action picker per role.","Never rely on hiding the option alone — enforce server-side, which this route already does.","In e2e tests, assert a non-admin token gets 403 for each admin-only action."],"tags":["authorization","http-403","tasks","rbac","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}