Preserve authoritative execution grant aggregates
This commit is contained in:
parent
0e351c3640
commit
8a4999344d
2 changed files with 84 additions and 52 deletions
|
|
@ -576,22 +576,6 @@ impl CredentialRegistry {
|
||||||
Ok(remora_domain::ports::ActiveCredentialGrant::for_execution_grant(grant_id.clone()))
|
Ok(remora_domain::ports::ActiveCredentialGrant::for_execution_grant(grant_id.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Idempotently revoke an execution grant's credential access.
|
|
||||||
pub async fn revoke_execution_grant(&self, grant_id: &OpaqueId) -> Result<(), CredentialError> {
|
|
||||||
let now = OffsetDateTime::now_utc();
|
|
||||||
sqlx::query(
|
|
||||||
"UPDATE execution_grant_credential_pins
|
|
||||||
SET status = 'revoked', revoked_at = COALESCE(revoked_at, $2)
|
|
||||||
WHERE grant_id = $1",
|
|
||||||
)
|
|
||||||
.bind(grant_id.as_str())
|
|
||||||
.bind(now)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(DurableError::from)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the durable pin for an active execution grant (fresh activity check).
|
/// Load the durable pin for an active execution grant (fresh activity check).
|
||||||
pub async fn load_active_grant_pin(
|
pub async fn load_active_grant_pin(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -103,12 +103,55 @@ fn token_material(value: &str) -> SecretMaterial {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn bind_grant(
|
async fn bind_grant(
|
||||||
|
pool: &PgPool,
|
||||||
registry: &CredentialRegistry,
|
registry: &CredentialRegistry,
|
||||||
label: &str,
|
label: &str,
|
||||||
pin: &CredentialGrantVersion,
|
pin: &CredentialGrantVersion,
|
||||||
) -> ActiveCredentialGrant {
|
) -> ActiveCredentialGrant {
|
||||||
|
let grant_id = grant_id(label);
|
||||||
|
let job_id = format!("job-{label}-aaaaaaaa");
|
||||||
|
let sensitive_source_ref = format!("source-{label}-aaaaaaaa");
|
||||||
|
let mut tx = pool.begin().await.expect("begin authoritative grant");
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO sensitive_source_inputs (sensitive_source_ref, source_input, created_at)
|
||||||
|
VALUES ($1, '{}'::jsonb, now())",
|
||||||
|
)
|
||||||
|
.bind(&sensitive_source_ref)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.expect("insert sensitive source");
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO execution_grants (
|
||||||
|
grant_id, owner_id, target_store, target_bucket, target_prefix,
|
||||||
|
policy_version, credential_id, credential_version_id, status, created_at
|
||||||
|
)
|
||||||
|
VALUES ($1, 'owner-fixture-aaaaaaaa', 'fixture', 'fixture', '[]'::jsonb,
|
||||||
|
'fixture', $2, $3, 'active', now())",
|
||||||
|
)
|
||||||
|
.bind(grant_id.as_str())
|
||||||
|
.bind(pin.credential_id.as_str())
|
||||||
|
.bind(pin.version_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.expect("insert authoritative grant");
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO jobs (
|
||||||
|
job_id, owner_id, phase, lifecycle, source_projection, sensitive_source_ref,
|
||||||
|
target_store, target_bucket, target_prefix, execution_grant_id,
|
||||||
|
dispatch_needed, created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, 'owner-fixture-aaaaaaaa', 'queued', 'visible', '{}'::jsonb, $2,
|
||||||
|
'fixture', 'fixture', '[]'::jsonb, $3, false, now(), now())",
|
||||||
|
)
|
||||||
|
.bind(job_id)
|
||||||
|
.bind(sensitive_source_ref)
|
||||||
|
.bind(grant_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.expect("insert authoritative job");
|
||||||
|
tx.commit().await.expect("commit authoritative grant");
|
||||||
registry
|
registry
|
||||||
.bind_execution_grant(&grant_id(label), pin)
|
.bind_execution_grant(&grant_id, pin)
|
||||||
.await
|
.await
|
||||||
.expect("bind grant")
|
.expect("bind grant")
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +225,7 @@ async fn read_http_message(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn create_pin_and_exact_version_resolution() {
|
async fn create_pin_and_exact_version_resolution() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("exact1");
|
let owner = owner("exact1");
|
||||||
|
|
@ -200,7 +243,7 @@ async fn create_pin_and_exact_version_resolution() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("pin");
|
.expect("pin");
|
||||||
let access = bind_grant(®istry, "exact1", &pin).await;
|
let access = bind_grant(&pool, ®istry, "exact1", &pin).await;
|
||||||
|
|
||||||
let secret = resolver
|
let secret = resolver
|
||||||
.resolve_access(&access)
|
.resolve_access(&access)
|
||||||
|
|
@ -259,7 +302,7 @@ async fn vault_create_only_write_rejects_an_occupied_intent_path() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rotation_does_not_change_prior_pin_and_rejects_substitution() {
|
async fn rotation_does_not_change_prior_pin_and_rejects_substitution() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("rotate");
|
let owner = owner("rotate");
|
||||||
|
|
@ -277,7 +320,7 @@ async fn rotation_does_not_change_prior_pin_and_rejects_substitution() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("first pin");
|
.expect("first pin");
|
||||||
let first_access = bind_grant(®istry, "rotate-a", &first_pin).await;
|
let first_access = bind_grant(&pool, ®istry, "rotate-a", &first_pin).await;
|
||||||
|
|
||||||
registry
|
registry
|
||||||
.rotate(
|
.rotate(
|
||||||
|
|
@ -291,7 +334,7 @@ async fn rotation_does_not_change_prior_pin_and_rejects_substitution() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("second pin");
|
.expect("second pin");
|
||||||
let second_access = bind_grant(®istry, "rotate-b", &second_pin).await;
|
let second_access = bind_grant(&pool, ®istry, "rotate-b", &second_pin).await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolver.resolve_access(&first_access).await.expect("first"),
|
resolver.resolve_access(&first_access).await.expect("first"),
|
||||||
|
|
@ -328,7 +371,7 @@ async fn unbound_grant_handle_cannot_resolve() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn execution_grant_revocation_fails_closed_on_retained_handle() {
|
async fn execution_grant_revocation_fails_closed_on_retained_handle() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("grev01");
|
let owner = owner("grev01");
|
||||||
|
|
@ -347,36 +390,44 @@ async fn execution_grant_revocation_fails_closed_on_retained_handle() {
|
||||||
.await
|
.await
|
||||||
.expect("pin");
|
.expect("pin");
|
||||||
let gid = grant_id("grev01");
|
let gid = grant_id("grev01");
|
||||||
let access = registry
|
let access = bind_grant(&pool, ®istry, "grev01", &pin).await;
|
||||||
.bind_execution_grant(&gid, &pin)
|
|
||||||
.await
|
|
||||||
.expect("bind");
|
|
||||||
|
|
||||||
resolver
|
resolver
|
||||||
.resolve_access(&access)
|
.resolve_access(&access)
|
||||||
.await
|
.await
|
||||||
.expect("resolve while active");
|
.expect("resolve while active");
|
||||||
|
|
||||||
registry
|
let mut tx = pool.begin().await.expect("begin revocation");
|
||||||
.revoke_execution_grant(&gid)
|
sqlx::query(
|
||||||
|
"UPDATE execution_grants
|
||||||
|
SET status = 'revoked', revoked_at = now()
|
||||||
|
WHERE grant_id = $1",
|
||||||
|
)
|
||||||
|
.bind(gid.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.expect("revoke grant");
|
.expect("revoke authoritative grant");
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE execution_grant_credential_pins
|
||||||
|
SET status = 'revoked', revoked_at = now()
|
||||||
|
WHERE grant_id = $1",
|
||||||
|
)
|
||||||
|
.bind(gid.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.expect("revoke grant pin");
|
||||||
|
tx.commit().await.expect("commit revocation");
|
||||||
|
|
||||||
// Same retained handle must fail after durable revocation.
|
// Same retained handle must fail after durable revocation.
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
resolver.resolve_access(&access).await,
|
resolver.resolve_access(&access).await,
|
||||||
Err(remora_credentials::CredentialError::GrantInactive)
|
Err(remora_credentials::CredentialError::GrantInactive)
|
||||||
));
|
));
|
||||||
// Idempotent revoke.
|
|
||||||
registry
|
|
||||||
.revoke_execution_grant(&gid)
|
|
||||||
.await
|
|
||||||
.expect("idempotent");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn credential_revocation_blocks_new_pins() {
|
async fn credential_revocation_blocks_new_pins() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("revoke");
|
let owner = owner("revoke");
|
||||||
|
|
@ -394,7 +445,7 @@ async fn credential_revocation_blocks_new_pins() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("pin before revoke");
|
.expect("pin before revoke");
|
||||||
let access = bind_grant(®istry, "crevoke", &pin).await;
|
let access = bind_grant(&pool, ®istry, "crevoke", &pin).await;
|
||||||
|
|
||||||
registry
|
registry
|
||||||
.revoke(&caller(&owner), &created.id)
|
.revoke(&caller(&owner), &created.id)
|
||||||
|
|
@ -479,7 +530,7 @@ async fn pin_and_revoke_serialize_under_lock() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn destroyed_version_fails_closed() {
|
async fn destroyed_version_fails_closed() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("destroy");
|
let owner = owner("destroy");
|
||||||
|
|
@ -497,7 +548,7 @@ async fn destroyed_version_fails_closed() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("pin");
|
.expect("pin");
|
||||||
let access = bind_grant(®istry, "destroy", &pin).await;
|
let access = bind_grant(&pool, ®istry, "destroy", &pin).await;
|
||||||
|
|
||||||
registry
|
registry
|
||||||
.destroy_version(&caller(&owner), &pin.credential_id, &pin.version_id)
|
.destroy_version(&caller(&owner), &pin.credential_id, &pin.version_id)
|
||||||
|
|
@ -518,7 +569,7 @@ async fn destroyed_version_fails_closed() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pin_after_destroying_prior_version_uses_new_active() {
|
async fn pin_after_destroying_prior_version_uses_new_active() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("pindest");
|
let owner = owner("pindest");
|
||||||
|
|
@ -536,7 +587,7 @@ async fn pin_after_destroying_prior_version_uses_new_active() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("first pin");
|
.expect("first pin");
|
||||||
let first_access = bind_grant(®istry, "pindest-a", &first_pin).await;
|
let first_access = bind_grant(&pool, ®istry, "pindest-a", &first_pin).await;
|
||||||
|
|
||||||
registry
|
registry
|
||||||
.rotate(
|
.rotate(
|
||||||
|
|
@ -550,7 +601,7 @@ async fn pin_after_destroying_prior_version_uses_new_active() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("second pin");
|
.expect("second pin");
|
||||||
let second_access = bind_grant(®istry, "pindest-b", &second_pin).await;
|
let second_access = bind_grant(&pool, ®istry, "pindest-b", &second_pin).await;
|
||||||
|
|
||||||
registry
|
registry
|
||||||
.destroy_version(
|
.destroy_version(
|
||||||
|
|
@ -594,10 +645,7 @@ async fn secret_store_outage_on_resolve_fails_closed() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("pin");
|
.expect("pin");
|
||||||
let access = good_registry
|
let access = bind_grant(&pool, &good_registry, "outage2", &pin).await;
|
||||||
.bind_execution_grant(&grant_id("outage2"), &pin)
|
|
||||||
.await
|
|
||||||
.expect("bind");
|
|
||||||
|
|
||||||
let bad_vault = VaultClient::new(VaultConfig {
|
let bad_vault = VaultClient::new(VaultConfig {
|
||||||
address: "http://127.0.0.1:1".into(),
|
address: "http://127.0.0.1:1".into(),
|
||||||
|
|
@ -682,7 +730,7 @@ async fn concurrent_rotates_all_succeed_and_prior_pin_holds() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("first pin");
|
.expect("first pin");
|
||||||
let first_access = bind_grant(®istry, "conc01", &first_pin).await;
|
let first_access = bind_grant(&pool, ®istry, "conc01", &first_pin).await;
|
||||||
|
|
||||||
let registry = Arc::new(registry);
|
let registry = Arc::new(registry);
|
||||||
let barrier = Arc::new(Barrier::new(4));
|
let barrier = Arc::new(Barrier::new(4));
|
||||||
|
|
@ -736,7 +784,7 @@ async fn concurrent_rotates_all_succeed_and_prior_pin_holds() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn domain_port_resolve_requires_durable_grant() {
|
async fn domain_port_resolve_requires_durable_grant() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let owner = owner("port01");
|
let owner = owner("port01");
|
||||||
|
|
@ -754,7 +802,7 @@ async fn domain_port_resolve_requires_durable_grant() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("pin");
|
.expect("pin");
|
||||||
let access = bind_grant(®istry, "port01", &pin).await;
|
let access = bind_grant(&pool, ®istry, "port01", &pin).await;
|
||||||
let via_port = CredentialResolver::resolve(&resolver, &access)
|
let via_port = CredentialResolver::resolve(&resolver, &access)
|
||||||
.await
|
.await
|
||||||
.expect("port resolve");
|
.expect("port resolve");
|
||||||
|
|
@ -1306,7 +1354,7 @@ async fn version_coordinate_migration_backfills_populated_schema() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn transcript_does_not_contain_secret_literals() {
|
async fn transcript_does_not_contain_secret_literals() {
|
||||||
let Some((_, registry, resolver)) = harness().await else {
|
let Some((pool, registry, resolver)) = harness().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let marker = "unique-secret-marker-9f3c2a1b";
|
let marker = "unique-secret-marker-9f3c2a1b";
|
||||||
|
|
@ -1325,7 +1373,7 @@ async fn transcript_does_not_contain_secret_literals() {
|
||||||
.pin_current_version(&caller(&owner), &created.id)
|
.pin_current_version(&caller(&owner), &created.id)
|
||||||
.await
|
.await
|
||||||
.expect("pin");
|
.expect("pin");
|
||||||
let access = bind_grant(®istry, "redact", &pin).await;
|
let access = bind_grant(&pool, ®istry, "redact", &pin).await;
|
||||||
let secret = resolver.resolve_access(&access).await.expect("resolve");
|
let secret = resolver.resolve_access(&access).await.expect("resolve");
|
||||||
let transcript = format!("{created:?}{secret:?}");
|
let transcript = format!("{created:?}{secret:?}");
|
||||||
assert!(!transcript.contains(marker));
|
assert!(!transcript.contains(marker));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue