use std::sync::Arc;
use gunnar_grpc::auth::AuthGate;
use gunnar_grpc::{
sign_credential_for, AccessService, AuthMode, Caller, ControlPlane, EventBus,
MemoryControlPlane, ServerConfig, ServiceContext, CREDENTIAL_METADATA_KEY,
};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::*;
use tonic::{Code, Request};
const NS: &str = "nordisk";
const REPO: &str = "gunnar";
fn mint() -> ssh_key::PrivateKey {
ssh_key::PrivateKey::random(
&mut rand::rand_core::UnwrapErr(rand::rngs::SysRng),
ssh_key::Algorithm::Ed25519,
)
.expect("mint an ed25519 key")
}
fn openssh_of(key: &ssh_key::PrivateKey) -> String {
key.public_key()
.to_openssh()
.expect("render the public key")
}
fn fingerprint_of(key: &ssh_key::PrivateKey) -> String {
key.public_key()
.fingerprint(ssh_key::HashAlg::Sha256)
.to_string()
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("after 1970")
.as_millis() as i64
}
struct Harness {
plane: Arc<MemoryControlPlane>,
ctx: ServiceContext,
}
impl Harness {
fn new() -> Harness {
let plane = Arc::new(MemoryControlPlane::with_repo(NS, REPO));
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(64)),
ServerConfig::default(),
);
assert_eq!(ctx.config.auth, AuthMode::Required, "the shipped default");
Harness { plane, ctx }
}
fn person(&self, name: &str) -> (String, ssh_key::PrivateKey) {
let key = mint();
let principal = self
.plane
.add_principal(name, name, PrincipalKind::User)
.expect("principal added");
self.plane
.add_key(&principal.id, &openssh_of(&key), name, None, "", None)
.expect("key registered");
(principal.id, key)
}
fn caller_for(&self, key: &ssh_key::PrivateKey, nonce: &str) -> Caller {
let credential = sign_credential_for(key, now_ms(), nonce, "Admin.Health").expect("sign");
let mut request = Request::new(());
request.metadata_mut().insert(
CREDENTIAL_METADATA_KEY,
credential.to_header_value().parse().expect("ascii"),
);
AuthGate::new(Arc::clone(&self.ctx.control), self.ctx.config.auth)
.authenticate_bound(request.metadata(), now_ms(), Some("Admin.Health"))
.expect("a freshly signed credential from a registered key must verify")
}
fn authed<T>(&self, caller: &Caller, message: T) -> Request<T> {
let mut request = Request::new(message);
request.extensions_mut().insert(caller.clone());
request
}
fn access(&self) -> AccessService {
AccessService::new(self.ctx.clone())
}
fn keys_of(&self, principal: &str) -> Vec<String> {
let mut keys: Vec<String> = self
.plane
.list_keys(Some(principal))
.expect("list")
.into_iter()
.map(|k| k.fingerprint)
.collect();
keys.sort();
keys
}
}
fn add(principal: &str, key: &ssh_key::PrivateKey) -> AddKeyRequest {
AddKeyRequest {
principal_id: principal.to_owned(),
openssh_public_key: openssh_of(key),
comment: "laptop".to_owned(),
expires_at_unix_ms: None,
attestation: None,
}
}
#[tokio::test]
async fn a_person_adds_a_key_to_themselves_with_no_grant_and_no_attestation() {
let harness = Harness::new();
let (kim, kim_key) = harness.person("kim");
let caller = harness.caller_for(&kim_key, "nonce-self-add-0001");
let laptop = mint();
let added = harness
.access()
.add_key(harness.authed(&caller, add(&kim, &laptop)))
.await
.expect("a person may add a key to their own principal")
.into_inner()
.key
.expect("the key comes back");
assert_eq!(added.principal_id, kim);
assert_eq!(added.fingerprint, fingerprint_of(&laptop));
let mut want = vec![fingerprint_of(&kim_key), fingerprint_of(&laptop)];
want.sort();
assert_eq!(harness.keys_of(&kim), want);
}
#[tokio::test]
async fn a_person_may_not_add_a_key_to_anybody_else() {
let harness = Harness::new();
let (_kim, kim_key) = harness.person("kim");
let (ola, ola_key) = harness.person("ola");
let caller = harness.caller_for(&kim_key, "nonce-self-add-0002");
let status = harness
.access()
.add_key(harness.authed(&caller, add(&ola, &mint())))
.await
.expect_err("a key on another person is not the caller's to add");
assert_eq!(status.code(), Code::PermissionDenied, "{status:?}");
assert_eq!(harness.keys_of(&ola), vec![fingerprint_of(&ola_key)]);
}
#[tokio::test]
async fn a_person_lists_their_own_keys_and_nobody_elses() {
let harness = Harness::new();
let (kim, kim_key) = harness.person("kim");
let (ola, _ola_key) = harness.person("ola");
let caller = harness.caller_for(&kim_key, "nonce-self-list-001");
let keys = harness
.access()
.list_keys(harness.authed(
&caller,
ListKeysRequest {
principal_id: kim.clone(),
},
))
.await
.expect("a person may list their own keys")
.into_inner()
.keys;
assert_eq!(
keys.iter()
.map(|k| k.fingerprint.clone())
.collect::<Vec<_>>(),
vec![fingerprint_of(&kim_key)]
);
for (asked, what) in [
(ola.as_str(), "another person's keys"),
("", "every key on the box"),
] {
let status = harness
.access()
.list_keys(harness.authed(
&caller,
ListKeysRequest {
principal_id: asked.to_owned(),
},
))
.await
.expect_err(what);
assert_eq!(status.code(), Code::PermissionDenied, "{what}: {status:?}");
}
}
#[tokio::test]
async fn a_person_revokes_their_own_key_and_the_refusal_for_anothers_names_nobody() {
let harness = Harness::new();
let (kim, kim_key) = harness.person("kim");
let (ola, ola_key) = harness.person("ola");
let laptop = mint();
harness
.plane
.add_key(&kim, &openssh_of(&laptop), "laptop", None, &kim, None)
.expect("a second key of kim's");
let caller = harness.caller_for(&kim_key, "nonce-self-revoke-1");
harness
.access()
.revoke_key(harness.authed(
&caller,
RevokeKeyRequest {
fingerprint: fingerprint_of(&laptop),
},
))
.await
.expect("a person may revoke their own key");
assert_eq!(harness.keys_of(&kim), vec![fingerprint_of(&kim_key)]);
let theirs = harness
.access()
.revoke_key(harness.authed(
&caller,
RevokeKeyRequest {
fingerprint: fingerprint_of(&ola_key),
},
))
.await
.expect_err("another person's key is not the caller's to revoke");
let nobodys = harness
.access()
.revoke_key(harness.authed(
&caller,
RevokeKeyRequest {
fingerprint: "SHA256:no-such-key-anywhere".to_owned(),
},
))
.await
.expect_err("a key that does not exist is not the caller's either");
assert_eq!(theirs.code(), Code::PermissionDenied, "{theirs:?}");
assert_eq!(nobodys.code(), Code::PermissionDenied, "{nobodys:?}");
assert!(
!theirs.message().contains(&ola),
"the refusal says whose the key is: {}",
theirs.message()
);
assert_eq!(
theirs.message(),
nobodys.message(),
"another person's key is told apart from no key at all"
);
assert_eq!(harness.keys_of(&ola), vec![fingerprint_of(&ola_key)]);
}
#[tokio::test]
async fn a_person_may_revoke_the_key_they_are_signing_with() {
let harness = Harness::new();
let (kim, kim_key) = harness.person("kim");
let caller = harness.caller_for(&kim_key, "nonce-self-revoke-2");
harness
.access()
.revoke_key(harness.authed(
&caller,
RevokeKeyRequest {
fingerprint: fingerprint_of(&kim_key),
},
))
.await
.expect("the signing key is the caller's own");
assert!(harness.keys_of(&kim).is_empty());
}
#[tokio::test]
async fn an_admin_adding_to_another_still_owes_their_attestation() {
let harness = Harness::new();
let (admin, admin_key) = harness.person("admin");
harness.plane.grant_server(&admin, GrantLevel::Admin);
let (kim, kim_key) = harness.person("kim");
let caller = harness.caller_for(&admin_key, "nonce-admin-add-001");
let status = harness
.access()
.add_key(harness.authed(&caller, add(&kim, &mint())))
.await
.expect_err("kim holds a key, so an admin's add owes kim's attestation");
assert_ne!(status.code(), Code::Ok);
assert_eq!(harness.keys_of(&kim), vec![fingerprint_of(&kim_key)]);
}