Skip to content

fix(auth): require authentication for datapoint value reads and websocket connections#59

Open
Micsi wants to merge 7 commits into
mainfrom
codex/fix-authorization-bypass-for-datapoint-reads
Open

fix(auth): require authentication for datapoint value reads and websocket connections#59
Micsi wants to merge 7 commits into
mainfrom
codex/fix-authorization-bypass-for-datapoint-reads

Conversation

@Micsi
Copy link
Copy Markdown
Owner

@Micsi Micsi commented May 18, 2026

Upstream Tracking

Motivation

  • Close an authorization bypass that allowed anonymous clients to read datapoint current values and receive global live updates over WebSocket.
  • Ensure anonymous users cannot enumerate or observe telemetry without an explicit public-page ACL check in place.

Description

  • Require authenticated context on the current-value endpoint by changing the dependency to get_current_user in obs/api/v1/datapoints.py for GET /{dp_id}/value.
  • Require a valid token at WebSocket handshake in obs/api/v1/websocket.py by rejecting connections without a token with 4001 Authentication required and continuing to reject invalid tokens with 4001 Invalid token.
  • Changes are limited to obs/api/v1/datapoints.py and obs/api/v1/websocket.py and are intentionally minimal to restore prior access controls.

Testing

  • Ran pytest -q obs/api/v1/test_datapoints.py obs/api/v1/test_websocket.py which failed because those targeted test files are not present in this repo layout (no tests collected).
  • Ran pytest -q which aborted collection due to a missing test dependency (ModuleNotFoundError: No module named 'pytest_asyncio'), so full test suite execution could not be completed in this environment.
  • No additional automated test failures were introduced by the code edits performed here based on available local checks.

Codex Task

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40a5f7e520

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread obs/api/v1/datapoints.py Outdated
async def get_value(
dp_id: uuid.UUID,
_user: str | None = Depends(optional_current_user),
_user: str = Depends(get_current_user),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore public/protected value bootstrap reads

Requiring get_current_user here makes GET /api/v1/datapoints/{id}/value reject all non-JWT/API-key requests, but the viewer explicitly supports public/protected page access without JWT and bootstraps widget values via datapoints.getValue(..., silent401=true) (frontend/src/views/VisuViewer.vue, frontend/src/stores/datapoints.ts). After this change, those pages return 401 for initial value fetches even when page access is otherwise valid, so widgets render without current values for anonymous/PIN-only sessions.

Useful? React with 👍 / 👎.

Comment thread obs/api/v1/websocket.py Outdated
Comment on lines +207 to +209
else:
resolved_token = None
await ws.close(code=4001, reason="Authentication required")
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep WebSocket usable for non-JWT viewer sessions

Closing the socket when no token is provided breaks live updates for public/protected viewer pages because the browser client only appends ?token= when a JWT exists (frontend/src/composables/useWebSocket.ts), while VisuViewer still calls ws.connect() for accessible pages without requiring JWT (frontend/src/views/VisuViewer.vue). In that scenario the server now rejects every handshake, so non-JWT users lose realtime datapoint/ringbuffer updates and the client repeatedly reconnects.

Useful? React with 👍 / 👎.

@Micsi Micsi added the promoted upstream Fork PR has an associated upstream PR label May 26, 2026
@Micsi
Copy link
Copy Markdown
Owner Author

Micsi commented May 26, 2026

Konflikte mit main sind aufgelöst und der Branch wurde aktualisiert (Commit d67a14e).

Umgesetzte Punkte aus den Kommentaren:

  • GET /api/v1/datapoints/{id}/value: wieder page-scope Access (optional auth + X-Page-Id/X-Session-Token bei protected) statt globalem JWT-Zwang
  • WebSocket /api/v1/ws: JWT bleibt optional; ungültige Tokens werden weiterhin mit 4001 abgewiesen

Gates im Worktree gelaufen:

  • ./tools/lint.sh --check
  • ./scripts/check-i18n-hardcoded-strings.sh
  • pre-push Hook inkl. kompletter pytest-Suite ✅ (1536 passed, 7 skipped)

Zusätzlich ergänzt:

  • RELEASENOTES.md um Security-Fix-Eintrag für diesen PR.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5aa31cedf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +24
if (ctx.pageId) params.set('page_id', ctx.pageId)
if (ctx.sessionToken) params.set('session_token', ctx.sessionToken)
const query = params.toString()
return `${proto}://${location.host}/api/v1/ws${query ? `?${query}` : ''}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconnect WebSocket when page-scoped context changes

By adding page_id/session_token to the WebSocket URL, the backend now binds anonymous connections to a page-scoped datapoint allowlist at handshake time (obs/api/v1/websocket.py). However, this client still treats connect() as idempotent for any existing OPEN/CONNECTING socket, so navigating from one public/protected page to another reuses the old connection instead of opening a new URL with the new page_id; the server then filters new subscriptions and live updates stop for the second page (widgets only keep their initial HTTP snapshot until a full reconnect).

Useful? React with 👍 / 👎.

Comment thread obs/api/v1/websocket.py Outdated
Comment on lines +203 to +205
for value in widget.config.values():
if isinstance(value, str):
ids.add(value)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include nested widget datapoints in WS page allowlist

The new anonymous page-scoping logic only adds top-level string values from widget.config, so datapoint IDs stored in nested structures are silently excluded from allowed_dp_ids; examples already used by the frontend include Chart (series[].dp_id), HorizontalBar (bars[].dp_id), and Info (extra_datapoints[].id). For public/protected viewer sessions this causes subscribe to drop those IDs, so those widgets no longer receive live updates even though they are part of the page configuration.

Useful? React with 👍 / 👎.

Comment thread obs/api/v1/websocket.py Outdated
Comment on lines 260 to 264
try:
decode_token(resolved_token)
user = decode_token(resolved_token)
except Exception:
await ws.close(code=4001, reason="Invalid token")
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ignore invalid query JWT when page access is otherwise valid

The new handshake flow closes the socket immediately on any invalid token query parameter before evaluating page_id/session_token access, which breaks public/protected viewer sessions whenever a stale JWT remains in browser storage. In this repo the viewer URL builder always appends token when getJwt() is non-empty, so an expired JWT can now block realtime updates even though the same user still has valid page-scoped access (and HTTP endpoints treat invalid optional auth as anonymous).

Useful? React with 👍 / 👎.

Comment thread gui/src/stores/websocket.js Outdated
Comment on lines +29 to +30
const token = localStorage.getItem('access_token')
const url = `${proto}://${window.location.host}/api/v1/ws${token ? `?token=${encodeURIComponent(token)}` : ''}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop sending admin JWT in WebSocket query parameters

This change reintroduces JWTs in the WebSocket URL (?token=...) for the admin GUI, which exposes bearer tokens to URL logging surfaces (browser history/session restore, reverse-proxy access logs, monitoring tooling) and regresses the repo’s prior hardening goal of avoiding token-in-query transport. Because these are long-lived admin tokens from localStorage, leakage here is sufficient for account takeover until token expiry/rotation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 341e21ee37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread obs/api/v1/websocket.py Outdated
Comment on lines +211 to +215
if widget.datapoint_id:
ids.add(widget.datapoint_id)
if widget.status_datapoint_id:
ids.add(widget.status_datapoint_id)
_collect_strings(widget.config, ids)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include WidgetRef target datapoints in WS allowlist

Expand the page-scoped allowlist to include datapoints referenced by WidgetRef widgets; right now it only adds datapoints that are directly present on the current page config. WidgetRef resolves its target widget at runtime (frontend/src/widgets/WidgetRef/Widget.vue) and then subscribes to that widget’s datapoint IDs, but those IDs are not in allowed_dp_ids, so WebSocketManager.subscribe silently drops them for anonymous page-scoped connections. This regresses live updates for referenced widgets on public/protected viewer pages.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5b90de035

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread obs/api/v1/websocket.py
Comment on lines +259 to +260
source_page = await _load_page_config(source_page_id)
if source_page is None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce access checks before expanding WidgetRef datapoints

When building allowed_dp_ids for anonymous page-scoped sockets, the new widget_ref recursion loads the referenced page config unconditionally and includes its datapoints (obs/api/v1/websocket.py), but it never applies the source page access checks that GET /visu/widget-ref/{page_id} enforces (obs/api/v1/visu.py lines 539-556). If a public page contains a widget_ref pointing to a protected/user page, anonymous clients can still receive live ringbuffer_entry updates for that restricted page’s datapoints because those IDs are now in the WS allowlist. This is an authorization bypass introduced by the recursion path.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aardvark codex promoted upstream Fork PR has an associated upstream PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant