use crate::control_plane::CoreControlPlane;
use crate::stores::ServerStores;
use gunnar_grpc::ControlPlane;
use std::collections::BTreeMap;
use std::sync::Arc;
pub const FEED_PRINCIPAL_PREFIX: &str = "principal.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FedPrincipal {
pub id: String,
pub kind: gunnar_proto::PrincipalKind,
pub admin: bool,
pub narrowed: bool,
pub key: String,
pub read: Option<String>,
pub replaces: bool,
}
pub const REPLACED_BY_OFFER: [&str; 3] = ["gunnar-ui", "gunnar-guest", gunnar_pairing_door::front::CERTIFY_PRINCIPAL];
pub const OFFERED_PRINCIPALS: [(&str, &str, &str); 4] = [
(OFFER_CONSOLE_PUBKEY, "gunnar-ui", "machine admin"),
(OFFER_MONETIZE_IDENTITY, "monetize", "machine admin"),
(OFFER_GUEST_PUBKEY, "gunnar-guest", "machine none"),
(gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY, gunnar_pairing_door::front::CERTIFY_PRINCIPAL, "machine admin"),
];
pub const OFFER_RESTORE_IDENTITY: &str = "restore_identity";
pub const RESTORE_NAMESPACE: &str = "monetize";
pub const OFFER_MONETIZE_IDENTITY: &str = "monetize_identity";
pub const OFFER_GUEST_PUBKEY: &str = "guest_pubkey";
pub const OFFER_CONSOLE_PUBKEY: &str = "console_pubkey";
pub fn offered_principals(feed: &BTreeMap<String, String>) -> Vec<(String, String)> {
OFFERED_PRINCIPALS
.iter()
.filter(|(_, id, _)| !feed.contains_key(&format!("{FEED_PRINCIPAL_PREFIX}{id}")))
.filter_map(|(offer, id, how)| nordisk_pairing::feedbus::offered(feed, offer).map(|key| (format!("{FEED_PRINCIPAL_PREFIX}{id}"), format!("{how} {key}"))))
.collect()
}
#[cfg(feature = "metered")]
const ROLES: &str = "admin|none|billing|repair";
#[cfg(not(feature = "metered"))]
const ROLES: &str = "admin|none|repair";
pub fn parse(feed: &BTreeMap<String, String>) -> Result<Vec<FedPrincipal>, String> {
let mut out = Vec::new();
for (k, v, offered) in feed.iter().map(|(k, v)| (k.clone(), v.clone(), false)).chain(offered_principals(feed).into_iter().map(|(k, v)| (k, v, true))) {
let (k, v) = (k.as_str(), v.as_str());
let Some(id) = k.strip_prefix(FEED_PRINCIPAL_PREFIX) else { continue };
let bad = |why: &str| Err(format!("REFUSED [fed-principal] `{k}`: {why} — the line is `<user|machine|deploy> <{ROLES}> <openssh public key>`"));
if id.is_empty() || id.contains('.') || id.contains('/') {
return bad("the id is empty or not a plain name");
}
let mut parts = v.trim().splitn(3, ' ');
let (Some(kind), Some(role), Some(key)) = (parts.next(), parts.next(), parts.next()) else {
return bad("fewer than three fields");
};
let kind = match kind {
"user" => gunnar_proto::PrincipalKind::User,
"machine" => gunnar_proto::PrincipalKind::Machine,
"deploy" => gunnar_proto::PrincipalKind::Deploy,
_ => return bad("the kind is not user, machine or deploy"),
};
let (admin, narrowed) = match role {
"admin" => (true, false),
"none" => (false, false),
#[cfg(feature = "metered")]
"billing" => (false, true),
"repair" if id == gunnar_grpc::REPAIR_PRINCIPAL => (false, false),
"repair" => return bad(&format!("the repair role is the principal `{}`'s alone", gunnar_grpc::REPAIR_PRINCIPAL)),
_ => return bad(&format!("the role is not one of {ROLES}")),
};
if gunnar_core::auth::parse_public_key(key.trim()).is_err() {
return bad("the key is not an OpenSSH public key line");
}
let replaces = offered && REPLACED_BY_OFFER.contains(&id);
out.push(FedPrincipal { id: id.to_string(), kind, admin, narrowed, key: key.trim().to_string(), read: None, replaces });
}
if let Some(key) = nordisk_pairing::feedbus::offered(feed, OFFER_RESTORE_IDENTITY) {
let parsed = gunnar_core::auth::parse_public_key(key.trim()).map_err(|_| format!("REFUSED [fed-principal] `{OFFER_RESTORE_IDENTITY}`: the key is not an OpenSSH public key line"))?;
let fp = gunnar_core::Fingerprint::of_key(&parsed);
let tag: String = fp.as_str().trim_start_matches("SHA256:").chars().filter(|c| c.is_ascii_alphanumeric()).take(12).collect::<String>().to_ascii_lowercase();
out.push(FedPrincipal { id: format!("monetize-restore-{tag}"), kind: gunnar_proto::PrincipalKind::Machine, admin: false, narrowed: false, key: key.trim().to_string(), read: Some(RESTORE_NAMESPACE.to_string()), replaces: false });
}
Ok(out)
}
pub const FEED_OPERATOR: &str = "operator_name";
pub fn operator_of(feed: &BTreeMap<String, String>) -> Option<String> {
feed.get(FEED_OPERATOR).map(|v| v.trim().to_owned()).filter(|v| !v.is_empty())
}
pub const OFFER_OPERATOR_CONSOLE_PUBKEY: &str = "operator_console_pubkey";
pub const OPERATOR_CONSOLE_COMMENT: &str = "gunnar-ui-operator";
pub const FEED_OPERATOR_INVITE: &str = "operator_invite_sha256";
pub const FEED_OPERATOR_INVITE_TTL: &str = "operator_invite_ttl_secs";
pub fn invite_ttl_ms(feed: &BTreeMap<String, String>) -> Result<i64, String> {
let Some(raw) = feed.get(FEED_OPERATOR_INVITE_TTL).map(|v| v.trim()).filter(|v| !v.is_empty()) else {
return Ok(gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS);
};
let secs: i64 = raw.parse().map_err(|_| format!("`{FEED_OPERATOR_INVITE_TTL}` is {raw:?}, not a number of seconds"))?;
gunnar_grpc::seam::bootstrap_ttl(secs.saturating_mul(1000)).map_err(|e| format!("`{FEED_OPERATOR_INVITE_TTL}` = {raw}: {e}"))
}
pub const FEED_OPERATOR_WELCOME: &str = "operator_welcome_readme";
pub const WELCOME_REPO: &str = "welcome";
pub const WELCOME_MADE_REL: &str = "pairing/welcome.made";
fn welcome(plane: &CoreControlPlane, stores: &Arc<ServerStores>, account: &str, readme: &str) -> Result<Option<String>, String> {
let made = stores.root().join(WELCOME_MADE_REL);
if made.exists() {
return Ok(None);
}
let fail = |what: &str, e: String| format!("the welcome repository of `{account}`: {what}: {e}");
let held = plane.list_repos(Some(account)).map_err(|e| fail("listing the account's repositories", e.to_string()))?;
let said = if held.is_empty() {
let path = gunnar_proto::RepoPath::new(account, WELCOME_REPO);
plane
.create_repo(gunnar_grpc::NewRepo {
path: path.clone(),
hash_kind: gunnar_proto::HashKind::Sha256,
default_branch: String::new(),
visibility: gunnar_proto::Visibility::Private,
description: "Welcome to your own Gunnar".to_owned(),
adopt: false,
})
.map_err(|e| fail("creating it", e.to_string()))?;
let commit = plane.write_first_commit(&path, readme).map_err(|e| fail("its README", e.to_string()))?;
tracing::info!(target: "gunnar::audit", event = "operator.welcome_repository", account, commit = %commit, "the operator's welcome repository is made: {account}/{WELCOME_REPO}, its README the installer's thanks");
format!("operator {account}: the welcome repository {account}/{WELCOME_REPO} made, its README the installer's thanks (commit {})", &commit[..commit.len().min(12)])
} else {
format!("operator {account}: holds {} repositor(ies) already — no welcome repository", held.len())
};
if let Some(d) = made.parent() {
std::fs::create_dir_all(d).map_err(|e| fail("its mark", e.to_string()))?;
}
std::fs::write(&made, b"made\n").map_err(|e| fail("its mark", e.to_string()))?;
Ok(Some(said))
}
pub fn fed_operator(root: &std::path::Path) -> Option<String> {
let text = crate::fed_at_rest::read(root, crate::pairing::JOINED_REL)?;
let reply = nordisk_pairing::join::JoinReply::parse(&text).ok()?;
operator_of(&reply.feed)
}
pub fn install(stores: &Arc<ServerStores>, feed: &BTreeMap<String, String>) -> Result<Vec<String>, String> {
let attach = keep_attach_ca(stores.root(), feed)?;
let mut wanted = parse(feed)?;
let operator = operator_of(feed);
if wanted.is_empty() && operator.is_none() {
return Ok(attach.into_iter().collect());
}
wanted.sort_by_key(|p| enrol_order(&p.id));
note_fed(stores, feed);
let plane = CoreControlPlane::new(Arc::clone(stores));
let mut said = Vec::new();
let mut refused = Vec::new();
for p in &wanted {
match install_one(&plane, stores, p) {
Ok(line) => said.push(line),
Err(e) => refused.push(e),
}
}
note_fed(stores, feed);
if let Some(name) = &operator {
match plane.claim_operator_named(Some(name), "") {
Ok(claim) => {
said.push(format!(
"operator {}: {}",
claim.principal.id,
match (claim.created, claim.adopted_namespace) {
(true, true) => "created (keyless, gunnar-admin), its account namespace ADOPTED — it existed with no owner",
(true, false) => "created (keyless, gunnar-admin) with its account namespace",
(false, _) => "already there",
}
));
if let Some(offered) = nordisk_pairing::feedbus::offered(feed, OFFER_OPERATOR_CONSOLE_PUBKEY) {
match operator_console(&plane, stores, &claim.principal.id, &offered) {
Ok(line) => said.push(line),
Err(e) => refused.push(e),
}
}
if let Some(text) = feed.get(FEED_OPERATOR_WELCOME).map(|t| t.trim()).filter(|t| !t.is_empty()) {
match welcome(&plane, stores, &claim.principal.id, text) {
Ok(Some(line)) => said.push(line),
Ok(None) => {}
Err(e) => refused.push(e),
}
}
if let Some(hash) = feed.get(FEED_OPERATOR_INVITE).map(|h| h.trim()).filter(|h| !h.is_empty()) {
let invite = invite_ttl_ms(feed).and_then(|ttl| {
gunnar_grpc::seam::bootstrap_hash(hash)
.and_then(|hash| plane.operator_bootstrap_invite(&claim.principal.id, &hash, gunnar_core::now_ms(), ttl))
.map_err(|e| e.to_string())
});
match invite {
Ok(gunnar_grpc::BootstrapInvite::Spent) => said.push(format!(
"operator {}: holds a passkey — the installer's first invite is spent",
claim.principal.id
)),
Ok(done) => said.push(format!(
"operator {}: the first invite (the break-glass token's hash) {} — the first passkey registers on this account",
claim.principal.id,
if done == gunnar_grpc::BootstrapInvite::Recorded { "recorded" } else { "renewed" }
)),
Err(e) => refused.push(format!("the operator `{}`'s first invite: {e}", claim.principal.id)),
}
}
}
Err(e) => refused.push(format!("the operator `{name}`: {e}")),
}
}
if !refused.is_empty() {
return Err(refused.join("; "));
}
said.extend(attach);
Ok(said)
}
pub fn enrol_order(id: &str) -> u8 {
match id {
gunnar_pairing_door::front::CERTIFY_PRINCIPAL => 0,
"gunnar-ui" => 1,
"monetize" => 2,
_ => 3,
}
}
pub fn note_fed(stores: &ServerStores, feed: &BTreeMap<String, String>) {
let Ok(wanted) = parse(feed) else {
stores.await_enrolment(BTreeMap::new());
return;
};
let auth = stores.auth();
let mut awaited = BTreeMap::new();
for p in &wanted {
let Ok(parsed) = gunnar_core::auth::parse_public_key(&p.key) else { continue };
let fingerprint = gunnar_core::Fingerprint::of_key(&parsed);
let Ok(held) = auth.keys_of(&p.id) else { continue };
if held.iter().any(|k| k.fingerprint_sha256 == fingerprint.as_str()) {
continue;
}
if held.is_empty() || p.replaces {
awaited.insert(fingerprint.as_str().to_string(), p.key.clone());
}
}
stores.await_enrolment(awaited);
}
fn operator_console(plane: &CoreControlPlane, stores: &Arc<ServerStores>, operator: &str, offered: &str) -> Result<String, String> {
let fail = |what: &str, e: String| format!("the console's credential for the operator `{operator}`: {what}: {e}");
let parsed = gunnar_core::auth::parse_public_key(offered).map_err(|e| fail("the offered key", format!("{e:#}")))?;
let fingerprint = gunnar_core::Fingerprint::of_key(&parsed);
let held = stores.auth().keys_of(operator).map_err(|e| fail("reading its keys", format!("{e:#}")))?;
if held.iter().any(|k| k.fingerprint_sha256 == fingerprint.as_str()) {
return Ok(format!("operator {operator}: the console's credential ({}): already its key", fingerprint.as_str()));
}
let mut did = Vec::new();
let stale: Vec<&gunnar_core::PublicKey> = held.iter().filter(|k| is_console_credential(&k.openssh)).collect();
for k in &stale {
plane.revoke_key(&k.fingerprint_sha256).map_err(|e| fail("revoking the credential a front laid before held", e.to_string()))?;
}
if !stale.is_empty() {
tracing::info!(operator, replaced = stale.len(), new = %fingerprint.as_str(), "fed principals: the front was laid again — the console's credential for the operator it held before is revoked");
did.push(format!("replaced the credential a front laid before held ({})", stale.iter().map(|k| k.fingerprint_sha256.as_str()).collect::<Vec<_>>().join(", ")));
}
let own = held.len() - stale.len();
if own > 0 {
tracing::warn!(
target: "gunnar::audit",
event = "operator.console_credential_waits",
operator,
fingerprint = %fingerprint.as_str(),
own,
"the console's credential for the operator is NOT enrolled at install: the account holds keys of the owner's own, so the console asks for his word"
);
did.push(format!("NOT enrolled — the account holds {own} key(s) of his own, so it takes his word (the console asks for it)"));
return Ok(format!("operator {operator}: the console's credential ({}): {}", fingerprint.as_str(), did.join(", ")));
}
plane
.add_key(operator, offered, "the console's credential for the operator, offered by the front", None, gunnar_core::BOOTSTRAP_ADMIN_ID, None)
.map_err(|e| fail("registering it", e.to_string()))?;
tracing::warn!(
target: "gunnar::audit",
event = "operator.console_credential_enrolled",
operator,
fingerprint = %fingerprint.as_str(),
"the console's credential for the operator is enrolled at install as the account's first key: the owner's first passkey finds it held"
);
did.push("enrolled as its first key (no attestation owed: it held none)".into());
Ok(format!("operator {operator}: the console's credential ({}): {}", fingerprint.as_str(), did.join(", ")))
}
fn is_console_credential(openssh: &str) -> bool {
openssh.split_whitespace().nth(2) == Some(OPERATOR_CONSOLE_COMMENT)
}
fn install_one(plane: &CoreControlPlane, stores: &Arc<ServerStores>, p: &FedPrincipal) -> Result<String, String> {
let auth = stores.auth();
{
let fail = |what: &str, e: String| format!("the fed principal `{}`: {what}: {e}", p.id);
let read_on: String;
let mut did = Vec::new();
if auth.principal(&p.id).map_err(|e| fail("reading it", format!("{e:#}")))?.is_none() {
plane.add_principal(&p.id, &p.id, p.kind).map_err(|e| fail("creating it", e.to_string()))?;
did.push("created");
}
let parsed = gunnar_core::auth::parse_public_key(&p.key).map_err(|e| fail("its key", format!("{e:#}")))?;
let fingerprint = gunnar_core::Fingerprint::of_key(&parsed);
if p.admin {
plane.grant(&p.id, "", "", gunnar_proto::GrantLevel::Admin).map_err(|e| fail("the gunnar-admin grant", e.to_string()))?;
did.push("gunnar-admin");
}
if let Some(ns) = &p.read {
plane.grant(&p.id, ns, "", gunnar_proto::GrantLevel::Read).map_err(|e| fail("the read grant", e.to_string()))?;
read_on = format!("read on {ns}");
did.push(&read_on);
}
#[cfg(feature = "metered")]
if p.narrowed && narrowed_of(stores.root(), &p.id).map_err(|e| fail("reading its narrowing", e))?.is_none() {
write_narrowed(stores.root(), &p.id, gunnar_grpc::BILLING_METHODS).map_err(|e| fail("narrowing it", e))?;
did.push("narrowed to billing");
}
let held = auth.keys_of(&p.id).map_err(|e| fail("reading its keys", format!("{e:#}")))?;
if !held.iter().any(|k| k.fingerprint_sha256 == fingerprint.as_str()) {
if p.replaces && !held.is_empty() {
for k in &held {
plane.revoke_key(&k.fingerprint_sha256).map_err(|e| fail("revoking the key the front laid before held", e.to_string()))?;
}
tracing::info!(principal = %p.id, replaced = held.len(), new = %fingerprint.as_str(), "fed principals: the front was laid again — its offered key replaces the one it held before");
did.push("replaced the key the front laid before held");
}
plane
.add_key(&p.id, &p.key, "fed by the installer", None, gunnar_core::BOOTSTRAP_ADMIN_ID, None)
.map_err(|e| fail("registering its key", e.to_string()))?;
did.push("key registered");
}
Ok(format!("{} ({}): {}", p.id, fingerprint.as_str(), if did.is_empty() { "already there".to_string() } else { did.join(", ") }))
}
}
pub const ATTACH_CA_REL: &str = "attach/ca.pem";
pub const FEED_ATTACH_CA: &str = "attach.ca";
pub fn keep_attach_ca(root: &std::path::Path, feed: &BTreeMap<String, String>) -> Result<Option<String>, String> {
let Some(pem) = feed.get(FEED_ATTACH_CA).map(|v| v.trim()).filter(|v| !v.is_empty()) else { return Ok(None) };
if !pem.starts_with("-----BEGIN CERTIFICATE-----") {
return Err(format!("REFUSED [fed-attach-ca] `{FEED_ATTACH_CA}` is not a PEM certificate"));
}
let path = root.join(ATTACH_CA_REL);
if std::fs::read_to_string(&path).map(|t| t.trim() == pem).unwrap_or(false) {
return Ok(Some("the attach CA: already kept".into()));
}
let dir = path.parent().expect("a parent");
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let tmp = path.with_extension("staging");
std::fs::write(&tmp, format!("{pem}\n")).map_err(|e| format!("{}: {e}", tmp.display()))?;
std::fs::rename(&tmp, &path).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(Some("the attach CA kept: the central monetize's client identity is trusted at the next start of the gRPC listener".into()))
}
pub fn grpc_client_cas(issuer_pem: &str, root: &std::path::Path) -> String {
match std::fs::read_to_string(root.join(ATTACH_CA_REL)) {
Ok(extra) if extra.contains("-----BEGIN CERTIFICATE-----") => format!("{}{}{extra}", issuer_pem, if issuer_pem.ends_with('\n') { "" } else { "\n" }),
_ => issuer_pem.to_string(),
}
}
pub const NARROWED_REL: &str = "narrowed";
fn narrowed_path(root: &std::path::Path, id: &str) -> Result<std::path::PathBuf, String> {
if id.is_empty()
|| id.starts_with('.')
|| id.contains("..")
|| !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
{
return Err(format!("`{id}` is not a plain principal id"));
}
Ok(root.join(NARROWED_REL).join(id))
}
pub fn write_narrowed(root: &std::path::Path, id: &str, methods: &[&str]) -> Result<(), String> {
let path = narrowed_path(root, id)?;
let dir = path.parent().expect("a parent");
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let tmp = path.with_extension("staging");
std::fs::write(&tmp, methods.join("\n") + "\n").map_err(|e| format!("{}: {e}", tmp.display()))?;
{
use std::os::unix::fs::PermissionsExt as _;
let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600));
}
std::fs::rename(&tmp, &path).map_err(|e| format!("{}: {e}", path.display()))
}
pub fn narrowed_of(root: &std::path::Path, id: &str) -> Result<Option<Vec<String>>, String> {
let path = narrowed_path(root, id)?;
match std::fs::read_to_string(&path) {
Ok(t) => Ok(Some(t.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_owned).collect())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("{}: {e}", path.display())),
}
}
pub fn restore_keys(feed: &BTreeMap<String, String>) -> Vec<String> {
let mut keys: Vec<String> = feed
.get(crate::pairing::FEED_ADMIN)
.map(|k| k.trim().to_owned())
.filter(|k| gunnar_core::auth::parse_public_key(k).is_ok())
.into_iter()
.collect();
keys.extend(parse(feed).unwrap_or_default().into_iter().filter(|p| p.admin || p.id == gunnar_grpc::REPAIR_PRINCIPAL).map(|p| p.key));
keys
}
pub fn restore_keys_at(root: &std::path::Path) -> Result<Vec<String>, String> {
let path = root.join(crate::pairing::JOINED_REL);
let Some(text) = crate::fed_at_rest::read_checked(root, crate::pairing::JOINED_REL).map_err(|e| format!("{e:#}"))? else { return Ok(Vec::new()) };
let reply = nordisk_pairing::join::JoinReply::parse(&text).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(restore_keys(&reply.feed))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_restore_is_taken_from_the_fed_admin_keys_and_from_no_other() {
let (admin, monetize, guest, console) = (a_key(), a_key(), a_key(), a_key());
let fed = feed(&[
("admin_pubkey", admin.as_str()),
("principal.monetize", &format!("machine admin {monetize}")),
("principal.gunnar-guest", &format!("machine none {guest}")),
("offer.vm1.console_pubkey", console.as_str()),
]);
let keys = restore_keys(&fed);
for k in [&admin, &monetize, &console] {
assert!(keys.contains(k), "{k} is fed as an admin, so it may restore: {keys:?}");
}
assert!(!keys.contains(&guest), "a principal with no grant is not one a restore is taken from");
assert!(restore_keys(&feed(&[("admin_pubkey", "not a key")])).is_empty(), "a line that does not parse names nobody");
assert!(restore_keys(&BTreeMap::new()).is_empty(), "no feed, no keys: the box refuses restores by name");
}
fn a_key() -> String {
let pem = gunnar_core::mint_ed25519().unwrap();
let k = gunnar_core::ssh_key::PrivateKey::from_openssh(&pem).unwrap();
k.public_key().to_openssh().unwrap()
}
fn feed(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn a_restore_identity_is_a_machine_of_its_own_that_reads_the_backup_namespace_only() {
let data = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(data.path()).unwrap());
stores.auth().bootstrap_admin(&a_key()).unwrap();
stores.catalog().create_namespace(RESTORE_NAMESPACE).unwrap();
let key = a_key();
let said = install(&stores, &feed(&[(&format!("offer.front.{OFFER_RESTORE_IDENTITY}"), &key)])).unwrap();
let p = parse(&feed(&[(&format!("offer.front.{OFFER_RESTORE_IDENTITY}"), &key)])).unwrap();
assert_eq!(p.len(), 1, "{said:?}");
let id = &p[0].id;
assert!(id.starts_with("monetize-restore-") && !p[0].admin && p[0].read.as_deref() == Some(RESTORE_NAMESPACE), "{p:?}");
assert!(stores.auth().principal(id).unwrap().is_some(), "{said:?}");
assert!(stores.auth().principal("monetize").unwrap().is_none(), "never the monetize principal");
assert!(said.iter().any(|l| l.contains("read on monetize")), "{said:?}");
let again = install(&stores, &feed(&[(&format!("offer.front.{OFFER_RESTORE_IDENTITY}"), &a_key())])).unwrap();
assert!(again.iter().all(|l| l.contains("created")), "{again:?}");
}
#[test]
fn the_three_estate_principals_parse_and_a_bad_line_is_refused_by_name() {
let key = a_key();
#[allow(non_snake_case)]
let KEY = key.as_str();
let f = feed(&[
("principal.rickard", &format!("user admin {KEY}")),
("principal.monetize", &format!("machine admin {KEY}")),
("principal.gunnar-guest", &format!("machine none {KEY}")),
("admin_pubkey", KEY),
("monetize_pubkey", "eb16"),
]);
let got = parse(&f).unwrap();
assert_eq!(got.len(), 3, "only principal.* keys");
let guest = got.iter().find(|p| p.id == "gunnar-guest").unwrap();
assert!(!guest.admin && guest.kind == gunnar_proto::PrincipalKind::Machine, "the guest gets NO grant");
assert!(got.iter().find(|p| p.id == "rickard").unwrap().admin);
for bad in [format!("robot admin {KEY}"), format!("user root {KEY}"), "user admin not-a-key".to_string(), "user admin".to_string()] {
let e = parse(&feed(&[("principal.x", &bad)])).unwrap_err();
assert!(e.starts_with("REFUSED [fed-principal] `principal.x`"), "{e}");
}
assert!(parse(&feed(&[("principal.", &format!("user admin {KEY}"))])).is_err());
let offered = parse(&feed(&[("offer.front.certify_pubkey", KEY)])).unwrap();
assert_eq!(offered.len(), 1);
assert!(offered[0].id == "front-certify" && offered[0].admin && offered[0].kind == gunnar_proto::PrincipalKind::Machine, "{offered:?}");
}
#[test]
fn the_fed_attach_ca_is_kept_and_joins_the_listeners_client_cas() {
let dir = tempfile::tempdir().unwrap();
let ca = "-----BEGIN CERTIFICATE-----\nATTACH\n-----END CERTIFICATE-----\n";
assert!(keep_attach_ca(dir.path(), &feed(&[])).unwrap().is_none(), "no attach.ca, nothing written");
assert!(keep_attach_ca(dir.path(), &feed(&[("attach.ca", ca)])).unwrap().is_some());
let issuer = "-----BEGIN CERTIFICATE-----\nISSUER\n-----END CERTIFICATE-----\n";
let both = grpc_client_cas(issuer, dir.path());
assert!(both.starts_with(issuer) && both.contains("ATTACH"), "{both}");
assert_eq!(grpc_client_cas(issuer, tempfile::tempdir().unwrap().path()), issuer, "no attach CA: the issuer alone, as before");
assert!(keep_attach_ca(dir.path(), &feed(&[("attach.ca", "not a pem")])).is_err(), "refused by name");
}
#[cfg(not(feature = "metered"))]
#[test]
fn an_open_build_refuses_a_billing_principal_by_name() {
let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC/NlSHUwzgG2pmW6qZhn0rXR0PFs1Nqgd1rLTUQgj+t";
let err = parse(&feed(&[("principal.monetize-central", &format!("machine billing {key}"))])).unwrap_err();
assert!(err.contains("the role is not one of admin|none"), "{err}");
}
#[cfg(feature = "metered")]
#[test]
fn a_billing_principal_holds_no_admin_grant_and_is_narrowed_to_monetizes_methods() {
let key = a_key();
let got = parse(&feed(&[("principal.monetize-central", &format!("machine billing {key}"))])).unwrap();
assert_eq!(got.len(), 1);
assert!(!got[0].admin && got[0].narrowed, "{got:?}");
let dir = tempfile::tempdir().unwrap();
write_narrowed(dir.path(), "monetize-central", gunnar_grpc::BILLING_METHODS).unwrap();
assert_eq!(narrowed_of(dir.path(), "monetize-central").unwrap().unwrap(), gunnar_grpc::BILLING_METHODS.iter().map(|m| m.to_string()).collect::<Vec<_>>());
assert_eq!(narrowed_of(dir.path(), "rickard").unwrap(), None, "a principal nobody narrowed");
assert!(narrowed_of(dir.path(), "../etc").is_err(), "a principal id is a plain name");
assert_eq!(narrowed_of(dir.path(), "acme.bob").unwrap(), None, "a dotted id is a plain name nobody narrowed");
assert!(narrowed_of(dir.path(), ".hidden").is_err() && narrowed_of(dir.path(), "a..b").is_err());
}
}
#[cfg(test)]
mod install_tests {
use super::*;
fn a_key() -> String {
let pem = gunnar_core::mint_ed25519().unwrap();
gunnar_core::ssh_key::PrivateKey::from_openssh(&pem).unwrap().public_key().to_openssh().unwrap()
}
#[test]
fn the_primary_installs_the_fed_principals_after_its_admin_and_again_changes_nothing() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
stores.auth().bootstrap_admin(&a_key()).unwrap();
let (op, mon, guest) = (a_key(), a_key(), a_key());
let feed: BTreeMap<String, String> = [
("principal.rickard".to_string(), format!("user admin {op}")),
("principal.monetize".to_string(), format!("machine admin {mon}")),
("principal.gunnar-guest".to_string(), format!("machine none {guest}")),
]
.into_iter()
.collect();
let said = install(&stores, &feed).unwrap();
assert_eq!(said.len(), 3, "{said:?}");
assert!(said.iter().all(|l| l.contains("created") && l.contains("key registered")), "{said:?}");
let auth = stores.auth();
assert_eq!(auth.principal("monetize").unwrap().unwrap().kind, gunnar_core::PrincipalKind::Machine);
assert_eq!(auth.keys_of("rickard").unwrap().len(), 1);
assert!(!auth.grants(Some("rickard")).unwrap().is_empty(), "the operator is gunnar-admin");
assert!(auth.grants(Some("gunnar-guest")).unwrap().is_empty(), "the guest has NO grant");
let again = install(&stores, &feed).unwrap();
assert!(again.iter().all(|l| !l.contains("created") && !l.contains("key registered")), "{again:?}");
assert_eq!(auth.keys_of("rickard").unwrap().len(), 1, "not registered twice");
}
#[test]
fn the_buyers_welcome_repository_is_made_once_with_the_sellers_readme() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
stores.auth().bootstrap_admin(&a_key()).unwrap();
let text = "# Welcome to your own Gunnar\n\nThank you for buying it.\n";
let feed: BTreeMap<String, String> = [(FEED_OPERATOR.to_string(), "acme-owner".to_string()), (FEED_OPERATOR_WELCOME.to_string(), text.to_string())].into_iter().collect();
let said = install(&stores, &feed).unwrap();
assert!(said.iter().any(|l| l.contains("acme-owner/welcome made")), "{said:?}");
let plane = CoreControlPlane::new(Arc::clone(&stores));
let repos = plane.list_repos(Some("acme-owner")).unwrap();
assert_eq!(repos.iter().map(|r| r.path.as_ref().map(|p| p.repo.clone()).unwrap_or_default()).collect::<Vec<_>>(), vec![WELCOME_REPO.to_string()], "the only repository");
assert_eq!(repos[0].visibility, gunnar_proto::Visibility::Private as i32, "the buyer's, private");
let again = install(&stores, &feed).unwrap();
assert!(!again.iter().any(|l| l.contains("welcome")), "made once: {again:?}");
assert_eq!(plane.list_repos(Some("acme-owner")).unwrap().len(), 1);
}
#[test]
fn every_listed_namespace_names_the_kind_of_the_principal_whose_account_it_is() {
use gunnar_grpc::ControlPlane as _;
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
stores.auth().bootstrap_admin(&a_key()).unwrap();
let feed: BTreeMap<String, String> = [
("principal.rickard".to_string(), format!("user admin {}", a_key())),
("principal.monetize".to_string(), format!("machine admin {}", a_key())),
("principal.dwarves-oden".to_string(), format!("machine none {}", a_key())),
]
.into_iter()
.collect();
install(&stores, &feed).unwrap();
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane.create_namespace("team", "").unwrap();
let kinds: BTreeMap<String, i32> = plane.list_namespaces().unwrap().into_iter().map(|n| (n.name, n.owner_kind)).collect();
assert_eq!(kinds["rickard"], gunnar_proto::PrincipalKind::User as i32);
assert_eq!(kinds["monetize"], gunnar_proto::PrincipalKind::Machine as i32);
assert_eq!(kinds["dwarves-oden"], gunnar_proto::PrincipalKind::Machine as i32);
assert_eq!(kinds["team"], gunnar_proto::PrincipalKind::Unspecified as i32, "no principal of that id");
use gunnar_core::{Principal, PrincipalKind as K};
let auth = stores.auth();
auth.add_principal(Principal::new("gunnar-twin", "gunnar-twin", K::Machine).unwrap()).unwrap();
auth.add_principal(Principal::new("twin", "twin", K::Group).unwrap()).unwrap();
auth.add_to_group("twin", "gunnar-twin").unwrap();
auth.add_principal(Principal::new("crew", "crew", K::Group).unwrap()).unwrap();
auth.add_to_group("crew", "rickard").unwrap();
auth.add_to_group("crew", "monetize").unwrap();
auth.add_principal(Principal::new("empty", "empty", K::Group).unwrap()).unwrap();
for ns in ["twin", "crew", "empty"] {
plane.create_namespace(ns, "").unwrap();
}
let kinds: BTreeMap<String, i32> = plane.list_namespaces().unwrap().into_iter().map(|n| (n.name, n.owner_kind)).collect();
assert_eq!(kinds["twin"], gunnar_proto::PrincipalKind::Machine as i32, "a group of machines only");
assert_eq!(kinds["crew"], gunnar_proto::PrincipalKind::Group as i32, "a person in it: a team");
assert_eq!(kinds["empty"], gunnar_proto::PrincipalKind::Group as i32, "no member: nobody said it is a machine");
}
#[test]
fn the_primary_enrols_monetize_the_guest_and_the_front_from_vm1s_offers() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let (op, mon, guest, certify) = (a_key(), a_key(), a_key(), a_key());
stores.auth().bootstrap_admin(&op).unwrap();
let feed: BTreeMap<String, String> = [
("admin_pubkey".to_string(), op.clone()),
(format!("offer.front.{OFFER_MONETIZE_IDENTITY}"), mon.clone()),
(format!("offer.front.{OFFER_GUEST_PUBKEY}"), guest.clone()),
(format!("offer.front.{}", gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY), certify),
("offer.main.ticket_pubkey".to_string(), "ab".repeat(32)),
]
.into_iter()
.collect();
let said = install(&stores, &feed).unwrap();
assert_eq!(said.len(), 3, "{said:?}");
let auth = stores.auth();
assert_eq!(auth.principal("monetize").unwrap().unwrap().kind, gunnar_core::PrincipalKind::Machine);
assert!(!auth.grants(Some("monetize")).unwrap().is_empty(), "monetize is gunnar-admin");
assert_eq!(auth.keys_of("gunnar-guest").unwrap().len(), 1);
assert!(auth.grants(Some("gunnar-guest")).unwrap().is_empty(), "the guest has NO grant");
assert!(!auth.grants(Some("front-certify")).unwrap().is_empty(), "the front certifies with an admin key");
assert!(install(&stores, &feed).unwrap().iter().all(|l| !l.contains("created") && !l.contains("key registered")), "idempotent");
let mut named = feed.clone();
named.insert("principal.monetize".into(), format!("machine none {mon}"));
assert!(!parse(&named).unwrap().iter().find(|p| p.id == "monetize").unwrap().admin);
assert_eq!(parse(&named).unwrap().iter().filter(|p| p.id == "monetize").count(), 1);
}
#[test]
fn a_front_laid_again_replaces_the_keys_it_mints_and_the_old_ones_stop_authenticating() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let certify_offer = format!("offer.front.{}", gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY);
let fed = |certify: &str, console: &str, guest: &str, mon: &str| -> BTreeMap<String, String> {
[
("admin_pubkey".to_string(), op.clone()),
(certify_offer.clone(), certify.to_string()),
(format!("offer.front.{OFFER_CONSOLE_PUBKEY}"), console.to_string()),
(format!("offer.front.{OFFER_GUEST_PUBKEY}"), guest.to_string()),
(format!("offer.front.{OFFER_MONETIZE_IDENTITY}"), mon.to_string()),
]
.into_iter()
.collect()
};
let (c1, k1, g1, mon) = (a_key(), a_key(), a_key(), a_key());
install(&stores, &fed(&c1, &k1, &g1, &mon)).unwrap();
let fp = |k: &str| gunnar_core::Fingerprint::of_key(&gunnar_core::auth::parse_public_key(k).unwrap());
let auth = stores.auth();
let (c2, k2, g2) = (a_key(), a_key(), a_key());
let said = install(&stores, &fed(&c2, &k2, &g2, &mon)).expect("the new front's keys are installed, not refused for an attestation");
for (id, old, new) in [("front-certify", &c1, &c2), ("gunnar-ui", &k1, &k2), ("gunnar-guest", &g1, &g2)] {
let held: Vec<String> = auth.keys_of(id).unwrap().into_iter().map(|k| k.fingerprint_sha256).collect();
assert_eq!(held, vec![fp(new).as_str().to_string()], "{id} holds the new front's key only: {said:?}");
assert!(auth.principal_by_fingerprint(&fp(old)).unwrap().is_none(), "{id}: the dead front's key no longer authenticates");
assert!(said.iter().any(|l| l.starts_with(id) && l.contains("replaced")), "{id}: said: {said:?}");
}
assert!(!auth.grants(Some("front-certify")).unwrap().is_empty(), "the certify key keeps its gunnar-admin grant");
assert!(install(&stores, &fed(&c2, &k2, &g2, &mon)).unwrap().iter().all(|l| !l.contains("created") && !l.contains("key registered") && !l.contains("replaced")), "idempotent");
let e = install(&stores, &fed(&c2, &k2, &g2, &a_key())).unwrap_err();
assert!(e.contains("monetize") && e.contains("attestation"), "{e}");
assert_eq!(auth.keys_of("monetize").unwrap().len(), 1);
}
fn a_full_feed(op: &str, certify: &str, console: &str, mon: &str, guest: &str) -> BTreeMap<String, String> {
[
("admin_pubkey".to_string(), op.to_string()),
("principal.dwarves-oden".to_string(), format!("machine admin {}", a_key())),
("principal.installer-seed".to_string(), format!("machine admin {}", a_key())),
(format!("offer.front.{}", gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY), certify.to_string()),
(format!("offer.front.{OFFER_CONSOLE_PUBKEY}"), console.to_string()),
(format!("offer.front.{OFFER_MONETIZE_IDENTITY}"), mon.to_string()),
(format!("offer.front.{OFFER_GUEST_PUBKEY}"), guest.to_string()),
]
.into_iter()
.collect()
}
#[test]
fn the_fronts_certify_key_is_enrolled_first_then_the_console_then_monetize() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let said = install(&stores, &a_full_feed(&op, &a_key(), &a_key(), &a_key(), &a_key())).unwrap();
let order: Vec<&str> = said.iter().map(|l| l.split(' ').next().unwrap_or("")).collect();
assert_eq!(&order[..3], &[gunnar_pairing_door::front::CERTIFY_PRINCIPAL, "gunnar-ui", "monetize"], "{said:#?}");
assert_eq!(order.len(), 6, "every fed principal is still enrolled: {said:#?}");
}
#[test]
fn a_fed_key_not_enrolled_yet_is_awaited_and_its_holder_told_to_ask_again() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let certify_pem = gunnar_core::mint_ed25519().unwrap();
let certify = gunnar_core::ssh_key::PrivateKey::from_openssh(&certify_pem).unwrap();
let certify_line = certify.public_key().to_openssh().unwrap();
let fp = |k: &str| gunnar_core::Fingerprint::of_key(&gunnar_core::auth::parse_public_key(k).unwrap()).as_str().to_string();
let mon = a_key();
let feed = a_full_feed(&op, &certify_line, &a_key(), &mon, &a_key());
let plane: Arc<dyn gunnar_grpc::ControlPlane> = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let gate = gunnar_grpc::AuthGate::new(Arc::clone(&plane), gunnar_grpc::AuthMode::Required);
let presented = |nonce: &str| {
let now = gunnar_core::now_ms() as i64;
let credential =
gunnar_grpc::sign_credential_for(&certify, now, nonce, "Admin.Health").unwrap();
let mut md = tonic::metadata::MetadataMap::new();
md.insert(
gunnar_grpc::CREDENTIAL_METADATA_KEY,
credential.to_header_value().parse().unwrap(),
);
gate.authenticate_bound(&md, now, Some("Admin.Health"))
};
assert_eq!(presented("nonce-before-the-feed"), Err(gunnar_grpc::AuthFailure::NotVerified), "nothing fed yet: unknown");
note_fed(&stores, &feed);
assert_eq!(plane.awaiting_enrolment(&fp(&certify_line)), Some(certify_line.clone()));
assert_eq!(presented("nonce-fed-and-awaited"), Err(gunnar_grpc::AuthFailure::Enrolling), "the front's certify key, fed and not enrolled yet");
install(&stores, &feed).unwrap();
assert_eq!(plane.awaiting_enrolment(&fp(&certify_line)), None);
assert_eq!(presented("nonce-once-enrolled-01").map(|c| c.principal_id), Ok(gunnar_pairing_door::front::CERTIFY_PRINCIPAL.to_string()));
let second = a_key();
let refeed = a_full_feed(&op, &certify_line, &a_key(), &second, &a_key());
note_fed(&stores, &refeed);
assert_eq!(plane.awaiting_enrolment(&fp(&second)), None, "monetize's second key is refused by install, not awaited");
assert!(install(&stores, &refeed).is_err());
assert_eq!(plane.awaiting_enrolment(&fp(&second)), None);
assert_eq!(plane.awaiting_enrolment(&fp(&mon)), None);
}
#[test]
fn a_principal_refused_for_now_does_not_hold_back_the_front_certify_key() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let certify_offer = format!("offer.front.{}", gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY);
let fed = |certify: &str, mon: &str| -> BTreeMap<String, String> {
[("admin_pubkey".to_string(), op.clone()), (certify_offer.clone(), certify.to_string()), (format!("offer.front.{OFFER_MONETIZE_IDENTITY}"), mon.to_string())].into_iter().collect()
};
let (c1, mon) = (a_key(), a_key());
install(&stores, &fed(&c1, &mon)).unwrap();
let c2 = a_key();
let e = install(&stores, &fed(&c2, &a_key())).unwrap_err();
assert!(e.contains("`monetize`"), "the refusal is named: {e}");
let fp = gunnar_core::Fingerprint::of_key(&gunnar_core::auth::parse_public_key(&c2).unwrap());
let held: Vec<String> = stores.auth().keys_of("front-certify").unwrap().into_iter().map(|k| k.fingerprint_sha256).collect();
assert_eq!(held, vec![fp.as_str().to_string()], "the new front's certify key is in, whatever monetize waits for");
assert!(install(&stores, &fed(&c2, &mon)).is_ok());
}
#[test]
fn a_fed_admin_key_never_authenticates_before_its_admin_grant_exists() {
let mut seen = Vec::new();
for round in 0..30 {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let feed: BTreeMap<String, String> = [("admin_pubkey".to_string(), op), (format!("offer.front.{}", gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY), a_key())].into_iter().collect();
let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
let (s2, d2) = (Arc::clone(&stores), Arc::clone(&done));
let watcher = std::thread::spawn(move || {
let mut bad = 0u32;
while !d2.load(std::sync::atomic::Ordering::SeqCst) {
let keys = s2.auth().keys_of("front-certify").map(|k| k.len()).unwrap_or(0);
if keys > 0 && s2.auth().grants(Some("front-certify")).map(|g| g.is_empty()).unwrap_or(false) {
bad += 1;
}
}
bad
});
install(&stores, &feed).unwrap();
done.store(true, std::sync::atomic::Ordering::SeqCst);
let bad = watcher.join().unwrap();
if bad > 0 {
seen.push(format!("round {round}: {bad} look(s) saw front-certify's key with no grant"));
}
}
assert!(seen.is_empty(), "the certify key authenticated with no grant: {seen:?}");
}
fn operator_feed(op: &str) -> BTreeMap<String, String> {
[("admin_pubkey".to_string(), op.to_string()), (FEED_OPERATOR.to_string(), "rickard".to_string())].into_iter().collect()
}
#[test]
fn the_fed_operator_is_claimed_at_install_keyless_gunnar_admin_with_its_account() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let said = install(&stores, &operator_feed(&op)).unwrap();
assert!(said.iter().any(|l| l.starts_with("operator rickard: created")), "{said:?}");
let auth = stores.auth();
let rickard = auth.principal("rickard").unwrap().expect("the operator exists after the install");
assert_eq!(rickard.kind, gunnar_core::PrincipalKind::User);
assert!(auth.keys_of("rickard").unwrap().is_empty(), "the operator must be KEYLESS: the console's key is its first");
assert!(
auth.grants(Some("rickard")).unwrap().iter().any(|g| matches!(g.scope, gunnar_core::Scope::Server) && g.level == gunnar_core::Level::Admin),
"the operator is the estate's admin"
);
assert!(stores.catalog().namespace("rickard").unwrap().is_some(), "its account namespace");
assert_eq!(auth.keys_of(gunnar_core::BOOTSTRAP_ADMIN_ID).unwrap().len(), 1);
let again = install(&stores, &operator_feed(&op)).unwrap();
assert!(again.iter().any(|l| l == "operator rickard: already there"), "{again:?}");
}
#[test]
fn a_namespace_a_first_push_conjured_for_the_operator_is_adopted_with_its_repositories() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane.create_namespace("rickard", "").unwrap();
plane
.create_repo(gunnar_grpc::NewRepo {
path: gunnar_proto::RepoPath::new("rickard", "t2-scratch"),
hash_kind: gunnar_proto::HashKind::Sha256,
default_branch: String::new(),
visibility: gunnar_proto::Visibility::Private,
description: String::new(),
adopt: false,
})
.unwrap();
let refused = plane.add_principal("rickard", "rickard", gunnar_proto::PrincipalKind::User).unwrap_err();
assert!(refused.to_string().contains("namespace already exists"), "{refused}");
let said = install(&stores, &operator_feed(&op)).unwrap();
assert!(said.iter().any(|l| l.contains("ADOPTED")), "{said:?}");
assert!(stores.auth().principal("rickard").unwrap().is_some());
assert!(gunnar_core::AuthTable::owns_account("rickard", "rickard"));
assert!(
plane.get_repo(&gunnar_proto::RepoPath::new("rickard", "t2-scratch")).is_ok(),
"the adopted account keeps what was pushed into it"
);
}
#[test]
fn the_claim_answers_the_fed_operator_whatever_it_was_asked() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
stores.auth().bootstrap_admin(&a_key()).unwrap();
std::fs::create_dir_all(dir.path().join("pairing")).unwrap();
std::fs::write(
dir.path().join(crate::pairing::JOINED_REL),
"estate = \"operator-claim-test\"\nrole = \"primary\"\npeers = []\n\n[me]\nname = \"main\"\noverlay = \"10.13.0.2\"\n\n[feed]\noperator_name = \"rickard\"\n",
)
.unwrap();
assert_eq!(fed_operator(dir.path()).as_deref(), Some("rickard"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
let claim = gunnar_grpc::ControlPlane::claim_operator(&plane, "Rickard-typed-something-else").unwrap();
assert_eq!(claim.principal.id, "rickard");
assert_eq!(claim.named_by, "fed");
assert!(claim.created);
let again = gunnar_grpc::ControlPlane::claim_operator(&plane, "").unwrap();
assert!(!again.created);
assert_eq!(again.principal, claim.principal, "the same row and the same stamp");
}
#[test]
fn a_feed_without_an_operator_claims_nobody() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let feed: BTreeMap<String, String> = [("admin_pubkey".to_string(), op)].into_iter().collect();
install(&stores, &feed).unwrap();
assert!(stores.auth().principal("rickard").unwrap().is_none());
let plane = CoreControlPlane::new(Arc::clone(&stores));
let refused = gunnar_grpc::ControlPlane::claim_operator(&plane, "").unwrap_err();
assert!(matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)), "{refused}");
}
fn a_console_credential() -> String {
format!("{} {OPERATOR_CONSOLE_COMMENT}", a_key())
}
fn fingerprint(line: &str) -> String {
gunnar_core::Fingerprint::of_key(&gunnar_core::auth::parse_public_key(line).unwrap()).as_str().to_owned()
}
fn held(stores: &ServerStores, id: &str) -> Vec<String> {
stores.auth().keys_of(id).unwrap().into_iter().map(|k| k.fingerprint_sha256).collect()
}
#[test]
fn the_fronts_operator_credential_is_the_operators_first_key_from_the_install() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let credential = a_console_credential();
let mut feed = operator_feed(&op);
feed.insert(format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), credential.clone());
let said = install(&stores, &feed).unwrap();
assert_eq!(held(&stores, "rickard"), vec![fingerprint(&credential)], "the operator's ONLY key is the console's credential: {said:?}");
assert!(said.iter().any(|l| l.contains("console's credential") && l.contains("first key")), "{said:?}");
let kept = stores.auth().keys_of("rickard").unwrap();
assert_eq!(kept[0].openssh.split_whitespace().nth(2), Some(OPERATOR_CONSOLE_COMMENT), "the mark is kept with the key: {}", kept[0].openssh);
let again = install(&stores, &feed).unwrap();
assert!(again.iter().any(|l| l.contains("console's credential") && l.contains("already")), "{again:?}");
assert_eq!(held(&stores, "rickard").len(), 1, "not registered twice");
assert_eq!(held(&stores, gunnar_core::BOOTSTRAP_ADMIN_ID).len(), 1);
}
#[test]
fn over_a_conjured_namespace_the_adopted_account_takes_the_consoles_credential() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
CoreControlPlane::new(Arc::clone(&stores)).create_namespace("rickard", "").unwrap();
let credential = a_console_credential();
let mut feed = operator_feed(&op);
feed.insert(format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), credential.clone());
let said = install(&stores, &feed).unwrap();
assert!(said.iter().any(|l| l.contains("ADOPTED")), "{said:?}");
assert_eq!(held(&stores, "rickard"), vec![fingerprint(&credential)], "{said:?}");
}
#[test]
fn a_front_laid_again_replaces_the_credential_the_dead_front_held() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let (first, second) = (a_console_credential(), a_console_credential());
let mut feed = operator_feed(&op);
feed.insert(format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), first.clone());
install(&stores, &feed).unwrap();
feed.insert(format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), second.clone());
let said = install(&stores, &feed).unwrap();
assert_eq!(held(&stores, "rickard"), vec![fingerprint(&second)], "the dead front's credential must be gone and the new one held: {said:?}");
assert!(said.iter().any(|l| l.contains("replaced")), "{said:?}");
}
#[test]
fn an_account_holding_the_owners_own_key_is_not_given_a_credential_over_his_head() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let (first, second, his) = (a_console_credential(), a_console_credential(), a_key());
let mut feed = operator_feed(&op);
feed.insert(format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), first.clone());
install(&stores, &feed).unwrap();
CoreControlPlane::new(Arc::clone(&stores)).add_key("rickard", &his, "laptop", None, "rickard", None).unwrap();
feed.insert(format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), second.clone());
let said = install(&stores, &feed).unwrap();
assert_eq!(held(&stores, "rickard"), vec![fingerprint(&his)], "his key stays, the dead front's credential goes, the new one waits for his word: {said:?}");
assert!(said.iter().any(|l| l.contains("NOT enrolled") && l.contains("of his own")), "{said:?}");
}
#[test]
fn an_offered_credential_with_no_operator_named_goes_nowhere() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let feed: BTreeMap<String, String> =
[("admin_pubkey".to_string(), op), (format!("offer.front.{OFFER_OPERATOR_CONSOLE_PUBKEY}"), a_console_credential())].into_iter().collect();
install(&stores, &feed).unwrap();
assert!(stores.auth().principal("rickard").unwrap().is_none());
assert_eq!(stores.auth().keys().unwrap().len(), 1, "only the bootstrap admin's key");
}
const BREAK_GLASS: &str = "uitest-break-glass-0000000000000000000000000001";
fn hash_of(secret: &str) -> String {
gunnar_grpc::webauthn::sha256_hex(secret.as_bytes())
}
fn a_passkey(id: &str) -> gunnar_core::model::WebCredential {
gunnar_core::model::WebCredential {
id: id.to_owned(),
principal_id: String::new(),
label: "YubiKey".to_owned(),
public_key_hex: format!("04{}", "11".repeat(64)),
sign_count: 0,
created: gunnar_core::now_ms(),
last_used: None,
sessions: Vec::new(),
}
}
#[test]
fn the_installers_bootstrap_invite_puts_the_first_passkey_on_the_operator_and_is_then_spent() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
CoreControlPlane::new(Arc::clone(&stores)).create_namespace("rickard", "").unwrap();
let mut feed = operator_feed(&op);
feed.insert(FEED_OPERATOR_INVITE.to_string(), hash_of(BREAK_GLASS));
let said = install(&stores, &feed).unwrap();
assert!(said.iter().any(|l| l.contains("first invite") && l.contains("recorded")), "{said:?}");
let invites = stores.auth().invites().unwrap();
assert_eq!(invites.len(), 1, "{invites:?}");
assert_eq!(invites[0].id, gunnar_grpc::seam::OPERATOR_INVITE_ID);
assert_eq!(invites[0].principal_id, "rickard");
assert_eq!(invites[0].secret_hash, hash_of(BREAK_GLASS), "the hash, never the token");
assert_eq!(invites[0].server_level, None, "an invite never makes an admin; the claim already did");
let again = install(&stores, &feed).unwrap();
assert!(again.iter().any(|l| l.contains("first invite") && l.contains("renewed")), "{again:?}");
assert_eq!(stores.auth().invites().unwrap().len(), 1);
let (credential, principal, level) = stores
.auth()
.register_web_credential(Some(&hash_of(BREAK_GLASS)), None, a_passkey("the-owners-yubikey"), gunnar_core::now_ms())
.expect("the break-glass token registers the operator's first passkey");
assert_eq!(principal.id, "rickard");
assert_eq!(credential.principal_id, "rickard");
assert_eq!(level, None);
assert!(stores.auth().invites().unwrap().is_empty(), "redeemed once");
let spent = install(&stores, &feed).unwrap();
assert!(spent.iter().any(|l| l.contains("spent")), "{spent:?}");
assert!(stores.auth().invites().unwrap().is_empty(), "no second bootstrap once he holds a passkey");
assert!(stores
.auth()
.register_web_credential(Some(&hash_of(BREAK_GLASS)), None, a_passkey("a-copied-token"), gunnar_core::now_ms())
.is_err());
}
#[test]
fn a_token_fed_in_place_of_its_hash_is_refused_and_nothing_is_recorded() {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let mut feed = operator_feed(&op);
feed.insert(FEED_OPERATOR_INVITE.to_string(), BREAK_GLASS.to_string());
let refused = install(&stores, &feed).unwrap_err();
assert!(refused.contains("SHA-256") && refused.contains("never the token"), "{refused}");
assert!(stores.auth().invites().unwrap().is_empty());
}
#[test]
fn a_sold_estates_first_login_invite_lives_as_the_feed_says_registers_once_and_is_then_spent() {
const FIRST_LOGIN: &str = "first-login-0000000000000000000000000000000000000000000000000001";
let week_secs: i64 = 7 * 24 * 60 * 60;
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
let op = a_key();
stores.auth().bootstrap_admin(&op).unwrap();
let mut feed: BTreeMap<String, String> = [
("admin_pubkey".to_string(), op.clone()),
(FEED_OPERATOR.to_string(), "acmebuyer".to_string()),
(FEED_OPERATOR_INVITE.to_string(), hash_of(FIRST_LOGIN)),
(FEED_OPERATOR_INVITE_TTL.to_string(), week_secs.to_string()),
]
.into_iter()
.collect();
let before = gunnar_core::now_ms();
let said = install(&stores, &feed).unwrap();
let after = gunnar_core::now_ms();
assert!(said.iter().any(|l| l.contains("operator acmebuyer")), "the buyer is the operator: {said:?}");
let invites = stores.auth().invites().unwrap();
assert_eq!(invites.len(), 1, "{invites:?}");
assert_eq!(invites[0].principal_id, "acmebuyer");
let lives = invites[0].expires - invites[0].created;
assert_eq!(lives, week_secs * 1000, "the invite lives as long as the feed says, not {} days", lives / 86_400_000);
assert!(invites[0].expires >= before + week_secs * 1000 && invites[0].expires <= after + week_secs * 1000);
let late = stores.auth().register_web_credential(Some(&hash_of(FIRST_LOGIN)), Some("acmebuyer"), a_passkey("too-late"), invites[0].expires + 1);
assert!(late.is_err(), "an expired first-login link registers nothing");
let (credential, principal, level) = stores
.auth()
.register_web_credential(Some(&hash_of(FIRST_LOGIN)), Some("acmebuyer"), a_passkey("the-buyers-key"), gunnar_core::now_ms())
.expect("the first-login link registers the buyer's first passkey");
assert_eq!((principal.id.as_str(), credential.principal_id.as_str(), level), ("acmebuyer", "acmebuyer", None));
assert!(stores.auth().invites().unwrap().is_empty(), "redeemed once");
assert!(
stores.auth().register_web_credential(Some(&hash_of(FIRST_LOGIN)), Some("acmebuyer"), a_passkey("a-second-key"), gunnar_core::now_ms()).is_err(),
"a link used once registers nothing more"
);
let spent = install(&stores, &feed).unwrap();
assert!(spent.iter().any(|l| l.contains("spent")), "{spent:?}");
assert!(stores.auth().invites().unwrap().is_empty());
for bad in ["soon", "0", "59", &((gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS / 1000) + 1).to_string()] {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
stores.auth().bootstrap_admin(&op).unwrap();
feed.insert(FEED_OPERATOR_INVITE_TTL.to_string(), bad.to_string());
let refused = install(&stores, &feed).unwrap_err();
assert!(refused.contains(FEED_OPERATOR_INVITE_TTL), "{bad:?}: {refused}");
assert!(stores.auth().invites().unwrap().is_empty(), "{bad:?}");
}
}
}