odysseus-dev/odysseus · error · HTTPException

Access denied

Error message

Access denied

What it means

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.

Source

Thrown at routes/task_routes.py:582

        current user. Anonymous callers get nothing (prevents
        cross-tenant drain — see review CRIT-B)."""
        user = _owner(request)
        if not user:
            return {"notifications": []}
        notes = task_scheduler.pop_notifications(owner=user)
        return {"notifications": notes}

    @router.post("/{task_id}/clear-cache")
    async def clear_task_cache(request: Request, task_id: str):
        """Clear derived cache for one built-in task."""
        user = _owner(request)
        db = SessionLocal()
        try:
            task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
            if not task:
                raise HTTPException(404, "Task not found")
            if user and task.owner != user:
                raise HTTPException(403, "Access denied")
            action = task.action or ""
        finally:
            db.close()

        cache_tables = {
            "summarize_emails": ("email_summaries",),
            "draft_email_replies": ("email_ai_replies",),
            "email_auto_translate": ("email_translations",),
            "extract_email_events": ("email_calendar_extractions",),
            "learn_sender_signatures": ("sender_signatures",),
            "check_email_urgency": ("email_tags", "email_urgency_alerts"),
        }
        tables = cache_tables.get(action)
        if not tables:
            raise HTTPException(400, "This task has no clearable cache")

        import sqlite3
        from pathlib import Path

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Clear caches only on tasks you own.
  2. Scope the client UI so users only see/can act on their own tasks.
  3. If an admin must clear another user's cache, use an admin-capable surface rather than this owner-checked route.

Example fix

// before
await api.clearCache(taskIdFromUrl);

// after
const task = await api.getTask(taskIdFromUrl);
if (task.owner !== currentUser) throw new Error('Not your task');
await api.clearCache(taskIdFromUrl);
Defensive patterns

Strategy: validation

Validate before calling

const task = await api.getTask(taskId);
if (task.owner !== currentUser) throw new Error('Not your task — cannot clear cache');
await api.clearCache(taskId);

Try / catch

try { await api.clearCache(taskId); }
catch (e) {
  if (e.status === 403) { showNotice('You can only clear your own task caches'); return; }
  throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/df2f28d235a936c8. Report an issue: GitHub.