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
- Log in as an account with is_admin=true, then retry.
- 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.
- Grant admin to a legitimate account via PUT /users/{username}/privileges from an existing admin session.
- 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
- Gate the admin UI on a verified is_admin flag, not on username guesses.
- Ensure first-run setup creates at least one admin.
- Re-check privileges after PUT /users/{username}/privileges changes.
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
- Authentication required
- Action '{action}' requires admin privileges
- This endpoint requires an API token
- HTTP ' + r.status
- Unknown wipe kind: {kind!r}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/9d4fc8f4a3ea208c.
Report an issue: GitHub.