odysseus-dev/odysseus · error · HTTPException

Admin only

Error message

Admin only

What it means

Raised as HTTP 403 by GET /users when _get_current_user(request) returns falsy OR auth_manager.is_admin(user) is false. Note the route conflates 'no session' and 'not admin' into one 403 'Admin only' (other routes use 401 for unauthenticated). User listing exposes account data, so it's restricted to administrators.

Source

Thrown at routes/auth_routes.py:278

            raise HTTPException(401, "Not authenticated")
        if not auth_manager.totp_disable(user, body.password):
            raise HTTPException(400, "Invalid password")
        return {"ok": True}

    @router.get("/2fa/status")
    async def totp_status(request: Request):
        """Check if 2FA is enabled for the current user."""
        user = _get_current_user(request)
        if not user:
            raise HTTPException(401, "Not authenticated")
        return {"enabled": auth_manager.totp_enabled(user)}

    # Admin-only routes
    @router.get("/users")
    async def list_users(request: Request):
        user = _get_current_user(request)
        if not user or not auth_manager.is_admin(user):
            raise HTTPException(403, "Admin only")
        return {"users": auth_manager.list_users()}

    @router.post("/users")
    async def admin_create_user(body: CreateUserRequest, request: Request):
        user = _get_current_user(request)
        if not user or not auth_manager.is_admin(user):
            raise HTTPException(403, "Admin only")
        if len(body.password) < PASSWORD_MIN_LENGTH:
            raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
        if len(body.username.strip()) < 1:
            raise HTTPException(400, "Username is required")
        if body.username.lower() in RESERVED_USERNAMES:
            raise HTTPException(403, "Username is reserved")
        ok = auth_manager.create_user(body.username, body.password, body.is_admin)
        if not ok:
            raise HTTPException(409, "Username already taken")
        return {"ok": True}

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Log in as an account with is_admin=true, then retry.
  2. If no admin exists yet, create/promote one: use setup to bootstrap the initial admin, or have the operator set is_admin directly in the user store.
  3. Grant admin to a legitimate account via PUT /users/{username}/privileges from an existing admin session.
  4. Fix clients to distinguish this 403 from other failures and surface 'admin required' to the operator.
Defensive patterns

Strategy: type-guard

Validate before calling

// Verify admin privileges before calling admin endpoints
const me = await (await fetch('/me', {credentials:'include'})).json(); // or session info endpoint
if (!me?.is_admin) { showError('This view requires an admin account'); return; }

Type guard

function isAdminSession(session) {
  return session != null && session.user != null && session.user.is_admin === true;
}

Try / catch

catch (e) { if (e.status === 403 && /admin only/i.test(e.message)) hideAdminUiAndWarn(); }

Prevention

When it happens

Trigger: Calling GET /users while logged in as a non-admin user, with no/expired session, or with a session whose user lost admin privileges via PUT /users/{username}/privileges.

Common situations: First-run deployments where the only account isn't admin yet (admin flag never set on the initial user), privilege revoked while a tab stayed open, or scripts reusing a non-admin session cookie.

Related errors


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