{"record":{"id":"b68a629fb7b7ae82","repo":"odysseus-dev/odysseus","slug":"admin-only","errorCode":null,"errorMessage":"Admin only","messagePattern":"Admin only","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"critical","filePath":"core/middleware.py","lineNumber":53,"sourceCode":"    \"\"\"\n    # In-process bypass for tool-layer loopback calls. Two paths:\n    # (a) header-direct (caller set X-Odysseus-Internal-Token), or\n    # (b) the auth middleware already validated the token and stamped\n    #     request.state.current_user = \"internal-tool\".\n    try:\n        hdr = request.headers.get(INTERNAL_TOOL_HEADER)\n        if hdr and secrets.compare_digest(hdr, INTERNAL_TOOL_TOKEN):\n            return\n        if getattr(request.state, \"current_user\", None) == INTERNAL_TOOL_USER:\n            return\n    except Exception:\n        pass\n\n    auth_mgr = getattr(request.app.state, \"auth_manager\", None)\n    if os.getenv(\"AUTH_ENABLED\", \"true\").lower() == \"false\":\n        return\n    if not auth_mgr or not auth_mgr.is_configured:\n        raise HTTPException(403, \"Admin only\")\n    user = getattr(request.state, \"current_user\", None)\n    if not user or not auth_mgr.is_admin(user):\n        raise HTTPException(403, \"Admin only\")\n\n\nclass SecurityHeadersMiddleware(BaseHTTPMiddleware):\n    \"\"\"Add standard security headers to all responses.\"\"\"\n\n    async def dispatch(self, request: Request, call_next) -> Response:\n        # Generate a per-request nonce for inline scripts\n        nonce = secrets.token_hex(16)\n        request.state.csp_nonce = nonce\n\n        response = await call_next(request)\n        path = request.url.path\n\n        # Tool render endpoints\n        is_tool_render = path.startswith(\"/api/tools/\") and path.endswith(\"/render\")","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/core/middleware.py#L35-L71","documentation":"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.","triggerScenarios":"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.","commonSituations":"Fresh deployment where setup was not completed; auth database/config missing or unreadable; app mounted without the auth middleware wiring app.state.auth_manager.","solutions":["Complete first-run setup so the auth manager is configured with an admin account","Verify app.state.auth_manager is set and is_configured is true at startup","For internal tooling, send the INTERNAL_TOOL_HEADER with the correct token (constant-time compared)","As a last resort in trusted environments, set AUTH_ENABLED=false"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"auth_mgr = getattr(app.state, 'auth_manager', None)\nif os.getenv('AUTH_ENABLED', 'true').lower() != 'false' and (not auth_mgr or not auth_mgr.is_configured):\n    fail_startup('Complete setup or configure the auth manager before serving admin routes')","typeGuard":null,"tryCatchPattern":"from fastapi import HTTPException\ntry:\n    admin_guard(request)\nexcept HTTPException as e:\n    if e.status_code == 403 and not app.state.auth_manager.is_configured:\n        redirect('/setup')  # guide to first-run setup\n    raise","preventionTips":["Fail fast at startup when auth is enabled but unconfigured","Complete first-run setup before exposing the app","Use the internal-tool header for trusted automation instead of disabling auth"],"tags":["authentication","middleware","authorization","configuration"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}