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/principal_identity.rs

//! **A principal's id is not its name.**
//!
//! The id is the account: chosen once, keyed on everywhere, never moved. The
//! name is a label: unique, and free to change without a key, a grant, a
//! membership, a gate or the namespace noticing. This file proves both halves
//! on the applied table, and proves the refusals red before the acceptances.
//!
//! The `created` stamp rides along: it is the table's clock, not the caller's,
//! and it is what lets a reader tell a delete-and-recreate under one id from
//! the row it first saw.

mod common;

use common::{Fixture, KAT_ED25519, KAT_ED25519_FINGERPRINT};
use gunnar_core::model::PrincipalKind;
use gunnar_core::{AuthTable, Error, Grant, Level, Principal, Scope};

fn add(fx: &Fixture, id: &str, name: &str) -> Principal {
    fx.auth
        .add_principal(Principal::new(id, name, PrincipalKind::User).expect("legal id"))
        .expect("add principal")
}

/// The headline: the same name may not be carried twice, whether the other
/// holder has it as a name or as an id. Both reds, then the honest green.
#[test]
fn a_name_is_unique_against_names_and_ids() {
    let fx = Fixture::new();
    add(&fx, "alice", "alice");
    add(&fx, "bob", "Bob Builder");

    // RED: another principal's name.
    let err = fx
        .auth
        .add_principal(Principal::new("bob2", "Bob Builder", PrincipalKind::User).unwrap())
        .expect_err("a taken name must be refused");
    assert!(
        matches!(&err, Error::NameExists { name, principal_id } if name == "Bob Builder" && principal_id == "bob"),
        "{err}"
    );

    // RED: another principal's ID, spelled as a name. A display name that is
    // somebody else's identity would be a row two readers disagree about.
    let err = fx
        .auth
        .add_principal(Principal::new("carol", "alice", PrincipalKind::User).unwrap())
        .expect_err("a name equal to another principal's id must be refused");
    assert!(
        matches!(&err, Error::NameExists { name, principal_id } if name == "alice" && principal_id == "alice"),
        "{err}"
    );

    // GREEN: a free name on a free id.
    add(&fx, "carol", "Carol");
    assert_eq!(
        fx.auth.principals().expect("list").len(),
        3,
        "the two refusals must have written nothing"
    );
}

/// `principal_by_name` is one probe, and it answers the CURRENT name: after a
/// rename the old name resolves to nobody, even though the row is still stored
/// under an id that spells it.
#[test]
fn lookup_by_name_answers_the_current_name_only() {
    let fx = Fixture::new();
    add(&fx, "alice", "alice");

    assert_eq!(
        fx.auth
            .principal_by_name("alice")
            .expect("read")
            .map(|p| p.id),
        Some("alice".to_owned())
    );
    assert!(fx.auth.principal_by_name("alicia").expect("read").is_none());

    fx.auth.rename_principal("alice", "alicia").expect("rename");

    assert_eq!(
        fx.auth
            .principal_by_name("alicia")
            .expect("read")
            .map(|p| p.id),
        Some("alice".to_owned()),
        "the new name must resolve to the same id"
    );
    // The row is still at key "alice" — the id — but "alice" is not anybody's
    // NAME any more. A lookup that answered the row here would be a lookup by
    // id wearing a name's clothes.
    assert!(
        fx.auth.principal_by_name("alice").expect("read").is_none(),
        "the old name must not resolve after a rename"
    );
}

/// The whole point: a rename moves the label and nothing else. Every keyed
/// thing — key, grant, membership, account ownership — is asserted on the
/// applied ACL answer before and after.
#[test]
fn a_rename_changes_the_name_and_nothing_that_is_keyed_on_the_id() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    let own = fx.repo("alice", "diary");
    add(&fx, "alice", "alice");
    fx.auth
        .add_principal(Principal::new("backend", "backend", PrincipalKind::Group).unwrap())
        .expect("group");
    fx.auth
        .register_key("alice", KAT_ED25519, None)
        .expect("register key");
    fx.auth
        .grant(Grant::repo("alice", repo.path(), Level::Write))
        .expect("grant");
    fx.auth
        .add_to_group("backend", "alice")
        .expect("membership");

    let before = fx.auth.principal("alice").expect("read").expect("exists");
    assert_eq!(before.name, "alice");

    let renamed = fx
        .auth
        .rename_principal("alice", "Alice Liddell")
        .expect("rename");
    assert_eq!(renamed.id, "alice", "the id must not move");
    assert_eq!(renamed.name, "Alice Liddell");
    assert_eq!(
        renamed.created, before.created,
        "a rename is not a re-creation"
    );

    // Applied output, not a round-trip of the request:
    let after = fx
        .auth
        .principal("alice")
        .expect("read")
        .expect("still there");
    assert_eq!(after, renamed);
    let fp = gunnar_core::Fingerprint::parse(KAT_ED25519_FINGERPRINT).unwrap();
    assert_eq!(
        fx.auth
            .principal_by_fingerprint(&fp)
            .expect("read")
            .map(|p| p.name),
        Some("Alice Liddell".to_owned()),
        "the key must still authenticate as the renamed principal"
    );
    assert!(
        fx.auth.may("alice", &repo, Level::Write).expect("acl"),
        "the grant must survive the rename"
    );
    assert!(
        fx.auth.may("alice", &own, Level::Admin).expect("acl"),
        "the account is the id; a rename must not take it away"
    );
    assert_eq!(
        fx.auth.members_of("backend").expect("members"),
        vec!["alice".to_owned()],
        "the membership must survive the rename"
    );

    // And the freed name is NOT free: it is still somebody's id.
    let err = fx
        .auth
        .add_principal(Principal::new("alice2", "alice", PrincipalKind::User).unwrap())
        .expect_err("a name that is a live id stays taken");
    assert!(matches!(err, Error::NameExists { .. }), "{err}");

    // Renaming to a taken name is refused, and the row is untouched by the
    // refusal.
    add(&fx, "bob", "bob");
    let err = fx
        .auth
        .rename_principal("bob", "Alice Liddell")
        .expect_err("a taken name must be refused on rename too");
    assert!(matches!(err, Error::NameExists { .. }), "{err}");
    assert_eq!(
        fx.auth.principal("bob").expect("read").unwrap().name,
        "bob",
        "a refused rename must change nothing"
    );

    // Nobody to rename.
    let err = fx
        .auth
        .rename_principal("nobody", "x")
        .expect_err("an unknown id must be refused");
    assert!(matches!(err, Error::NoSuchPrincipal(_)), "{err}");
}

/// A removed principal frees its name in the same update; a re-created id is
/// a NEW row, and `created` says so.
#[test]
fn removal_frees_the_name_and_a_recreation_is_a_new_row() {
    let fx = Fixture::new();
    let first = add(&fx, "alice", "Alice");
    assert!(first.created > 0, "the table must stamp a creation time");

    fx.auth.remove_principal("alice").expect("remove");
    assert!(fx.auth.principal_by_name("Alice").expect("read").is_none());

    // The name is free for a different id…
    let other = add(&fx, "alice2", "Alice");
    assert_eq!(other.id, "alice2");
    fx.auth.remove_principal("alice2").expect("remove");

    // …and the id is claimable again, as a NEW row: same id, later stamp. This
    // is what a console pins alongside the id to notice the recreation.
    std::thread::sleep(std::time::Duration::from_millis(2));
    let second = add(&fx, "alice", "Alice");
    assert_eq!(second.id, first.id);
    assert!(
        second.created > first.created,
        "a recreation under the same id must carry a later creation time: {} vs {}",
        second.created,
        first.created
    );
}

/// The stamp is the TABLE's: whatever a caller wrote in `created` is replaced,
/// so the field is the server's word and cannot be back-dated by a client.
#[test]
fn created_is_stamped_by_the_table_not_the_caller() {
    let fx = Fixture::new();
    let mut claimed = Principal::new("alice", "alice", PrincipalKind::User).unwrap();
    claimed.created = 1;
    let stored = fx.auth.add_principal(claimed).expect("add");
    assert_ne!(stored.created, 1, "the caller's stamp must not be kept");
    assert!(
        stored.created > 1_600_000_000_000,
        "not a real clock: {}",
        stored.created
    );
    assert_eq!(
        fx.auth.principal("alice").expect("read").unwrap().created,
        stored.created,
        "what was returned must be what was written"
    );
}

/// The name index rides in the same document as the rows, so it survives a
/// reopen exactly as they do.
#[test]
fn the_rename_survives_reopen() {
    let fx = Fixture::new();
    add(&fx, "alice", "alice");
    fx.auth.rename_principal("alice", "alicia").expect("rename");

    let reopened = AuthTable::new(fx.store.clone()).expect("reopen");
    assert_eq!(
        reopened
            .principal_by_name("alicia")
            .expect("read")
            .map(|p| p.id),
        Some("alice".to_owned())
    );
    assert!(reopened.principal_by_name("alice").expect("read").is_none());
    // A server-scoped grant keyed by the id still resolves after the reopen and
    // the rename — the id is what the grant names.
    reopened
        .grant(Grant {
            principal: "alice".into(),
            scope: Scope::Server,
            level: Level::Admin,
        })
        .expect("grant by id");
    assert!(reopened.may_server("alice", Level::Admin).expect("acl"));
}