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 first_invite(stores: &ServerStores) -> Option<gunnar_core::model::FirstInviteRow> {
stores
.auth()
.first_invite(gunnar_grpc::seam::OPERATOR_INVITE_ID)
.unwrap()
}
fn core(
dir: &std::path::Path,
stores: &Arc<ServerStores>,
) -> Arc<gunnar_grpc::identity::GunnarIdentity> {
let control: Arc<dyn gunnar_grpc::ControlPlane> =
Arc::new(CoreControlPlane::new(Arc::clone(stores)));
crate::identity::build(dir, control, &gunnar_grpc::ServerConfig::default(), false)
}
fn register(
identity: &gunnar_grpc::identity::GunnarIdentity,
secret: &str,
seed: u8,
) -> Result<String, nornir_identity::Refused> {
let relay = nornir_identity::Relay {
rp_id: "console.example".into(),
origin: "https://console.example".into(),
client: None,
};
let c = identity.redeem_start(secret, "", &relay)?;
let made = identity.redeem_finish(
nornir_identity::test_host::SoftPasskey::new(seed)
.register(&c, "https://console.example"),
)?;
Ok(made.session.user_id)
}
#[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 invite = first_invite(&stores).expect("the first invite");
assert_eq!(invite.principal_id, "rickard");
assert_eq!(
invite.secret_hash,
hash_of(BREAK_GLASS),
"the hash, never the token"
);
assert_eq!(
invite.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!(first_invite(&stores).is_some());
let identity = core(dir.path(), &stores);
let owner = register(&identity, BREAK_GLASS, 0x37)
.expect("the break-glass token registers the operator's first passkey");
assert_eq!(owner, "rickard");
assert_eq!(
stores
.auth()
.web_credentials(Some("rickard"))
.unwrap()
.len(),
1,
"on his account"
);
assert!(first_invite(&stores).is_none(), "redeemed once");
let spent = install(&stores, &feed).unwrap();
assert!(spent.iter().any(|l| l.contains("spent")), "{spent:?}");
assert!(
first_invite(&stores).is_none(),
"no second bootstrap once he holds a passkey"
);
assert!(register(&identity, BREAK_GLASS, 0x38).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!(first_invite(&stores).is_none());
}
#[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 invite = first_invite(&stores).expect("the first-login invite");
assert_eq!(invite.principal_id, "acmebuyer");
let lives = invite.expires - invite.created;
assert_eq!(
lives,
week_secs * 1000,
"the invite lives as long as the feed says, not {} days",
lives / 86_400_000
);
assert!(
invite.expires >= before + week_secs * 1000
&& invite.expires <= after + week_secs * 1000
);
let identity = core(dir.path(), &stores);
let buyer = register(&identity, FIRST_LOGIN, 0x41)
.expect("the first-login link registers the buyer's first passkey");
assert_eq!(buyer, "acmebuyer");
assert!(first_invite(&stores).is_none(), "redeemed once");
assert!(
register(&identity, FIRST_LOGIN, 0x42).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!(first_invite(&stores).is_none());
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!(first_invite(&stores).is_none(), "{bad:?}");
}
}
}