Member tokens: reusable account credential authorizing key enrollment per node
This commit is contained in:
+47
-31
@@ -90,40 +90,50 @@ async fn application_pending_then_approve_then_enroll_binds_key() {
|
||||
.unwrap();
|
||||
assert_eq!(dup.status(), reqwest::StatusCode::CONFLICT);
|
||||
|
||||
// MA approves: member stub + invite; enrollment binds the key
|
||||
// MA approves: member stub; a member token then authorizes key enrollment
|
||||
commands::registry_approve(&dir, id, None).unwrap();
|
||||
let signed = registry::load_registry(&dir.join("registry.json")).unwrap();
|
||||
assert!(signed.doc.members.iter().any(|member| member.id == id));
|
||||
let apps = registry::load_applications(&dir.join("applications.json")).unwrap();
|
||||
assert_eq!(apps[0].status, "approved");
|
||||
|
||||
let key = Keypair::generate();
|
||||
let enrolled = http
|
||||
.post(format!("{base}/v1/enroll"))
|
||||
.json(&serde_json::json!({
|
||||
"id": id,
|
||||
"token": invite_token(&dir, id),
|
||||
"pubkey": key.public_hex(),
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(enrolled.status().is_success());
|
||||
let signed = registry::load_registry(&dir.join("registry.json")).unwrap();
|
||||
assert!(registry::authorized_keys(&signed, now_ts()).contains_key(&key.public_hex()));
|
||||
let enroll = |token: &str, pubkey: String| {
|
||||
let http = http.clone();
|
||||
let base = base.clone();
|
||||
let id = id.to_string();
|
||||
let token = token.to_string();
|
||||
async move {
|
||||
http.post(format!("{base}/v1/enroll"))
|
||||
.json(&serde_json::json!({
|
||||
"id": id, "token": token, "pubkey": pubkey,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status()
|
||||
}
|
||||
};
|
||||
|
||||
// the token is single-use
|
||||
let replayed = http
|
||||
.post(format!("{base}/v1/enroll"))
|
||||
.json(&serde_json::json!({
|
||||
"id": id,
|
||||
"token": invite_token(&dir, id),
|
||||
"pubkey": Keypair::generate().public_hex(),
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(replayed.status(), reqwest::StatusCode::FORBIDDEN);
|
||||
// the account credential is reusable: two nodes, two keys, one token
|
||||
let token = commands::registry_token(&dir, id, None).unwrap();
|
||||
let key1 = Keypair::generate();
|
||||
let key2 = Keypair::generate();
|
||||
assert!(enroll(&token, key1.public_hex()).await.is_success());
|
||||
assert!(enroll(&token, key2.public_hex()).await.is_success());
|
||||
let signed = registry::load_registry(&dir.join("registry.json")).unwrap();
|
||||
let authorized = registry::authorized_keys(&signed, now_ts());
|
||||
assert!(authorized.contains_key(&key1.public_hex()));
|
||||
assert!(authorized.contains_key(&key2.public_hex()));
|
||||
|
||||
// after revocation the token no longer enrolls; a fresh one does
|
||||
commands::registry_revoke_token(&dir, id).unwrap();
|
||||
let key3 = Keypair::generate();
|
||||
assert_eq!(
|
||||
enroll(&token, key3.public_hex()).await,
|
||||
reqwest::StatusCode::FORBIDDEN
|
||||
);
|
||||
let fresh = commands::registry_token(&dir, id, None).unwrap();
|
||||
assert!(enroll(&fresh, key3.public_hex()).await.is_success());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
@@ -231,16 +241,22 @@ async fn invite_reissues_token_per_node() {
|
||||
}
|
||||
};
|
||||
|
||||
// node 1: the invite from approval
|
||||
// node 1: a single-use handoff invite
|
||||
commands::registry_invite(&dir, id, None).unwrap();
|
||||
let key1 = Keypair::generate();
|
||||
let token1 = invite_token(&dir, id);
|
||||
assert!(enroll(token1.clone(), key1.public_hex()).await.is_success());
|
||||
|
||||
// the invite is single-use: replay is rejected
|
||||
assert_eq!(
|
||||
enroll(token1, Keypair::generate().public_hex()).await,
|
||||
reqwest::StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
// node 2: a fresh token from `registry invite`
|
||||
commands::registry_invite(&dir, id, None).unwrap();
|
||||
let key2 = Keypair::generate();
|
||||
let token2 = invite_token(&dir, id);
|
||||
assert_ne!(token1, token2);
|
||||
assert!(enroll(token2, key2.public_hex()).await.is_success());
|
||||
|
||||
let signed = registry::load_registry(&dir.join("registry.json")).unwrap();
|
||||
@@ -261,10 +277,10 @@ async fn wizard_enrolls_and_writes_config() {
|
||||
submit_application(&http, &base, "Wizard Test").await;
|
||||
let id = "wizard-test.frx.invalid";
|
||||
commands::registry_approve(&dir, id, None).unwrap();
|
||||
let token = commands::registry_token(&dir, id, None).unwrap();
|
||||
let signed = registry::load_registry(&dir.join("registry.json")).unwrap();
|
||||
let credentials = format!(
|
||||
"id={id} token={} registry={base}/registry.json ma_key={}",
|
||||
invite_token(&dir, id),
|
||||
"id={id} token={token} registry={base}/registry.json ma_key={}",
|
||||
signed.doc.ma_key
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user