Skip to content

feat(web_viewer): split anonymous read access from an admin session - #300

Open
agessaman wants to merge 5 commits into
devfrom
fix/webviewer-admin-split
Open

agessaman wants to merge 5 commits into
devfrom
fix/webviewer-admin-split

Conversation

@agessaman

@agessaman agessaman commented Sep 23, 2026

Copy link
Copy Markdown
Owner

This supersedes #289. It keeps @hickey's admin privilege level, reworked on current dev, and he's credited as co-author on the main commit.

What changes when web_viewer_password is set

  • Dashboard, Contacts, and Mesh Graph are readable without logging in, along with the read-only APIs those pages use (contacts, contact detail, mesh nodes/edges/stats, dashboard summary/series/top/windows, health, banner status) and the two read-only POST helpers (/api/decode-path, /api/mesh/resolve-path).
  • Radio, Config, Logs, every mutating API, /api/channels, and the live Socket.IO streams require an admin session, since they can expose channel keys, decrypted traffic, and operator logs. The Realtime page still renders publicly but shows "Admin login required" instead of connecting.
  • Anything not on the public allowlist fails closed. /api/stream_data is exempt from the session check because the bot authenticates it with X-Stream-Token.
  • Login compares HMAC digests with hmac.compare_digest and clears the session before setting authenticated_admin. Logout clears the whole session and returns to the dashboard.
  • Admin-only controls (nav links, contact star/geocode/delete/bulk actions, and the zombie-recover and clear-offline banner buttons) are hidden from anonymous viewers. That's display only; the server enforces every one of them.

With no password configured, nothing changes: every page and API stays open as before.

Fixes on top of the #289 rework

  • Contacts declared its own top-level const IS_ADMIN after base.html had already declared it. Classic scripts share one global scope, so that threw a SyntaxError and the Contacts page never loaded, in every mode. It now uses the global from base.html.
  • The banner's recover and clear-offline buttons were still shown to anonymous viewers, and their endpoints now return 401. They're gated like the other admin controls.
  • Logging out left that login's live sockets open, and a socket keeps the session it connected with, so an open Realtime tab kept streaming after logout. Logout now disconnects every socket opened under that login; other admin logins stay connected.
  • The login next check let ///host and /\host through (no netloc per urlparse, but browsers treat both as another origin), so a crafted login link could redirect off-site after sign-in. This predates the admin split. Targets starting with // or containing a backslash, tab, or newline now fall back to /.
  • Public paths only accepted literal GET, so HEAD returned 401 and would make an uptime check report the viewer as down. Public pages and GET APIs now accept HEAD and OPTIONS as well.

Testing

  • pytest tests/test_web_viewer.py: 359 passed, including new tests that fail on each bug above.
  • node --test tests/js/*.test.mjs: 6 passed.

agessaman and others added 5 commits September 22, 2026 20:31
When web_viewer_password is set, Dashboard, Contacts, and Mesh Graph are readable without logging in, along with the read-only APIs they need. Radio, Config, Logs, every mutating API, /api/channels, and the live Socket.IO streams require an admin session, since those can expose channel keys and decrypted traffic. The Realtime page still renders publicly but shows "Admin login required" instead of connecting. Anything not on the public allowlist fails closed.

Login now compares HMAC digests with hmac.compare_digest and clears the session before setting authenticated_admin. Logout clears the whole session and returns to the dashboard.

Co-authored-by: Gerard Hickey <hickey@kinetic-compute.com>
…dmin buttons

contacts.html declared its own top-level const IS_ADMIN, but base.html already declares one in an earlier script on the same page. Classic scripts share one global lexical scope, so the redeclaration threw a SyntaxError and the whole Contacts script never ran, leaving the table on its loading placeholder in every mode. Contacts now uses the global from base.html.

The zombie-recover and clear-offline banner buttons were still rendered for anonymous viewers even though their /api/admin/* endpoints now return 401. They are gated on the same auth_enabled/is_admin check as the other admin controls.
…ts, allow HEAD on public paths

Logging out cleared the HTTP session but left that login's Socket.IO connections open, and a socket keeps the session it connected with, so a /realtime tab kept streaming packets, decrypted messages, and logs after logout. Each login now gets an admin_login_id, sockets record it on connect, and logout disconnects the sockets opened under that login. Other admin logins stay connected.

The login next check passed ///host and /\host because urlparse sees no netloc in either, but browsers resolve both as another origin. Next targets are now rejected if they start with //, contain a backslash, or contain a tab or newline. This check predates the admin split.

Public pages and GET APIs only accepted literal GET, so HEAD (and OPTIONS) on them returned 401 and made uptime checks report the viewer as down. They now accept HEAD and OPTIONS too; admin paths are unchanged.
…t can parse base.html

The frontend lint job failed with "Parsing error: Unexpected token {" because base.html's inline script assigned IS_ADMIN from a raw Jinja expression, which isn't valid JS until rendered. The flag now lives in data-is-admin on <body> and the script reads it from document.body.dataset. I also dropped the unused AUTH_ENABLED constant and declared IS_ADMIN as a readonly ESLint global, since contacts.html reads it from base.html.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant