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:
117
crates/manager-core/tests/session_absolute_ttl.rs
Normal file
117
crates/manager-core/tests/session_absolute_ttl.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user