feat(auth): share links open only their own widgets; unmarked routes are closed (#5033)
#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 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import bcrypt
|
||||
from quart import Blueprint, render_template, request, redirect, url_for, session
|
||||
from sqlalchemy import select, func
|
||||
from steward.auth.middleware import login_user, logout_user, safe_next_url
|
||||
from steward.auth.middleware import login_user, logout_user, public_route, safe_next_url
|
||||
from steward.models.users import User, UserRole
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
@@ -48,6 +48,7 @@ async def get_user_by_username(app, username: str) -> User | None:
|
||||
|
||||
|
||||
@auth_bp.get("/login")
|
||||
@public_route("the login page")
|
||||
async def login():
|
||||
from quart import current_app
|
||||
if await get_user_count(current_app) == 0:
|
||||
@@ -59,6 +60,7 @@ async def login():
|
||||
|
||||
|
||||
@auth_bp.post("/login")
|
||||
@public_route("the login form")
|
||||
async def login_post():
|
||||
from quart import current_app
|
||||
from steward.core.audit import log_audit
|
||||
@@ -100,6 +102,7 @@ async def login_post():
|
||||
|
||||
|
||||
@auth_bp.get("/login/oidc")
|
||||
@public_route("starts SSO login")
|
||||
async def login_oidc():
|
||||
from quart import current_app
|
||||
from steward.auth.oidc import get_discovery, build_authorize_url, generate_state
|
||||
@@ -125,6 +128,7 @@ async def login_oidc():
|
||||
|
||||
|
||||
@auth_bp.get("/login/oidc/callback")
|
||||
@public_route("SSO login callback")
|
||||
async def login_oidc_callback():
|
||||
from quart import current_app
|
||||
from steward.auth.oidc import get_discovery, exchange_code, get_userinfo, map_role
|
||||
@@ -179,12 +183,14 @@ async def login_oidc_callback():
|
||||
|
||||
|
||||
@auth_bp.get("/logout")
|
||||
@public_route("ends the session")
|
||||
async def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@auth_bp.get("/setup")
|
||||
@public_route("first-run setup (refuses once a user exists)")
|
||||
async def setup():
|
||||
from quart import current_app
|
||||
if await get_user_count(current_app) > 0:
|
||||
@@ -196,6 +202,7 @@ async def setup():
|
||||
|
||||
|
||||
@auth_bp.post("/setup")
|
||||
@public_route("first-run setup (refuses once a user exists)")
|
||||
async def setup_post():
|
||||
from quart import current_app
|
||||
from steward.core.audit import log_audit
|
||||
|
||||
Reference in New Issue
Block a user