feat(server): v2 secure-session-core Task 3 - secure relay WS
SPEC-002 Phase 1 Task 3 (specs/v2-secure-session-core), code-reviewed APPROVED. - viewer_ws_handler: verify the session-scoped VIEWER token (validate_viewer_token sig+exp+purpose) + token_blacklist.is_revoked + session_id claim == requested session, before upgrade. Raw login JWTs no longer accepted on the viewer plane (closes audit CRITICAL #2; closes the *mechanism* of CRITICAL #1). - mint_viewer_token: authz gate is_admin() || has_permission("view") -> 403. - Agent identity binding: validate_agent_api_key returns AgentKeyAuth; a cak_- verified agent rebinds to the key's machine identity (fails closed if unresolvable), so a key for machine X cannot seize machine Y's session slot. - Frame caps on both WS upgrades (agent 4 MiB, viewer 64 KiB) - closes WS-OOM HIGH. - Viewer->agent input throttle (200 ev/s token bucket, bounded try_send) - closes input-injection MEDIUM. - Startup managed-session reconcile clarified. KNOWN FOLLOW-UPS (tracked todos): (1) authz STRENGTH - the "view" permission is held by every default role incl. viewer, and a viewer token grants input control, so the gate should be "control" or a VIEW_ONLY/CONTROL token split; CRITICAL #1 is mechanism-closed, strength pending decision. (2) revoke minted viewer tokens on logout (currently bounded only by 5-min TTL). Not cargo-check-verified (no toolchain on the authoring host). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,14 @@
|
||||
//! matches the requested session.
|
||||
//!
|
||||
//! Authorization (Phase 1): the requester must present a valid dashboard JWT
|
||||
//! (the [`AuthenticatedUser`] extractor) AND the target session must exist in
|
||||
//! the live session manager. Per-tenant / per-machine ACL narrowing is a
|
||||
//! Phase-4 concern; the tenancy claim is already carried so the WS and future
|
||||
//! phases can enforce it. Minting is itself the authorization gate — only an
|
||||
//! authenticated user can obtain a token, and only for a real session.
|
||||
//! (the [`AuthenticatedUser`] extractor), MUST be authorized to view sessions
|
||||
//! (`is_admin()` OR the `view` permission — see [`SESSION_VIEW_PERMISSION`]),
|
||||
//! AND the target session must exist in the live session manager. This minting
|
||||
//! endpoint is the authorization decision point: it is what actually closes
|
||||
//! audit CRITICAL #1 (any authenticated user could previously obtain viewer
|
||||
//! access to any session). The WS layer then trusts the session-scoped token.
|
||||
//! Per-tenant / per-machine ACL narrowing is a Phase-4 concern; the tenancy
|
||||
//! claim is already carried so the WS and future phases can enforce it.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
@@ -30,6 +33,15 @@ use super::machine_keys::ApiError;
|
||||
|
||||
type ApiResult<T> = Result<T, (StatusCode, Json<ApiError>)>;
|
||||
|
||||
/// Permission (in GC's existing catalog — see `api::users` `valid_permissions`:
|
||||
/// `view`, `control`, `transfer`, `manage_users`, `manage_clients`) that gates
|
||||
/// obtaining a viewer token. `view` is the established "may view sessions"
|
||||
/// permission, held by every non-degraded role (admin/operator/viewer) and
|
||||
/// required of `viewer`. Admins bypass it via `is_admin()`. No NEW permission is
|
||||
/// introduced: the intra-tenant role distinction is honored using the existing
|
||||
/// `view` grant. (Policy DECIDED — Mike, 2026-05-29: admin-or-view-permission.)
|
||||
const SESSION_VIEW_PERMISSION: &str = "view";
|
||||
|
||||
/// Response carrying a freshly minted viewer token.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ViewerTokenResponse {
|
||||
@@ -65,7 +77,25 @@ pub async fn mint_viewer_token(
|
||||
let session_id = Uuid::parse_str(&id)
|
||||
.map_err(|_| err(StatusCode::BAD_REQUEST, "INVALID_SESSION_ID", "Invalid session ID"))?;
|
||||
|
||||
// Authorization gate: the session must exist (live session manager is the
|
||||
// AUTHORIZATION GATE (closes audit CRITICAL #1). Authentication alone is not
|
||||
// enough: the user must be an admin OR hold the `view` permission. A user
|
||||
// with no view grant cannot obtain a viewer token, and therefore cannot join
|
||||
// any session's viewer WS (which now requires this session-scoped token).
|
||||
if !(user.is_admin() || user.has_permission(SESSION_VIEW_PERMISSION)) {
|
||||
tracing::warn!(
|
||||
"User {} denied viewer-token mint for session {} (lacks '{}' permission)",
|
||||
user.username,
|
||||
session_id,
|
||||
SESSION_VIEW_PERMISSION
|
||||
);
|
||||
return Err(err(
|
||||
StatusCode::FORBIDDEN,
|
||||
"FORBIDDEN",
|
||||
"You do not have permission to view sessions",
|
||||
));
|
||||
}
|
||||
|
||||
// The session must exist (live session manager is the
|
||||
// source of truth for joinable sessions, matching GET /api/sessions/:id).
|
||||
let session = state.sessions.get_session(session_id).await.ok_or_else(|| {
|
||||
err(
|
||||
|
||||
Reference in New Issue
Block a user