From 6a1bc437c928224d37453c943b2aaefe11ecd6c6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 6 Oct 2026 10:51:29 -0400 Subject: [PATCH] feat(auth): share links open only their own widgets; unmarked routes are closed (#5033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5033 "Share links open only their own dashboard; every route without a role is on an explicit allow-list". Any live share token in ?s= made the caller a viewer on every viewer route — host pages, container logs, any dashboard — and the host-agent widget routes had no role check at all. - steward/dashboard/share.py: a share token opens, read-only, exactly the widget fragment URLs its own dashboard loads — same path, same configured parameters; only the time window (hours/range) may change. - require_role marks its view; public_route marks the deliberately open ones (login, setup, health, the share page, agent ingest, install.sh, agent.py) with the reason. A before_request refuses any view with neither, so a route that forgets its decorator is closed, not open. - /plugins/host_agent/widget and /widget/history now require viewer. - The history widget's range buttons keep the rest of the query (and so a share link's token) instead of rebuilding it; the uptime widget's "Full SLA" link shows to logged-in users, not to share viewers. - tests/test_route_access.py: a source scan that fails on any unmarked route in core or bundled plugins, the default-deny refusal, and the share matcher's cases. Co-Authored-By: Claude Opus 5.5 --- plugins/host_agent/routes.py | 7 +- .../host_agent/templates/widget_history.html | 12 ++- steward/app.py | 28 +++---- steward/auth/middleware.py | 27 +++++++ steward/auth/routes.py | 9 ++- steward/dashboard/routes.py | 3 +- steward/dashboard/share.py | 58 +++++++++++++++ steward/hosts/routes.py | 2 - steward/templates/hosts/uptime_widget.html | 2 +- tests/test_route_access.py | 74 +++++++++++++++++++ 10 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 steward/dashboard/share.py create mode 100644 tests/test_route_access.py diff --git a/plugins/host_agent/routes.py b/plugins/host_agent/routes.py index f74b7f8..f5c2221 100644 --- a/plugins/host_agent/routes.py +++ b/plugins/host_agent/routes.py @@ -9,7 +9,7 @@ from pathlib import Path import secrets from quart import Blueprint, current_app, jsonify, request, Response, render_template, redirect, url_for, session -from steward.auth.middleware import require_role +from steward.auth.middleware import public_route, require_role from steward.models.users import UserRole from sqlalchemy import select, func, or_, and_ from datetime import timedelta @@ -166,6 +166,7 @@ def _error(status: int, code: str, detail: str | None = None) -> tuple[Any, int] @host_agent_bp.post("/ingest") +@public_route("the agent authenticates with its bearer token") async def ingest(): auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer "): @@ -313,6 +314,7 @@ def _agent_version() -> str: @host_agent_bp.get("/install.sh") +@public_route("fetched by curl on the host being enrolled; carries an enrolment token") async def install_script(): token = request.args.get("token", "") if not token: @@ -344,6 +346,7 @@ async def install_script(): @host_agent_bp.get("/agent.py") +@public_route("fetched by the installer and by agent self-update; holds no secrets") async def agent_source(): try: body = AGENT_SOURCE_PATH.read_text(encoding="utf-8") @@ -470,6 +473,7 @@ async def _fleet_rows(session) -> list[dict]: @host_agent_bp.get("/widget") +@require_role(UserRole.viewer) async def widget_table(): """Fleet-glance dashboard widget: one row per monitored host, latest metrics.""" async with current_app.db_sessionmaker() as session: @@ -478,6 +482,7 @@ async def widget_table(): @host_agent_bp.get("/widget/history") +@require_role(UserRole.viewer) async def widget_history(): host_id = request.args.get("host_id", "") try: diff --git a/plugins/host_agent/templates/widget_history.html b/plugins/host_agent/templates/widget_history.html index 1c13783..01e7c5b 100644 --- a/plugins/host_agent/templates/widget_history.html +++ b/plugins/host_agent/templates/widget_history.html @@ -22,7 +22,7 @@ {% endif %} {% for opt in [1, 6, 24] %} - @@ -36,11 +36,15 @@