odysseus-dev/odysseus · critical · HTTPException

Admin only

Error message

Admin only

What it means

HTTP 403 from the admin middleware. After internal-tool bypasses fail, when AUTH_ENABLED is not 'false' and the app has no configured auth_manager (or it reports is_configured false), every request hitting this middleware is rejected with 'Admin only' because there is no way to establish an admin user.

Source

Thrown at core/middleware.py:53

    """
    # In-process bypass for tool-layer loopback calls. Two paths:
    # (a) header-direct (caller set X-Odysseus-Internal-Token), or
    # (b) the auth middleware already validated the token and stamped
    #     request.state.current_user = "internal-tool".
    try:
        hdr = request.headers.get(INTERNAL_TOOL_HEADER)
        if hdr and secrets.compare_digest(hdr, INTERNAL_TOOL_TOKEN):
            return
        if getattr(request.state, "current_user", None) == INTERNAL_TOOL_USER:
            return
    except Exception:
        pass

    auth_mgr = getattr(request.app.state, "auth_manager", None)
    if os.getenv("AUTH_ENABLED", "true").lower() == "false":
        return
    if not auth_mgr or not auth_mgr.is_configured:
        raise HTTPException(403, "Admin only")
    user = getattr(request.state, "current_user", None)
    if not user or not auth_mgr.is_admin(user):
        raise HTTPException(403, "Admin only")


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    """Add standard security headers to all responses."""

    async def dispatch(self, request: Request, call_next) -> Response:
        # Generate a per-request nonce for inline scripts
        nonce = secrets.token_hex(16)
        request.state.csp_nonce = nonce

        response = await call_next(request)
        path = request.url.path

        # Tool render endpoints
        is_tool_render = path.startswith("/api/tools/") and path.endswith("/render")

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Complete first-run setup so the auth manager is configured with an admin account
  2. Verify app.state.auth_manager is set and is_configured is true at startup
  3. For internal tooling, send the INTERNAL_TOOL_HEADER with the correct token (constant-time compared)
  4. As a last resort in trusted environments, set AUTH_ENABLED=false
Defensive patterns

Strategy: validation

Validate before calling

auth_mgr = getattr(app.state, 'auth_manager', None)
if os.getenv('AUTH_ENABLED', 'true').lower() != 'false' and (not auth_mgr or not auth_mgr.is_configured):
    fail_startup('Complete setup or configure the auth manager before serving admin routes')

Try / catch

from fastapi import HTTPException
try:
    admin_guard(request)
except HTTPException as e:
    if e.status_code == 403 and not app.state.auth_manager.is_configured:
        redirect('/setup')  # guide to first-run setup
    raise

Prevention

When it happens

Trigger: AUTH_ENABLED unset/true (default) while the auth manager was never initialized — e.g. first boot without a created account, or auth state failed to load — and an internal-tool header/token is not supplied.

Common situations: Fresh deployment where setup was not completed; auth database/config missing or unreadable; app mounted without the auth middleware wiring app.state.auth_manager.

Related errors


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