crates/gunnar-core/tests/tokens.rs
//! Access tokens: the credential smart HTTP authenticates with, and the
//! proof that it does not become a second way to be allowed something.
//!
//! An SSH key and an access token are two doors into **one** identity model.
//! The dangerous shape is the one where a token quietly carries authority of
//! its own — a "token scope", a "read-only token", a token that works on a
//! repository its principal has no grant on. Every assertion below is about
//! that: the token names a principal, and everything after that is the ACL that
//! was already there.
//!
//! Measure, never assert applies twice here, because a credential check has two ways to be
//! useless: one that accepts everything passes every "the right token works"
//! test, and one that accepts nothing passes every "the wrong token is refused"
//! test. Nothing below is asserted in one direction only.
mod common;
use common::Fixture;
use gunnar_core::model::PrincipalKind;
use gunnar_core::{Grant, Level, Principal, ANONYMOUS_PRINCIPAL_ID};
const SECRET: &str = "gunnar_pat_JOEcqYQ2mXqPyU2WfZ2kR7wYFbc3nQ0d";
const OTHER: &str = "gunnar_pat_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ";
fn principal(fx: &Fixture, id: &str) {
fx.auth
.add_principal(Principal::new(id, id, PrincipalKind::User).expect("legal id"))
.expect("add principal");
}
/// A token resolves to its principal, and **only** to its principal.
#[test]
fn a_token_names_one_principal_and_a_wrong_secret_names_nobody() {
let fx = Fixture::new();
principal(&fx, "deploy");
fx.auth
.register_token("deploy", SECRET, "ci runner", None)
.expect("register");
let who = fx
.auth
.principal_by_token(SECRET)
.expect("read")
.expect("the registered secret authenticates");
assert_eq!(who.id, "deploy");
// The other direction, without which "it authenticates" would also be true
// of a lookup that returned the first principal in the table.
assert!(
fx.auth.principal_by_token(OTHER).expect("read").is_none(),
"an unregistered secret must authenticate nobody"
);
assert!(
fx.auth.principal_by_token("").expect("read").is_none(),
"the empty secret must authenticate nobody; an HTTP request with no \
password sends exactly this"
);
}
/// **The secret is not recoverable from the table.**
///
/// The auth document is a git blob and every version of it is kept, so a stored
/// secret is a stored secret forever. Asserted over the whole serialised
/// document rather than over the token record, because a field added later
/// somewhere else in the document would not show up in a record-shaped check.
#[test]
fn the_stored_table_never_contains_the_secret() {
let fx = Fixture::new();
principal(&fx, "deploy");
let token = fx
.auth
.register_token("deploy", SECRET, "ci runner", None)
.expect("register");
let stored = fx.dump_auth_document();
assert!(
!stored.contains(SECRET),
"the token secret is in the stored auth document"
);
assert!(
stored.contains(&token.hash),
"…but its hash must be, or this assertion is measuring an empty \
document rather than a redacted one. Document was {} bytes",
stored.len()
);
assert_ne!(
token.hash, SECRET,
"the `hash` field must not be the secret under another name"
);
assert_eq!(token.hash.len(), 64, "sha-256, lowercase hex");
}
/// **A token carries no authority of its own.** It gets exactly the grants its
/// principal has, and it stops working the moment they are revoked.
#[test]
fn a_token_confers_nothing_beyond_its_principals_grants() {
let fx = Fixture::new();
let repo = fx.repo("acme", "widget");
let other = fx.repo("acme", "secret");
principal(&fx, "reader");
fx.auth
.register_token("reader", SECRET, "laptop", None)
.expect("register");
fx.auth
.grant(Grant::repo("reader", repo.path(), Level::Read))
.expect("grant");
let who = fx
.auth
.principal_by_token(SECRET)
.expect("read")
.expect("authenticates");
// The grant it does have.
assert!(fx.auth.may(&who.id, &repo, Level::Read).expect("acl"));
// The three it does not. A token that carried its own authority would be
// the thing that made any of these true.
assert!(
!fx.auth.may(&who.id, &repo, Level::Write).expect("acl"),
"a token holder must not push where its principal may only read"
);
assert!(
!fx.auth.may(&who.id, &other, Level::Read).expect("acl"),
"a token must not reach a repository its principal has no grant on"
);
assert!(!fx.auth.may_server(&who.id, Level::Read).expect("acl"));
// And revoking the *grant* — not the token — takes the access away, which
// is what "the token is not the authority" means.
fx.auth
.revoke_grant("reader", &gunnar_core::Scope::Repo(repo.path()))
.expect("revoke");
assert!(
!fx.auth.may(&who.id, &repo, Level::Read).expect("acl"),
"the token still authenticates, so access must now come only from the \
ACL, and the ACL says no"
);
assert!(fx.auth.principal_by_token(SECRET).expect("read").is_some());
}
/// Revocation, expiry and a deleted principal: the three ways a token stops
/// working, each with its live counterpart beside it.
#[test]
fn a_token_stops_working_when_revoked_when_expired_and_when_its_principal_goes() {
let fx = Fixture::new();
principal(&fx, "deploy");
let token = fx
.auth
.register_token("deploy", SECRET, "ci", None)
.expect("register");
assert!(fx.auth.principal_by_token(SECRET).expect("read").is_some());
let revoked = fx
.auth
.revoke_token(&token.hash)
.expect("revoke")
.expect("it was there");
assert_eq!(revoked.hash, token.hash);
assert!(
fx.auth.principal_by_token(SECRET).expect("read").is_none(),
"a revoked token must authenticate nobody"
);
assert!(
fx.auth.revoke_token(&token.hash).expect("revoke").is_none(),
"revoking twice reports nothing was removed"
);
// Expiry, at an explicit time so nothing waits.
let expires = 2_000_000_000_000;
fx.auth
.register_token("deploy", OTHER, "short-lived", Some(expires))
.expect("register");
assert!(
fx.auth
.principal_by_token_at(OTHER, expires - 1)
.expect("read")
.is_some(),
"before its expiry the token must work; without this the assertion \
below would also pass for a token that never worked at all"
);
assert!(
fx.auth
.principal_by_token_at(OTHER, expires)
.expect("read")
.is_none(),
"at its expiry the token must stop"
);
// The principal itself going away.
fx.auth.remove_principal("deploy").expect("remove");
assert!(
fx.auth
.principal_by_token_at(OTHER, expires - 1)
.expect("read")
.is_none(),
"a token whose principal has been deleted must authenticate nobody"
);
}
/// **A deleted principal's tokens do not come back with its name.**
///
/// `remove_principal` took the keys, the grants, the memberships and the gate,
/// and left the tokens. That was nearly harmless: `principal_by_token_at` ends
/// in `doc.principals.get(&token.principal_id)`, so an orphaned token
/// authenticates as nobody, which is what the test above asserts and what the
/// method's own doc promises.
///
/// It stopped being harmless at the moment the id was claimed again. A
/// principal id is free the instant its principal is gone — `add_principal`
/// refuses only a name that is currently TAKEN — and a token row names its
/// principal by that id. So recreating `deploy` handed the new `deploy` every
/// access token the old one had, with nothing said to the operator who removed
/// the principal or to the one who created it. It is the leftover-gate bug in
/// its other direction: there a stale row locked the next principal OUT, here
/// it let somebody else IN.
///
/// The green half is the whole point and is asserted first: a freshly created
/// principal with a freshly registered token authenticates. Without it, a
/// `remove_principal` that had broken the token table outright would satisfy
/// the refusal below and look like a fix.
#[test]
fn a_recreated_principal_does_not_inherit_the_removed_one_s_tokens() {
let fx = Fixture::new();
principal(&fx, "deploy");
fx.auth
.register_token("deploy", SECRET, "ci runner", None)
.expect("register");
assert_eq!(
fx.auth
.principal_by_token(SECRET)
.expect("read")
.map(|p| p.id),
Some("deploy".to_owned()),
"the fixture's own token does not work, so nothing below is about deletion"
);
// The principal goes, and the SAME id is claimed again — by somebody else,
// which is the case that matters and the one an id being reusable allows.
fx.auth.remove_principal("deploy").expect("remove");
principal(&fx, "deploy");
assert!(
fx.auth.principal("deploy").expect("read").is_some(),
"the id was not claimable again, so this test is not exercising the case it names"
);
assert!(
fx.auth.principal_by_token(SECRET).expect("read").is_none(),
"the new holder of the name `deploy` inherited the old one's access token. The \
token row outlived the principal it named, and the id was free to claim"
);
assert!(
fx.auth
.tokens()
.expect("list")
.iter()
.all(|token| token.principal_id != "deploy"),
"the token is refused but the row is still in the table, so it will come back with \
the name the next time the id is claimed"
);
// …and the table still works for the new principal, which is what says the
// removal took the right row and not the whole collection.
fx.auth
.register_token("deploy", OTHER, "the new one's", None)
.expect("register");
assert_eq!(
fx.auth
.principal_by_token(OTHER)
.expect("read")
.map(|p| p.id),
Some("deploy".to_owned()),
"the new principal cannot register a token of its own"
);
}
/// The anonymous id is reserved against tokens too.
///
/// `add_principal` already refuses it, so no token *could* be registered to it
/// through the ordinary door. This is the second lock: a table that somehow
/// held the anonymous principal must still not be reachable with a credential,
/// because a credential-bearing anonymous is a stranger with an identity, which
/// is the one thing rule 0 of the anonymous path assumes cannot happen.
#[test]
fn no_token_can_be_registered_to_the_anonymous_principal() {
let fx = Fixture::new();
let err = fx
.auth
.register_token(ANONYMOUS_PRINCIPAL_ID, SECRET, "nope", None)
.expect_err("the anonymous id is reserved");
assert!(
format!("{err}").contains(ANONYMOUS_PRINCIPAL_ID),
"the refusal must name the id it refused: {err}"
);
assert!(
fx.auth.principal_by_token(SECRET).expect("read").is_none(),
"…and must not have registered it anyway"
);
// The live half: a real principal takes the same call.
principal(&fx, "real");
fx.auth
.register_token("real", SECRET, "yes", None)
.expect("a real principal accepts a token");
}
/// Registering the same secret twice is refused rather than re-pointed.
#[test]
fn a_secret_cannot_be_registered_to_two_principals() {
let fx = Fixture::new();
principal(&fx, "one");
principal(&fx, "two");
fx.auth
.register_token("one", SECRET, "a", None)
.expect("register");
let err = fx
.auth
.register_token("two", SECRET, "b", None)
.expect_err("a duplicate secret is refused");
assert!(format!("{err}").contains("one"), "{err}");
assert!(
!format!("{err}").contains(SECRET),
"the refusal must not echo the secret into a log: {err}"
);
assert_eq!(
fx.auth
.principal_by_token(SECRET)
.expect("read")
.expect("still resolves")
.id,
"one",
"the first registration must still stand"
);
}
/// An unknown principal is refused, and the token list stays empty.
#[test]
fn a_token_for_an_unknown_principal_is_refused() {
let fx = Fixture::new();
fx.auth
.register_token("ghost", SECRET, "x", None)
.expect_err("no such principal");
assert!(fx.auth.tokens().expect("list").is_empty());
principal(&fx, "ghost");
fx.auth
.register_token("ghost", SECRET, "x", None)
.expect("now it exists");
assert_eq!(fx.auth.tokens().expect("list").len(), 1);
}
/// `touch_token` records use without being on the lookup path.
#[test]
fn last_used_is_recorded_and_starts_unset() {
let fx = Fixture::new();
principal(&fx, "deploy");
let token = fx
.auth
.register_token("deploy", SECRET, "ci", None)
.expect("register");
assert_eq!(token.last_used, None);
fx.auth.touch_token(&token.hash, 1_700_000_000_000).unwrap();
let listed = fx.auth.tokens().expect("list");
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].last_used, Some(1_700_000_000_000));
assert_eq!(listed[0].label, "ci");
}
/// **A bulk import is one transaction: all of it lands or none of it does.**
///
/// The population story (1→100 000 users) needs an import that is not
/// O(users²) in document clones, and `bulk_add_users` buys that with a single
/// CAS — which makes atomicity the property to pin: a batch with one refused
/// user must leave the table exactly as it found it, because a half-landed
/// import is a population nobody can enumerate.
///
/// Seen red against the naive per-user-commit loop this method replaces:
/// the atomicity assertion fails with 102 principals against the pinned 100 —
/// user101 and user102 landed before user57's duplicate refused the rest,
/// which is the half-import this test exists to forbid.
#[test]
fn a_bulk_import_lands_whole_or_not_at_all() {
use gunnar_core::{token_hash, BulkUser};
let fx = Fixture::new();
let user = |i: usize| BulkUser {
principal: gunnar_core::Principal::new(
format!("user{i}"),
format!("user{i}"),
gunnar_core::PrincipalKind::User,
)
.expect("id"),
token_hash: Some(token_hash(&format!("secret-{i}"))),
token_label: "bulk".into(),
openssh_public_key: None,
grants: vec![gunnar_core::Grant {
principal: format!("user{i}"),
scope: gunnar_core::Scope::Namespace("corpus".into()),
level: gunnar_core::Level::Read,
}],
};
// The happy half: 100 users, one call, everything reachable afterwards.
let users: Vec<BulkUser> = (1..=100).map(user).collect();
assert_eq!(
fx.auth.bulk_add_users(&users, 1_700_000_000_000).unwrap(),
100
);
assert_eq!(fx.auth.principals().unwrap().len(), 100);
let who = fx
.auth
.principal_by_token("secret-57")
.unwrap()
.expect("user57's token authenticates");
assert_eq!(who.id, "user57");
assert!(fx
.auth
.may_namespace("user57", "corpus", gunnar_core::Level::Read)
.unwrap());
// The atomic half: a batch whose LAST user collides must land nothing.
let mut bad: Vec<BulkUser> = (101..=102).map(user).collect();
bad.push(user(57));
fx.auth
.bulk_add_users(&bad, 1_700_000_000_000)
.expect_err("a batch with a duplicate must be refused");
assert_eq!(
fx.auth.principals().unwrap().len(),
100,
"the refused batch must have landed NOTHING — user101 and user102 arriving without \
user103 is the half-import this method exists to make impossible"
);
}
/// **`last_used` is minute-grained on purpose.** Writing it per request put
/// every authenticated HTTP request in contention on one shared-document CAS —
/// the serving tier's measured ceiling (`gunnar.multiuser_scaling`: 102 users
/// plateaued at 82 ops/s, 862 updates dropped in one window). A touch inside
/// [`gunnar_core::LAST_USED_GRANULARITY`] must be a no-op; one at the boundary
/// must write.
///
/// Seen red against the pre-granularity `touch_token`: the second assertion
/// failed with `Some(1_700_000_030_000)`, which is this test detecting the
/// per-request write the fix removes.
#[test]
fn a_fresh_last_used_is_not_rewritten_and_a_stale_one_is() {
let fx = Fixture::new();
principal(&fx, "deploy");
let token = fx
.auth
.register_token("deploy", SECRET, "ci", None)
.expect("register");
let t0 = 1_700_000_000_000;
// The FIRST touch of a quiet token writes immediately — a granularity that
// delayed the first record would leave a live credential looking unused.
fx.auth.touch_token(&token.hash, t0).unwrap();
assert_eq!(fx.auth.tokens().unwrap()[0].last_used, Some(t0));
// Inside the window: a no-op, and the recorded value does not move.
fx.auth
.touch_token(
&token.hash,
t0 + gunnar_core::LAST_USED_GRANULARITY - 30_000,
)
.unwrap();
assert_eq!(
fx.auth.tokens().unwrap()[0].last_used,
Some(t0),
"a touch inside the granularity window must not rewrite the shared document"
);
// At the boundary: stale, so it writes.
let t1 = t0 + gunnar_core::LAST_USED_GRANULARITY;
fx.auth.touch_token(&token.hash, t1).unwrap();
assert_eq!(
fx.auth.tokens().unwrap()[0].last_used,
Some(t1),
"a touch at the granularity boundary must record the new time"
);
}
/// **A removed principal's token does not come back with the name** (GUN-026).
///
/// `remove_principal` cut keys, grants, memberships and the gate, and left the
/// TOKEN row behind. `principal_by_token` looks the principal up, so the
/// orphan authenticated nobody while the name was free — which is why this was
/// only Info and why the existing arm above stayed green.
///
/// A principal id is claimable again the moment its holder is gone:
/// `add_principal` refuses a name that is *currently* taken and nothing more.
/// So the orphaned row waits, and the day somebody re-creates `deploy` — a
/// different person, a new laptop, a rebuilt CI account — the old secret
/// authenticates as them. That is the same shape the gate row was fixed for on
/// its own line four lines below, with the same reasoning written out.
///
/// Mutation-proven 2026-09-11: take the `doc.tokens.retain` out of
/// `remove_principal` and the last assertion here reads
/// `Some(Principal { id: "deploy", … })` — the new principal, holding the old
/// principal's credential.
#[test]
fn a_token_does_not_survive_its_principal_into_the_next_one_of_that_name() {
let fx = Fixture::new();
principal(&fx, "deploy");
fx.auth
.register_token("deploy", SECRET, "the old ci runner", None)
.expect("register");
assert!(
fx.auth.principal_by_token(SECRET).expect("read").is_some(),
"the fixture must start with a token that works, or nothing below means anything"
);
fx.auth.remove_principal("deploy").expect("remove");
// The row itself, not only what it resolves to: the listing is what an
// operator audits, and a credential nobody can see is a credential nobody
// revokes.
assert!(
!fx.auth
.tokens()
.expect("list tokens")
.iter()
.any(|token| token.principal_id == "deploy"),
"the removed principal's token is still in the table"
);
// The name, claimed again by somebody else.
principal(&fx, "deploy");
assert!(
fx.auth.principal_by_token(SECRET).expect("read").is_none(),
"the old principal's secret authenticated as the new principal of the same name"
);
// …and the new principal can hold a token of its own, so this is not a
// name that has been poisoned.
fx.auth
.register_token("deploy", OTHER, "the new ci runner", None)
.expect("register");
assert_eq!(
fx.auth
.principal_by_token(OTHER)
.expect("read")
.expect("the new secret authenticates")
.id,
"deploy"
);
}