test(auth): pin the admin-session absolute-TTL cap

`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) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-15 19:38:51 +02:00
parent 27bda66be6
commit 77397e4cb3

View File

@@ -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"
);
}