{"record":{"id":"df2f28d235a936c8","repo":"odysseus-dev/odysseus","slug":"access-denied","errorCode":null,"errorMessage":"Access denied","messagePattern":"Access denied","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"routes/task_routes.py","lineNumber":582,"sourceCode":"        current user. Anonymous callers get nothing (prevents\n        cross-tenant drain — see review CRIT-B).\"\"\"\n        user = _owner(request)\n        if not user:\n            return {\"notifications\": []}\n        notes = task_scheduler.pop_notifications(owner=user)\n        return {\"notifications\": notes}\n\n    @router.post(\"/{task_id}/clear-cache\")\n    async def clear_task_cache(request: Request, task_id: str):\n        \"\"\"Clear derived cache for one built-in task.\"\"\"\n        user = _owner(request)\n        db = SessionLocal()\n        try:\n            task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()\n            if not task:\n                raise HTTPException(404, \"Task not found\")\n            if user and task.owner != user:\n                raise HTTPException(403, \"Access denied\")\n            action = task.action or \"\"\n        finally:\n            db.close()\n\n        cache_tables = {\n            \"summarize_emails\": (\"email_summaries\",),\n            \"draft_email_replies\": (\"email_ai_replies\",),\n            \"email_auto_translate\": (\"email_translations\",),\n            \"extract_email_events\": (\"email_calendar_extractions\",),\n            \"learn_sender_signatures\": (\"sender_signatures\",),\n            \"check_email_urgency\": (\"email_tags\", \"email_urgency_alerts\"),\n        }\n        tables = cache_tables.get(action)\n        if not tables:\n            raise HTTPException(400, \"This task has no clearable cache\")\n\n        import sqlite3\n        from pathlib import Path","sourceCodeStart":564,"sourceCodeEnd":600,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L564-L600","documentation":"Ownership guard on clear-cache: the task exists but task.owner differs from the requesting user, so the cache-clear is refused with HTTP 403. Note the check is 'if user and task.owner != user' — when _owner(request) yields None/anonymous, the guard is skipped, so this fires only for authenticated non-owner users.","triggerScenarios":"POST /api/tasks/{id}/clear-cache where id belongs to another owner and the request carries an identifying owner (header/session that _owner resolves to a non-null user).","commonSituations":"Multi-user instance where cached email-derived tables are per-owner; admin-owned built-in tasks being cache-cleared by a regular account; account switch in the UI leaving an old task id selected.","solutions":["Clear caches only on tasks you own.","Scope the client UI so users only see/can act on their own tasks.","If an admin must clear another user's cache, use an admin-capable surface rather than this owner-checked route."],"exampleFix":"// before\nawait api.clearCache(taskIdFromUrl);\n\n// after\nconst task = await api.getTask(taskIdFromUrl);\nif (task.owner !== currentUser) throw new Error('Not your task');\nawait api.clearCache(taskIdFromUrl);","handlingStrategy":"validation","validationCode":"const task = await api.getTask(taskId);\nif (task.owner !== currentUser) throw new Error('Not your task — cannot clear cache');\nawait api.clearCache(taskId);","typeGuard":null,"tryCatchPattern":"try { await api.clearCache(taskId); }\ncatch (e) {\n  if (e.status === 403) { showNotice('You can only clear your own task caches'); return; }\n  throw e;\n}","preventionTips":["Restrict cache-clear UI to tasks where row.owner === current user.","Refresh identity context (account switcher) together with the task list."],"tags":["authorization","http-403","tasks","cache","multi-tenant"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}