fix(hermes): report consumer access denial

This commit is contained in:
jenkins 2026-08-02 17:00:40 -03:00
parent 4fe6dba491
commit fa21a760ef
3 changed files with 65 additions and 27 deletions

View File

@ -39,38 +39,76 @@ source = source.replace(attachBefore, attachAfter);
fs.writeFileSync(path, source); fs.writeFileSync(path, source);
NODE NODE
# The upstream self-hosted OIDC plugin authenticates users but deliberately # The upstream OIDC gate authenticates users but deliberately treats the
# treats the dashboard as one shared workstation. Allow a deployment to narrow # dashboard as one shared workstation. Allow a deployment to narrow that
# that workstation to an explicit OIDC subject without changing default # workstation to explicit OIDC subjects. Enforce this after normal provider
# behavior for the operator instance. # verification so a denied account is a 403, not a misleading provider 503.
RUN python - <<'PY' RUN python - <<'PY'
from pathlib import Path 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() source = path.read_text()
before = ''' if not user_id: helper_before = '''def _client_ip(request: Request) -> str:
raise ProviderError("ID token missing 'sub' (user_id) claim") 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: helper_after = helper_before + '''def _dashboard_user_allowed(session) -> bool:
raise ProviderError("ID token missing 'sub' (user_id) claim") """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: refresh_before = ''' new_session, refreshing_provider = refreshed
raise SystemExit("Hermes self-hosted OIDC patch context changed") request.state.session = new_session
path.write_text(source.replace(before, after)) 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 PY
COPY dockerfiles/hermes-session-migrate.py /opt/hermes/bin/hermes-session-migrate 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 'if (unmounting) return;' src/pages/ChatPage.tsx \
&& grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \ && grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \
&& grep -Fq 'HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS' \ && 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 && chmod 0755 /opt/hermes/bin/hermes-session-migrate

View File

@ -77,7 +77,7 @@ spec:
- rpi4 - rpi4
initContainers: initContainers:
- name: migrate-user-sessions - name: migrate-user-sessions
image: registry.bstein.dev/bstein/hermes-agent@sha256:ba3e81a1cefcc178729891a7af31269e65b94a72d880f0cb144d3ddbbceaeaba image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- /opt/hermes/bin/hermes-session-migrate - /opt/hermes/bin/hermes-session-migrate
@ -174,7 +174,7 @@ spec:
memory: 64Mi memory: 64Mi
containers: containers:
- name: hermes-chat - name: hermes-chat
image: registry.bstein.dev/bstein/hermes-agent@sha256:ba3e81a1cefcc178729891a7af31269e65b94a72d880f0cb144d3ddbbceaeaba image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
args: args:
- gateway - gateway

View File

@ -134,7 +134,7 @@ spec:
memory: 64Mi memory: 64Mi
containers: containers:
- name: hermes - name: hermes
image: registry.bstein.dev/bstein/hermes-agent@sha256:ba3e81a1cefcc178729891a7af31269e65b94a72d880f0cb144d3ddbbceaeaba image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
args: args:
- gateway - gateway