use std::sync::Arc;
use gunnar_grpc::ControlPlane as _;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
fn passkey(seed: u8) -> Vec<u8> {
let key = p256::ecdsa::SigningKey::from_slice(&[seed; 32]).expect("a scalar");
key.verifying_key().to_sec1_point(false).as_bytes().to_vec()
}
fn person(stores: &ServerStores, id: &str, kind: gunnar_core::PrincipalKind) {
stores
.auth()
.add_principal(gunnar_core::Principal {
id: id.to_owned(),
name: id.to_owned(),
kind,
created: 0,
})
.expect("principal");
}
#[test]
fn a_passkey_survives_a_restart_and_goes_with_its_principal() {
let dir = tempfile::tempdir().expect("tempdir");
{
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let added = plane
.add_credential("kim", b"kim-yubikey", &passkey(1), "YubiKey", 7)
.expect("kim's passkey");
assert_eq!(added.credential_id, b"kim-yubikey".to_vec());
assert_eq!(added.sign_count, 7);
}
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
let listed = plane.credentials(Some("kim")).expect("listed");
assert_eq!(listed.len(), 1, "{listed:?}");
assert_eq!(listed[0].label, "YubiKey");
let stored = plane
.credential(b"kim-yubikey")
.expect("read")
.expect("the passkey is still there");
assert_eq!(
stored.public_key,
passkey(1),
"the key came back byte for byte"
);
assert_eq!(stored.credential.principal_id, "kim");
plane.remove_principal("kim").expect("removed");
assert!(
plane.credentials(None).expect("listed").is_empty(),
"the passkey outlived kim"
);
assert!(plane.credential(b"kim-yubikey").expect("read").is_none());
}
#[test]
fn a_machine_holds_no_passkey_and_an_id_is_registered_once() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "ci", gunnar_core::PrincipalKind::Machine);
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let refused = plane
.add_credential("ci", b"ci-key", &passkey(2), "ci", 0)
.expect_err("a machine holds no passkey");
assert!(
matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)),
"{refused:?}"
);
plane
.add_credential("kim", b"kim-key", &passkey(3), "laptop", 0)
.expect("kim's");
let twice = plane
.add_credential("kim", b"kim-key", &passkey(4), "again", 0)
.expect_err("an id once");
assert!(
matches!(twice, gunnar_grpc::ControlError::AlreadyExists(_)),
"{twice:?}"
);
let ghost = plane
.add_credential("ghost", b"ghost-key", &passkey(5), "ghost", 0)
.expect_err("no such principal");
assert!(
matches!(ghost, gunnar_grpc::ControlError::NotFound(_)),
"{ghost:?}"
);
}
fn core(
dir: &std::path::Path,
plane: &Arc<CoreControlPlane>,
) -> Arc<gunnar_grpc::identity::GunnarIdentity> {
gunnar_server::identity::build(
dir,
plane.clone(),
&gunnar_grpc::ServerConfig::default(),
false,
)
}
fn relay() -> nornir_identity::Relay {
nornir_identity::Relay {
rp_id: "console.example".into(),
origin: "https://console.example".into(),
client: None,
}
}
#[test]
fn a_logins_counter_is_kept_and_a_replayed_counter_is_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let mut key = nornir_identity::test_host::SoftPasskey::new(6);
{
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
plane
.add_credential("kim", &key.id, &key.public_key(), "YubiKey", 0)
.expect("kim's passkey");
let identity = core(dir.path(), &plane);
for _ in 0..4 {
let c = identity.login_start(&relay()).expect("a login");
identity
.login_finish(key.login(&c, "https://console.example", true))
.expect("kim logs in");
}
}
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let stored = plane.credential(&key.id).expect("read").expect("there");
assert_eq!(
stored.credential.sign_count, 4,
"the counter survived the restart"
);
assert!(stored.credential.last_used_unix_ms.is_some());
let identity = core(dir.path(), &plane);
let mut replay = nornir_identity::test_host::SoftPasskey::new(6);
let c = identity.login_start(&relay()).expect("a login");
let refused = identity
.login_finish(replay.login(&c, "https://console.example", true))
.expect_err("a counter that went back");
assert_eq!(refused.code, nornir_identity::Code::Ceremony, "{refused:?}");
}
#[test]
fn an_invite_is_redeemed_once_with_its_grant() {
use gunnar_grpc::Identity as _;
use nornir_identity::IdentityHost as _;
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "boss", gunnar_core::PrincipalKind::User);
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
plane
.grant("boss", "", "", gunnar_proto::GrantLevel::Admin)
.expect("boss is a server admin");
let identity = core(dir.path(), &plane);
let mut boss = identity
.host()
.store()
.user("boss")
.expect("read")
.expect("boss");
boss.state = nornir_identity::UserState::Active;
identity.host().store().put_user(&boss).expect("active");
let token = identity
.vouched_session("boss", true)
.expect("boss's session")
.token;
let invited = identity
.invite(
&token,
nornir_identity::InviteAsk {
display: "kim".into(),
groups: vec![gunnar_grpc::identity::SERVER_WRITE.into()],
..Default::default()
},
)
.expect("an operator's invite");
let c = identity
.redeem_start(&invited.secret, "", &relay())
.expect("the ceremony");
let kim = identity
.redeem_finish(
nornir_identity::test_host::SoftPasskey::new(7).register(&c, "https://console.example"),
)
.expect("redeemed");
let id = kim.session.user_id;
assert!(
plane
.may(
&id,
&gunnar_grpc::Access::Server,
gunnar_proto::GrantLevel::Write
)
.expect("asked"),
"Write applied"
);
assert!(!plane
.may(
&id,
&gunnar_grpc::Access::Server,
gunnar_proto::GrantLevel::Admin
)
.expect("asked"));
let again = identity
.redeem_start(&invited.secret, "", &relay())
.expect_err("once");
assert_eq!(
again.code,
nornir_identity::Code::InviteNotOpen,
"{again:?}"
);
assert_eq!(plane.credentials(Some(&id)).expect("listed").len(), 1);
}
fn kit_key() -> nornir_identity::test_host::SoftPasskey {
nornir_identity::test_host::SoftPasskey::new(3)
}
fn kit_approval() -> gunnar_grpc::MachineApproval {
let key = kit_key();
gunnar_grpc::MachineApproval {
credential_id: key.id.clone(),
public_key: key.public_key(),
..approval("kit-dwarf", b"", 3)
}
}
fn approval(machine: &str, id: &[u8], seed: u8) -> gunnar_grpc::MachineApproval {
gunnar_grpc::MachineApproval {
machine: machine.to_owned(),
server_level: gunnar_proto::GrantLevel::Admin,
credential_id: id.to_vec(),
public_key: passkey(seed),
label: format!("installer kit on {}", machine.trim_start_matches("kit-")),
sign_count: 0,
now_unix_ms: gunnar_core::now_ms(),
step_up: None,
}
}
#[test]
fn an_approved_machine_enrolment_makes_a_machine_with_its_credential_and_survives_a_restart() {
use gunnar_grpc::Identity as _;
let dir = tempfile::tempdir().expect("tempdir");
{
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane
.add_credential("kim", b"kim-yubikey", &passkey(1), "YubiKey", 5)
.expect("kim's passkey");
let mut backwards = kit_approval();
backwards.step_up = Some((b"kim-yubikey".to_vec(), 4));
let refused = plane
.approve_machine_enrolment(backwards)
.expect_err("a rolled-back counter");
assert!(
matches!(refused, gunnar_grpc::ControlError::PermissionDenied(_)),
"{refused:?}"
);
assert!(plane.principal("kit-dwarf").expect("read").is_none());
let mut fresh = kit_approval();
fresh.step_up = Some((b"kim-yubikey".to_vec(), 6));
let approved = plane.approve_machine_enrolment(fresh).expect("approved");
assert!(approved.created);
assert_eq!(
approved.principal.kind,
gunnar_proto::PrincipalKind::Machine as i32
);
assert_eq!(approved.credential.principal_id, "kit-dwarf");
assert_eq!(approved.credential.credential_id, kit_key().id);
assert_eq!(
plane
.credential(b"kim-yubikey")
.expect("read")
.expect("there")
.credential
.sign_count,
6,
"the step-up's counter is kept"
);
let again = plane
.approve_machine_enrolment(kit_approval())
.expect_err("an id once");
assert!(
matches!(again, gunnar_grpc::ControlError::AlreadyExists(_)),
"{again:?}"
);
}
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
let machine = plane
.principal("kit-dwarf")
.expect("read")
.expect("the machine is still there");
assert_eq!(machine.kind, gunnar_proto::PrincipalKind::Machine as i32);
assert!(plane
.may(
"kit-dwarf",
&gunnar_grpc::Access::Server,
gunnar_proto::GrantLevel::Admin
)
.expect("asked"));
assert_eq!(
plane.credentials(Some("kit-dwarf")).expect("listed").len(),
1
);
assert!(plane
.credentials(Some("kim"))
.expect("listed")
.iter()
.all(|c| c.credential_id != kit_key().id));
let plane = Arc::new(plane);
let identity = core(dir.path(), &plane);
let c = identity.login_start(&relay()).expect("a login");
let session = identity
.login_finish(kit_key().login(&c, "https://console.example", true))
.expect("the machine logs in");
assert_eq!(session.session.user_id, "kit-dwarf");
person(&stores, "kit-person", gunnar_core::PrincipalKind::User);
let refused = plane
.approve_machine_enrolment(approval("kit-person", b"other-key", 4))
.expect_err("a person under the id");
assert!(
matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)),
"{refused:?}"
);
assert!(plane.credential(b"other-key").expect("read").is_none());
}
#[test]
fn an_approved_machine_leaves_the_owners_first_invite_pending() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane
.claim_operator_named(Some("rickard"), "")
.expect("the operator");
let hash = gunnar_grpc::webauthn::sha256_hex(b"break-glass");
assert_eq!(
plane
.operator_bootstrap_invite(
"rickard",
&hash,
gunnar_core::now_ms(),
gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS
)
.expect("recorded"),
gunnar_grpc::BootstrapInvite::Recorded
);
plane
.approve_machine_enrolment(approval("kit-fresh", b"kit-fresh-key", 5))
.expect("approved");
assert!(
plane.first_invite().expect("read").is_some(),
"the owner's first invite is still pending"
);
assert_eq!(
plane
.operator_bootstrap_invite(
"rickard",
&hash,
gunnar_core::now_ms(),
gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS
)
.expect("asked"),
gunnar_grpc::BootstrapInvite::Renewed
);
assert!(
plane
.credentials(Some("rickard"))
.expect("listed")
.is_empty(),
"nothing on the operator"
);
}