Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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