From 77397e4cb30a182db7838e074797f80baba0905c Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 15 Jul 2026 19:38:51 +0200 Subject: [PATCH] test(auth): pin the admin-session absolute-TTL cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PICLOUD_SESSION_ABSOLUTE_TTL_HOURS` (audit C1, migration 0070) is the whole mitigation for a stolen-but-warm admin token, and nothing tested it — grepping `absolute_expires_at` across all tests returned zero hits. New DB-backed suite pins the authoritative enforcement: the lookup filter `absolute_expires_at > NOW()`. A session with a future sliding `expires_at` but a past absolute cap must NOT resolve (the stolen-but-warm case), and `touch` cannot resurrect it even when it pushes `expires_at` a year out — the filter is the backstop regardless of the middleware clamp. Mutation-verified: dropping the `absolute_expires_at > NOW()` predicate makes both tests fail. Runs on a private database via picloud-test-support. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/session_absolute_ttl.rs | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 crates/manager-core/tests/session_absolute_ttl.rs diff --git a/crates/manager-core/tests/session_absolute_ttl.rs b/crates/manager-core/tests/session_absolute_ttl.rs new file mode 100644 index 0000000..3c33341 --- /dev/null +++ b/crates/manager-core/tests/session_absolute_ttl.rs @@ -0,0 +1,117 @@ +//! The admin-session ABSOLUTE TTL cap (audit 2026-07-11 C1, migration 0070). +//! +//! `PICLOUD_SESSION_ABSOLUTE_TTL_HOURS` is the whole mitigation for a stolen-but- +//! warm admin token — the highest-value credential in the system. A session's +//! sliding `expires_at` is bumped on every use, but it is clamped at +//! `login_time + absolute_ttl`, so even a continuously-used or stolen token self- +//! expires. Nothing tested it (grepping `absolute_expires_at` across all tests +//! returned zero hits). +//! +//! The AUTHORITATIVE enforcement is the lookup filter: `admin_session_repo`'s +//! `lookup` selects `... AND absolute_expires_at > NOW()`, so a row past its cap +//! is invisible no matter how far into the future `touch` has pushed `expires_at`. +//! The middleware's `sliding.min(absolute_expires_at)` clamp is defense-in-depth +//! on top; the lookup filter is what actually expires the token, and it is what +//! these tests pin. Deleting the `absolute_expires_at > NOW()` predicate — the +//! plausible regression — makes every past-cap assertion below fail. +//! +//! Skips cleanly when `DATABASE_URL` is unset. + +use chrono::{Duration, Utc}; +use picloud_manager_core::admin_session_repo::{ + AdminSessionRepository, PostgresAdminSessionRepository, +}; +use picloud_shared::AdminUserId; +use uuid::Uuid; + +async fn mk_admin(pool: &sqlx::PgPool) -> AdminUserId { + let uniq = Uuid::new_v4().simple().to_string(); + let (id,): (Uuid,) = sqlx::query_as( + "INSERT INTO admin_users (username, password_hash) VALUES ($1, 'x') RETURNING id", + ) + .bind(format!("sat-{uniq}")) + .fetch_one(pool) + .await + .expect("insert admin"); + id.into() +} + +/// A session within BOTH windows resolves; one past its absolute cap does NOT — +/// even though its sliding `expires_at` is still in the future. That gap (future +/// `expires_at`, past `absolute_expires_at`) is exactly the stolen-but-warm token +/// the cap exists to kill. +#[tokio::test] +async fn a_session_past_its_absolute_cap_does_not_resolve() { + let Some(pool) = picloud_test_support::test_pool("session_absolute_ttl").await else { + return; + }; + let repo = PostgresAdminSessionRepository::new(pool.clone()); + let user = mk_admin(&pool).await; + let now = Utc::now(); + + // Warm and within its cap — resolves. + repo.create( + user, + "hash-live", + now + Duration::hours(1), + now + Duration::hours(24), + ) + .await + .expect("create live"); + let live = repo.lookup("hash-live").await.expect("lookup live"); + assert!(live.is_some(), "a session within both windows must resolve"); + assert_eq!(live.unwrap().user_id, user); + + // Sliding window still open (expires_at in the future), but the ABSOLUTE cap + // has passed. Must NOT resolve — this is the whole guarantee. + repo.create( + user, + "hash-capped", + now + Duration::hours(1), + now - Duration::minutes(1), + ) + .await + .expect("create capped"); + assert!( + repo.lookup("hash-capped") + .await + .expect("lookup capped") + .is_none(), + "a warm session (future expires_at) past its absolute cap must self-expire" + ); +} + +/// `touch` cannot resurrect a past-cap session. Even after the sliding window is +/// bumped far into the future, the lookup's absolute filter keeps the row hidden — +/// so a continuously-used stolen token cannot slide past its cap. +#[tokio::test] +async fn touch_cannot_extend_a_session_past_its_absolute_cap() { + let Some(pool) = picloud_test_support::test_pool("session_absolute_ttl").await else { + return; + }; + let repo = PostgresAdminSessionRepository::new(pool.clone()); + let user = mk_admin(&pool).await; + let now = Utc::now(); + + // Absolute cap already passed; sliding window open. + repo.create( + user, + "hash-warm", + now + Duration::hours(1), + now - Duration::minutes(1), + ) + .await + .expect("create warm"); + + // Simulate the sliding-window bump with an UNCLAMPED value a year out — i.e. + // the exact thing the middleware clamp is meant to prevent. Even so: + repo.touch("hash-warm", now + Duration::days(365)) + .await + .expect("touch"); + + assert!( + repo.lookup("hash-warm").await.expect("lookup").is_none(), + "even an unclamped touch a year out cannot resurrect a past-cap session — \ + the lookup's absolute_expires_at filter is the backstop" + ); +}