From fa21a760efbe5b6b821dc9c2e4ec4aec0c10acc2 Mon Sep 17 00:00:00 2001 From: jenkins Date: Sun, 2 Aug 2026 17:00:40 -0300 Subject: [PATCH] fix(hermes): report consumer access denial --- dockerfiles/Dockerfile.hermes-agent | 86 ++++++++++++++++++++-------- services/hermes-chat/deployment.yaml | 4 +- services/hermes/deployment.yaml | 2 +- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/dockerfiles/Dockerfile.hermes-agent b/dockerfiles/Dockerfile.hermes-agent index 48c016dce..611bcbb54 100644 --- a/dockerfiles/Dockerfile.hermes-agent +++ b/dockerfiles/Dockerfile.hermes-agent @@ -39,38 +39,76 @@ source = source.replace(attachBefore, attachAfter); fs.writeFileSync(path, source); NODE -# The upstream self-hosted OIDC plugin authenticates users but deliberately -# treats the dashboard as one shared workstation. Allow a deployment to narrow -# that workstation to an explicit OIDC subject without changing default -# behavior for the operator instance. +# The upstream OIDC gate authenticates users but deliberately treats the +# dashboard as one shared workstation. Allow a deployment to narrow that +# workstation to explicit OIDC subjects. Enforce this after normal provider +# verification so a denied account is a 403, not a misleading provider 503. RUN python - <<'PY' from pathlib import Path -path = Path("/opt/hermes/plugins/dashboard_auth/self_hosted/__init__.py") +path = Path("/opt/hermes/hermes_cli/dashboard_auth/middleware.py") source = path.read_text() -before = ''' if not user_id: - raise ProviderError("ID token missing 'sub' (user_id) claim") +helper_before = '''def _client_ip(request: Request) -> str: + fwd = request.headers.get("x-forwarded-for", "") + if fwd: + return fwd.split(",")[0].strip() + return request.client.host if request.client else "" + - email = str(claims.get("email", "") or "") ''' -after = ''' if not user_id: - raise ProviderError("ID token missing 'sub' (user_id) claim") +helper_after = helper_before + '''def _dashboard_user_allowed(session) -> bool: + """Apply an optional deployment-level OIDC-subject allowlist.""" + import os + + allowed = { + value.strip() + for value in os.environ.get( + "HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS", "" + ).split(",") + if value.strip() + } + return not allowed or session.user_id in allowed + + +def _user_forbidden_response() -> Response: + """Return an authorization failure without exposing identities.""" + return JSONResponse( + { + "error": "forbidden", + "detail": "This Atlas account is not authorized for this dashboard.", + }, + status_code=403, + ) - allowed_user_ids = { - value.strip() - for value in os.environ.get( - "HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS", "" - ).split(",") - if value.strip() - } - if allowed_user_ids and user_id not in allowed_user_ids: - raise ProviderError("This account is not authorized for this dashboard") - email = str(claims.get("email", "") or "") ''' -if before not in source: - raise SystemExit("Hermes self-hosted OIDC patch context changed") -path.write_text(source.replace(before, after)) +refresh_before = ''' new_session, refreshing_provider = refreshed + request.state.session = new_session + response = await call_next(request) +''' +refresh_after = ''' new_session, refreshing_provider = refreshed + if not _dashboard_user_allowed(new_session): + return _user_forbidden_response() + request.state.session = new_session + response = await call_next(request) +''' +final_before = ''' request.state.session = session + return await call_next(request) +''' +final_after = ''' if not _dashboard_user_allowed(session): + return _user_forbidden_response() + request.state.session = session + return await call_next(request) +''' +for before, after, label in ( + (helper_before, helper_after, "allowlist helper"), + (refresh_before, refresh_after, "refreshed session"), + (final_before, final_after, "verified session"), +): + if before not in source: + raise SystemExit(f"Hermes dashboard auth {label} patch context changed") + source = source.replace(before, after, 1) +path.write_text(source) PY COPY dockerfiles/hermes-session-migrate.py /opt/hermes/bin/hermes-session-migrate @@ -80,5 +118,5 @@ RUN cd /opt/hermes/web \ && grep -Fq 'if (unmounting) return;' src/pages/ChatPage.tsx \ && grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \ && grep -Fq 'HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS' \ - /opt/hermes/plugins/dashboard_auth/self_hosted/__init__.py \ + /opt/hermes/hermes_cli/dashboard_auth/middleware.py \ && chmod 0755 /opt/hermes/bin/hermes-session-migrate diff --git a/services/hermes-chat/deployment.yaml b/services/hermes-chat/deployment.yaml index 60b417f49..ac97e8bb4 100644 --- a/services/hermes-chat/deployment.yaml +++ b/services/hermes-chat/deployment.yaml @@ -77,7 +77,7 @@ spec: - rpi4 initContainers: - name: migrate-user-sessions - image: registry.bstein.dev/bstein/hermes-agent@sha256:ba3e81a1cefcc178729891a7af31269e65b94a72d880f0cb144d3ddbbceaeaba + image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f imagePullPolicy: IfNotPresent command: - /opt/hermes/bin/hermes-session-migrate @@ -174,7 +174,7 @@ spec: memory: 64Mi containers: - name: hermes-chat - image: registry.bstein.dev/bstein/hermes-agent@sha256:ba3e81a1cefcc178729891a7af31269e65b94a72d880f0cb144d3ddbbceaeaba + image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f imagePullPolicy: IfNotPresent args: - gateway diff --git a/services/hermes/deployment.yaml b/services/hermes/deployment.yaml index aa3e5c283..74d141253 100644 --- a/services/hermes/deployment.yaml +++ b/services/hermes/deployment.yaml @@ -134,7 +134,7 @@ spec: memory: 64Mi containers: - name: hermes - image: registry.bstein.dev/bstein/hermes-agent@sha256:ba3e81a1cefcc178729891a7af31269e65b94a72d880f0cb144d3ddbbceaeaba + image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f imagePullPolicy: IfNotPresent args: - gateway