Trusty: provenance library workspace (initial import)
This commit is contained in:
883
crates/adapters/trusty-policy-postgres/tests/integration.rs
Normal file
883
crates/adapters/trusty-policy-postgres/tests/integration.rs
Normal file
@@ -0,0 +1,883 @@
|
||||
//! Behavioural tests against a real Postgres.
|
||||
//!
|
||||
//! Everything in the crate's unit tests asserts properties of SQL
|
||||
//! *text*. These assert what the database actually does with it, which
|
||||
//! is the only claim worth making about an authorisation boundary.
|
||||
//!
|
||||
//! Skipped, not failed, when `TRUSTY_POLICY_PG_URL` is unset, so
|
||||
//! `cargo test --workspace` stays green on a machine with no database.
|
||||
//! Same convention as `trusty-postgres`'s suite.
|
||||
//!
|
||||
//! ```text
|
||||
//! TRUSTY_POLICY_PG_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres \
|
||||
//! cargo test -p trusty-policy-postgres --test integration
|
||||
//! ```
|
||||
//!
|
||||
//! **The tests act as a non-superuser role.** A superuser bypasses row
|
||||
//! level security entirely, so a suite that ran as `postgres` would pass
|
||||
//! against a table with no protection at all.
|
||||
|
||||
use postgres::Client;
|
||||
use trusty_policy::{Grant, Limits, Tier};
|
||||
use trusty_policy_postgres::{CompilationReport, GrantId, GrantStore, PolicyPgError};
|
||||
|
||||
fn url() -> Option<String> {
|
||||
std::env::var("TRUSTY_POLICY_PG_URL").ok()
|
||||
}
|
||||
|
||||
/// Serialises the suite against one database.
|
||||
///
|
||||
/// Every `GrantStore::open` applies the schema, and concurrent
|
||||
/// `CREATE OR REPLACE FUNCTION` on the same function collides in
|
||||
/// `pg_proc` with "tuple concurrently updated". The tests also create
|
||||
/// and drop roles, which are cluster-global however carefully the table
|
||||
/// names are scoped.
|
||||
///
|
||||
/// Taken here rather than left to `--test-threads=1`, because a suite
|
||||
/// that is only green when invoked a particular way is one plain
|
||||
/// `cargo test` away from looking broken.
|
||||
fn db_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
|
||||
LOCK.get_or_init(|| std::sync::Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
/// Distinct names per test, so tests neither collide nor depend on
|
||||
/// order.
|
||||
struct Fixture {
|
||||
table: String,
|
||||
role: String,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new(tag: &str) -> Self {
|
||||
Self {
|
||||
table: format!("rls_probe_{tag}"),
|
||||
role: format!("rls_role_{tag}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A protected table, a non-superuser role that can reach it, and no
|
||||
/// grants at all.
|
||||
fn set_up(&self, store: &mut GrantStore) -> Vec<String> {
|
||||
let (table, role) = (&self.table, &self.role);
|
||||
let drop_role = drop_role_sql(role);
|
||||
store
|
||||
.client()
|
||||
.batch_execute(&format!(
|
||||
"DROP TABLE IF EXISTS public.{table};
|
||||
CREATE TABLE public.{table} (
|
||||
id SERIAL PRIMARY KEY,
|
||||
resource_scope TEXT NOT NULL,
|
||||
note TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO public.{table}(resource_scope, note) VALUES
|
||||
('board:9f2/calendar/team', 'visible'),
|
||||
('board:9f2/calendar/private', 'excluded'),
|
||||
('board:other/calendar', 'someone else');
|
||||
{drop_role}
|
||||
CREATE ROLE {role} NOSUPERUSER NOBYPASSRLS;
|
||||
-- SET ROLE requires membership. Supabase's `postgres` is
|
||||
-- not a true superuser, so the connecting user has to be
|
||||
-- granted the role it intends to act as.
|
||||
GRANT {role} TO CURRENT_USER;
|
||||
GRANT USAGE ON SCHEMA public TO {role};
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO {role};
|
||||
GRANT USAGE, SELECT ON SEQUENCE public.{table}_id_seq TO {role};
|
||||
GRANT USAGE ON SCHEMA trusty_policy TO {role};"
|
||||
))
|
||||
.expect("fixture set-up");
|
||||
|
||||
store
|
||||
.enforce(&format!("public.{table}"), "resource_scope")
|
||||
.expect("enforce")
|
||||
}
|
||||
|
||||
fn tear_down(&self, store: &mut GrantStore) {
|
||||
let (table, role) = (&self.table, &self.role);
|
||||
let drop_role = drop_role_sql(role);
|
||||
let _ = store.client().batch_execute(&format!(
|
||||
"DROP TABLE IF EXISTS public.{table};
|
||||
{drop_role}"
|
||||
));
|
||||
let _ = store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.permission_grant");
|
||||
}
|
||||
|
||||
/// Read the table the way the application would: as the unprivileged
|
||||
/// role, with a subject set for the transaction only.
|
||||
fn read_as(&self, client: &mut Client, subject: &str) -> Vec<String> {
|
||||
let mut txn = client.transaction().expect("txn");
|
||||
txn.execute(&format!("SET LOCAL ROLE {}", self.role), &[])
|
||||
.expect("assume role");
|
||||
txn.execute(
|
||||
"SELECT set_config('trusty.subject', $1, true)",
|
||||
&[&subject],
|
||||
)
|
||||
.expect("set subject");
|
||||
let rows = txn
|
||||
.query(
|
||||
&format!("SELECT note FROM public.{} ORDER BY id", self.table),
|
||||
&[],
|
||||
)
|
||||
.expect("select");
|
||||
let notes = rows.iter().map(|r| r.get::<_, String>(0)).collect();
|
||||
txn.rollback().expect("rollback");
|
||||
notes
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop a role only if it exists.
|
||||
///
|
||||
/// `DROP ROLE IF EXISTS` tolerates absence, but `DROP OWNED BY` does not
|
||||
/// -- and the grants a role holds must go before the role can. So both
|
||||
/// have to sit behind one existence check rather than relying on
|
||||
/// IF EXISTS.
|
||||
fn drop_role_sql(role: &str) -> String {
|
||||
format!(
|
||||
"DO $do$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{role}') THEN
|
||||
-- In PG16+, ADMIN OPTION on a role is not the same as
|
||||
-- holding its privileges, and DROP OWNED BY needs the
|
||||
-- latter. Creating the role is not enough.
|
||||
EXECUTE 'GRANT {role} TO CURRENT_USER';
|
||||
EXECUTE 'DROP OWNED BY {role}';
|
||||
EXECUTE 'DROP ROLE {role}';
|
||||
END IF;
|
||||
END $do$;"
|
||||
)
|
||||
}
|
||||
|
||||
/// Issue a grant, registering its principals first.
|
||||
///
|
||||
/// `permits()` refuses for a subject that is not a registered, live
|
||||
/// principal — identity before permission — so a test that only issued
|
||||
/// a grant would be testing the registration check rather than the rule
|
||||
/// it names.
|
||||
trait Registering {
|
||||
fn issue_registered(
|
||||
&mut self,
|
||||
grant: &Grant,
|
||||
) -> Result<(GrantId, CompilationReport), PolicyPgError>;
|
||||
}
|
||||
|
||||
impl Registering for GrantStore {
|
||||
fn issue_registered(
|
||||
&mut self,
|
||||
grant: &Grant,
|
||||
) -> Result<(GrantId, CompilationReport), PolicyPgError> {
|
||||
for who in [Some(&grant.subject), grant.acts_for.as_ref()].into_iter().flatten() {
|
||||
let kind = who.split(':').next().unwrap_or("agent");
|
||||
self.register_principal(who, kind, "")?;
|
||||
}
|
||||
self.issue(grant)
|
||||
}
|
||||
}
|
||||
|
||||
fn grant(subject: &str, scope: &str, tier: Tier) -> Grant {
|
||||
Grant {
|
||||
why: Some("integration fixture".into()),
|
||||
subject: subject.into(),
|
||||
acts_for: None,
|
||||
scope: scope.into(),
|
||||
tier,
|
||||
excludes: Vec::new(),
|
||||
limits: Limits::default(),
|
||||
// Far future; the expiry test overrides this deliberately.
|
||||
expires_at: 4_102_444_800,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// The acceptance criterion.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn a_read_the_grant_excludes_fails_at_the_database() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("excl");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let mut g = grant("agent:excl", "board:9f2/calendar", Tier::Read);
|
||||
g.excludes = vec!["board:9f2/calendar/private".into()];
|
||||
store.issue_registered(&g).expect("issue");
|
||||
|
||||
let seen = fx.read_as(store.client(), "agent:excl");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
!seen.contains(&"excluded".to_string()),
|
||||
"an excluded resource must be unreadable AT THE DATABASE, not merely \
|
||||
refused by an application check that could be bypassed; saw {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_exclusion_is_not_overridden_by_a_wider_grant() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("wider");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let mut narrow = grant("agent:wider", "board:9f2/calendar/team", Tier::Read);
|
||||
narrow.excludes = vec!["board:9f2/calendar/private".into()];
|
||||
store.issue_registered(&narrow).expect("issue narrow");
|
||||
|
||||
// Issued afterwards, covering strictly more, and carrying no
|
||||
// exclusion of its own.
|
||||
let wide = grant("agent:wider", "board:9f2", Tier::Read);
|
||||
store.issue_registered(&wide).expect("issue wide");
|
||||
|
||||
let seen = fx.read_as(store.client(), "agent:wider");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
!seen.contains(&"excluded".to_string()),
|
||||
"adding a wider grant later must not reopen what an exclusion shut; \
|
||||
saw {seen:?}"
|
||||
);
|
||||
assert!(
|
||||
seen.contains(&"visible".to_string()),
|
||||
"the wider grant should still permit everything it covers that is not \
|
||||
excluded; saw {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_policy_is_not_permission() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("deny");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let with_no_grant = fx.read_as(store.client(), "agent:nobody");
|
||||
let with_no_subject = fx.read_as(store.client(), "");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
with_no_grant.is_empty(),
|
||||
"a subject with no grant must see nothing, not fall back to something \
|
||||
wider; saw {with_no_grant:?}"
|
||||
);
|
||||
assert!(
|
||||
with_no_subject.is_empty(),
|
||||
"code that forgets to say who it is must get nothing rather than \
|
||||
everything; saw {with_no_subject:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scope_containing_a_like_metacharacter_does_not_widen_itself() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("meta");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
// Under LIKE semantics '%' matches everything and this would read
|
||||
// the whole table.
|
||||
store
|
||||
.issue(&grant("agent:meta", "%", Tier::Read))
|
||||
.expect("issue");
|
||||
let wildcard = fx.read_as(store.client(), "agent:meta");
|
||||
|
||||
store
|
||||
.issue(&grant("agent:meta2", "board:9f2/calendar/_eam", Tier::Read))
|
||||
.expect("issue");
|
||||
let underscore = fx.read_as(store.client(), "agent:meta2");
|
||||
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
wildcard.is_empty(),
|
||||
"a scope of '%' is a scope literally named '%', not a wildcard; saw \
|
||||
{wildcard:?}"
|
||||
);
|
||||
assert!(
|
||||
underscore.is_empty(),
|
||||
"'_' must not match an arbitrary character, or a near-miss scope would \
|
||||
silently grant its neighbour; saw {underscore:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sibling_whose_name_merely_starts_the_same_is_not_covered() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("sib");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
// 'board:9f2/calendar' must NOT cover 'board:9f2/calendar/team' via a
|
||||
// bare prefix test that ignores the separator -- it does cover it
|
||||
// here, legitimately -- but 'board:o' must not cover 'board:other'.
|
||||
store
|
||||
.issue(&grant("agent:sib", "board:o", Tier::Read))
|
||||
.expect("issue");
|
||||
let seen = fx.read_as(store.client(), "agent:sib");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
seen.is_empty(),
|
||||
"containment must break on a path separator; without it, 'board:o' \
|
||||
would grant 'board:other/calendar'; saw {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_expired_grant_stops_permitting_at_the_wall_clock() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("exp");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let mut g = grant("agent:exp", "board:9f2/calendar", Tier::Read);
|
||||
g.expires_at = 1_000_000_000; // 2001, comfortably past
|
||||
store.issue_registered(&g).expect("issue");
|
||||
|
||||
let seen = fx.read_as(store.client(), "agent:exp");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
seen.is_empty(),
|
||||
"expiry is wall clock and independent of whether work finished; saw \
|
||||
{seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_a_grant_takes_effect_without_ddl() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("rev");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let g = grant("agent:rev", "board:9f2/calendar", Tier::Read);
|
||||
let (id, _) = store.issue_registered(&g).expect("issue");
|
||||
let before = fx.read_as(store.client(), "agent:rev");
|
||||
|
||||
let revoked = store.revoke(&id).expect("revoke");
|
||||
let after = fx.read_as(store.client(), "agent:rev");
|
||||
|
||||
let twice = store.revoke(&id).expect("revoke again");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
!before.is_empty(),
|
||||
"the grant must permit something before revocation, or this test \
|
||||
proves nothing"
|
||||
);
|
||||
assert!(revoked, "revoking a live grant must report that it did so");
|
||||
assert!(
|
||||
after.is_empty(),
|
||||
"revocation must take effect at the next decision, with no DDL run; \
|
||||
saw {after:?}"
|
||||
);
|
||||
assert!(
|
||||
!twice,
|
||||
"revoking what is already revoked must report that nothing changed, \
|
||||
rather than reassuring the caller"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_cannot_exceed_the_principal_it_acts_for() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("deleg");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
store
|
||||
.client()
|
||||
.batch_execute(
|
||||
"INSERT INTO trusty_policy.principal(id, kind) VALUES
|
||||
('agent:d1', 'agent'), ('user:d1', 'user')
|
||||
ON CONFLICT DO NOTHING;
|
||||
INSERT INTO trusty_policy.delegation(agent_id, acts_for_id) VALUES
|
||||
('agent:d1', 'user:d1')
|
||||
ON CONFLICT DO NOTHING;",
|
||||
)
|
||||
.expect("principals");
|
||||
|
||||
// The agent's own grant is wider than the principal's.
|
||||
let mut wide = grant("agent:d1", "board:9f2", Tier::Read);
|
||||
wide.acts_for = Some("user:d1".into());
|
||||
store.issue_registered(&wide).expect("issue agent");
|
||||
store
|
||||
.issue(&grant("user:d1", "board:9f2/calendar/team", Tier::Read))
|
||||
.expect("issue principal");
|
||||
|
||||
let seen = fx.read_as(store.client(), "agent:d1");
|
||||
|
||||
// Now revoke the delegation and try again.
|
||||
store
|
||||
.client()
|
||||
.batch_execute(
|
||||
"UPDATE trusty_policy.delegation SET revoked_at = now()
|
||||
WHERE agent_id = 'agent:d1'",
|
||||
)
|
||||
.expect("revoke delegation");
|
||||
let after_revoke = fx.read_as(store.client(), "agent:d1");
|
||||
|
||||
store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.delegation; DELETE FROM trusty_policy.principal")
|
||||
.expect("clean principals");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert_eq!(
|
||||
seen,
|
||||
vec!["visible".to_string()],
|
||||
"the agent must be held to the intersection: its own grant covers \
|
||||
board:9f2 entirely, but the principal only permits the team \
|
||||
calendar; saw {seen:?}"
|
||||
);
|
||||
assert!(
|
||||
after_revoke.is_empty(),
|
||||
"revoking the delegation must stop the agent acting at all, without \
|
||||
touching either grant; saw {after_revoke:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grant_for_one_verb_does_not_authorise_another() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("verb");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
// Delete is the most destructive tier. If tiers implied one another
|
||||
// by ordering, this would also confer read.
|
||||
store
|
||||
.issue(&grant("agent:verb", "board:9f2/calendar", Tier::Delete))
|
||||
.expect("issue");
|
||||
let seen = fx.read_as(store.client(), "agent:verb");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
seen.is_empty(),
|
||||
"tier maps 1:1 onto the SQL verbs and deny-by-default governs the rest, \
|
||||
so a grant to delete must not silently confer the ability to read \
|
||||
everything deletable; saw {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforce_is_idempotent_and_forces_row_level_security() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("force");
|
||||
let first = fx.set_up(&mut store);
|
||||
|
||||
// Re-running must converge, not fail on an existing policy.
|
||||
let second = store
|
||||
.enforce(&format!("public.{}", fx.table), "resource_scope")
|
||||
.expect("second enforce");
|
||||
|
||||
let forced: bool = store
|
||||
.client()
|
||||
.query_one(
|
||||
"SELECT relforcerowsecurity FROM pg_class WHERE oid = to_regclass($1)",
|
||||
&[&format!("public.{}", fx.table)],
|
||||
)
|
||||
.expect("pg_class")
|
||||
.get(0);
|
||||
let policies: i64 = store
|
||||
.client()
|
||||
.query_one(
|
||||
"SELECT count(*) FROM pg_policies
|
||||
WHERE schemaname = 'public' AND tablename = $1",
|
||||
&[&fx.table],
|
||||
)
|
||||
.expect("pg_policies")
|
||||
.get(0);
|
||||
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert_eq!(first, second, "enforce must be deterministic across runs");
|
||||
assert!(
|
||||
forced,
|
||||
"without FORCE, the table owner bypasses every policy and the \
|
||||
protection is decorative"
|
||||
);
|
||||
assert_eq!(
|
||||
policies, 4,
|
||||
"one policy per verb, replaced rather than accumulated on re-run"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_relation_is_refused_rather_than_quoted() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
|
||||
assert!(
|
||||
store.enforce("public.no_such_table_here", "resource_scope").is_err(),
|
||||
"enabling row level security on a table that does not exist would \
|
||||
silently protect nothing"
|
||||
);
|
||||
assert!(
|
||||
store.enforce("public.no_such_table_here", "'; DROP TABLE x; --").is_err(),
|
||||
"an identifier that does not resolve must be refused, never escaped \
|
||||
and used"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issuing_the_same_grant_twice_leaves_one_row() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let g = grant("agent:idem", "board:9f2/calendar", Tier::Read);
|
||||
|
||||
let (first, report) = store.issue_registered(&g).expect("issue");
|
||||
let (second, _) = store.issue_registered(&g).expect("issue again");
|
||||
|
||||
let count: i64 = store
|
||||
.client()
|
||||
.query_one(
|
||||
"SELECT count(*) FROM trusty_policy.permission_grant WHERE subject = $1",
|
||||
&[&"agent:idem"],
|
||||
)
|
||||
.expect("count")
|
||||
.get(0);
|
||||
let _ = store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.permission_grant");
|
||||
|
||||
assert_eq!(first, second, "identity is content, so it must not vary");
|
||||
assert_eq!(
|
||||
count, 1,
|
||||
"a content-addressed id makes re-issuing idempotent rather than \
|
||||
quietly accumulating duplicate permissions"
|
||||
);
|
||||
assert!(
|
||||
report.is_total(),
|
||||
"this grant sets no limits, so nothing about it went unenforced"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_stored_canonical_form_reproduces_the_id() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let g = grant("agent:canon", "board:9f2/calendar", Tier::Read);
|
||||
let (id, _) = store.issue_registered(&g).expect("issue");
|
||||
|
||||
let stored: String = store
|
||||
.client()
|
||||
.query_one(
|
||||
"SELECT canonical_json FROM trusty_policy.permission_grant WHERE id = $1",
|
||||
&[&id.as_str()],
|
||||
)
|
||||
.expect("select")
|
||||
.get(0);
|
||||
let _ = store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.permission_grant");
|
||||
|
||||
assert_eq!(
|
||||
GrantId::of_canonical(&stored),
|
||||
id,
|
||||
"the bytes in the row must be the bytes that were hashed, or a \
|
||||
verifier holding the signed grant could not find or check its row"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_registered_principal_with_no_grant_still_sees_nothing() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("reg");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
// Registration is identity, not permission. Without this test,
|
||||
// `absent_policy_is_not_permission` would pass merely because the
|
||||
// subject was unregistered, and the deny-by-default rule it is named
|
||||
// for would go untested.
|
||||
store
|
||||
.register_principal("agent:registered", "agent", "has an identity, no grant")
|
||||
.expect("register");
|
||||
|
||||
let seen = fx.read_as(store.client(), "agent:registered");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
seen.is_empty(),
|
||||
"being someone is not the same as being allowed something; saw {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unregistered_principal_refuses_even_holding_a_grant() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("unreg");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
// Issued WITHOUT registering: a grant to nobody.
|
||||
store
|
||||
.issue(&grant("agent:ghost", "board:9f2/calendar", Tier::Read))
|
||||
.expect("issue");
|
||||
let seen = fx.read_as(store.client(), "agent:ghost");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
seen.is_empty(),
|
||||
"a grant held by a principal who was never registered is a grant to \
|
||||
nobody, and absent policy is not permission; saw {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_a_principal_stops_every_grant_it_holds_at_once() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("prev");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
// Two separate grants, so this tests the blunt instrument rather
|
||||
// than a single revocation wearing a different hat.
|
||||
store
|
||||
.issue_registered(&grant("agent:doomed", "board:9f2/calendar", Tier::Read))
|
||||
.expect("issue one");
|
||||
store
|
||||
.issue_registered(&grant("agent:doomed", "board:other", Tier::Read))
|
||||
.expect("issue two");
|
||||
let before = fx.read_as(store.client(), "agent:doomed");
|
||||
|
||||
let revoked = store.revoke_principal("agent:doomed").expect("revoke");
|
||||
let after = fx.read_as(store.client(), "agent:doomed");
|
||||
let twice = store.revoke_principal("agent:doomed").expect("revoke again");
|
||||
|
||||
let survivors: i64 = store
|
||||
.client()
|
||||
.query_one(
|
||||
"SELECT count(*) FROM trusty_policy.permission_grant WHERE subject = $1",
|
||||
&[&"agent:doomed"],
|
||||
)
|
||||
.expect("count")
|
||||
.get(0);
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert_eq!(
|
||||
before.len(),
|
||||
3,
|
||||
"both grants must permit something first, or this proves nothing; \
|
||||
saw {before:?}"
|
||||
);
|
||||
assert!(revoked, "revoking a live principal must report that it did so");
|
||||
assert!(
|
||||
after.is_empty(),
|
||||
"killing the key must end the agent's authority entirely, not grant \
|
||||
by grant -- revoking them one at a time races whoever is using them; \
|
||||
saw {after:?}"
|
||||
);
|
||||
assert!(!twice, "revoking what is already revoked must report no change");
|
||||
assert_eq!(
|
||||
survivors, 2,
|
||||
"the grant rows must survive revocation; an audit asks what was \
|
||||
permitted last March and a deleted row cannot answer"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_an_agent_leaves_the_principal_it_acted_for_untouched() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("sever");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let mut agent = grant("agent:s1", "board:9f2", Tier::Read);
|
||||
agent.acts_for = Some("user:s1".into());
|
||||
store.issue_registered(&agent).expect("issue agent");
|
||||
store
|
||||
.issue_registered(&grant("user:s1", "board:9f2/calendar/team", Tier::Read))
|
||||
.expect("issue principal");
|
||||
store.delegate("agent:s1", "user:s1").expect("delegate");
|
||||
|
||||
let agent_before = fx.read_as(store.client(), "agent:s1");
|
||||
store.revoke_principal("agent:s1").expect("revoke agent");
|
||||
let agent_after = fx.read_as(store.client(), "agent:s1");
|
||||
let principal_after = fx.read_as(store.client(), "user:s1");
|
||||
|
||||
store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.delegation")
|
||||
.expect("clean");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(!agent_before.is_empty(), "the agent must start able to act");
|
||||
assert!(
|
||||
agent_after.is_empty(),
|
||||
"killing the agent's key must end its authority; saw {agent_after:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
principal_after,
|
||||
vec!["visible".to_string()],
|
||||
"and the person behind it must be untouched -- that asymmetry is the \
|
||||
entire point of an agent never being a principal in its own right; \
|
||||
saw {principal_after:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_key_edge_records_a_reference_and_not_a_second_registry() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
|
||||
store
|
||||
.register_principal("agent:keyed", "agent", "")
|
||||
.expect("register");
|
||||
assert_eq!(
|
||||
store.producer_oid_of("agent:keyed").expect("lookup"),
|
||||
None,
|
||||
"an unbound principal has no producer, and None is not the same as \
|
||||
a producer named empty string"
|
||||
);
|
||||
|
||||
store
|
||||
.bind_key("agent:keyed", "1.3.6.1.4.1.99999.1")
|
||||
.expect("bind");
|
||||
let bound = store.producer_oid_of("agent:keyed").expect("lookup");
|
||||
|
||||
let unknown = store.bind_key("agent:missing", "1.3.6.1.4.1.99999.2");
|
||||
|
||||
let _ = store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.principal WHERE id = 'agent:keyed'");
|
||||
|
||||
assert_eq!(
|
||||
bound.as_deref(),
|
||||
Some("1.3.6.1.4.1.99999.1"),
|
||||
"the edge must round-trip; resolving it to a public key is the \
|
||||
producer registry's job, not this crate's"
|
||||
);
|
||||
assert!(
|
||||
unknown.is_err(),
|
||||
"a key bound to a principal that does not exist is a key nothing can \
|
||||
revoke, so binding it must fail rather than create a dangling edge"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_delegation_must_be_recorded_before_an_agent_can_use_it() {
|
||||
let Some(url) = url() else {
|
||||
eprintln!("skipped: TRUSTY_POLICY_PG_URL unset");
|
||||
return;
|
||||
};
|
||||
let _db = db_lock();
|
||||
let mut store = GrantStore::open(&url).expect("open");
|
||||
let fx = Fixture::new("edge");
|
||||
fx.set_up(&mut store);
|
||||
|
||||
let mut agent = grant("agent:e1", "board:9f2/calendar", Tier::Read);
|
||||
agent.acts_for = Some("user:e1".into());
|
||||
store.issue_registered(&agent).expect("issue agent");
|
||||
store
|
||||
.issue_registered(&grant("user:e1", "board:9f2/calendar", Tier::Read))
|
||||
.expect("issue principal");
|
||||
|
||||
// Both grants exist and agree. The edge does not.
|
||||
let without_edge = fx.read_as(store.client(), "agent:e1");
|
||||
store.delegate("agent:e1", "user:e1").expect("delegate");
|
||||
let with_edge = fx.read_as(store.client(), "agent:e1");
|
||||
store
|
||||
.revoke_delegation("agent:e1", "user:e1")
|
||||
.expect("revoke delegation");
|
||||
let after_revoke = fx.read_as(store.client(), "agent:e1");
|
||||
|
||||
store
|
||||
.client()
|
||||
.batch_execute("DELETE FROM trusty_policy.delegation")
|
||||
.expect("clean");
|
||||
fx.tear_down(&mut store);
|
||||
|
||||
assert!(
|
||||
without_edge.is_empty(),
|
||||
"two matching grants are not a delegation; the authority to act for \
|
||||
someone has to be recorded, or it is merely claimed; saw \
|
||||
{without_edge:?}"
|
||||
);
|
||||
assert!(
|
||||
!with_edge.is_empty(),
|
||||
"with the edge recorded, the intersection should permit; saw {with_edge:?}"
|
||||
);
|
||||
assert!(
|
||||
after_revoke.is_empty(),
|
||||
"and withdrawing it must take effect without touching either grant; \
|
||||
saw {after_revoke:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user