mr-668: split PolicyEngine::load into kind-typed loaders

Pre-fix, every caller of `PolicyEngine::load(path, graph_id)`
passed *some* `graph_id` argument — even when the policy was
server-scoped and Cedar's resolution would never touch a Graph
entity. The server-level loader at lib.rs passed the meaningless
sentinel `"server"`. A graph policy file containing a `graph_list`
rule compiled fine; a server policy file containing a `read` rule
compiled fine. Both silently no-op'd at request time because the
engine kind and the rule's resource kind disagreed.

Correct-by-design fix: replace `load` with two kind-typed loaders.

* `PolicyEngine::load_graph(path, graph_id)` — for per-graph
  policy files. Rejects any rule whose action `resource_kind()`
  is `Server`.
* `PolicyEngine::load_server(path)` — for server-level policy
  files. Takes no `graph_id`: server-scoped actions resolve against
  the singleton `Omnigraph::Server::"root"` entity, never a Graph.
  Rejects any rule whose action `resource_kind()` is `Graph`.

The old `load` is hard-deleted in the same commit because every
in-tree consumer migrates here (no semver promise on the workspace
crate, no external pinners). New `PolicyEngineKind` enum types
the loader's intent; `validate_kind_alignment` is the load-time
check that closes the "wrong action, wrong file, silent no-op"
class — operators get a load-time error instead of confused-and-
silent behavior at request time.

Callsites migrated:
* server lib.rs:374 (single-mode per-graph)   → load_graph
* server lib.rs:1065 (multi-mode server)      → load_server
* server lib.rs:1103 (multi-mode per-graph)   → load_graph
* CLI main.rs:732 (resolve_policy_engine)     → load_graph
* tests/server.rs ×5 (4 graph, 1 server)      → load_graph/load_server
* policy_engine_chassis.rs                    → load_graph

Four new in-source tests pin the contract: both rejection paths
and both positive paths.

Closes the "operator puts an action in the wrong file and the
rule silently never matches" class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ragnor Comerford 2026-05-27 13:35:22 +02:00
parent 67a46528ef
commit 4e2f18a95e
No known key found for this signature in database
5 changed files with 228 additions and 13 deletions

View file

@ -371,7 +371,7 @@ impl AppState {
let uri = uri.into();
let db = Omnigraph::open(&uri).await?;
let policy_engine = match policy_file {
Some(path) => Some(PolicyEngine::load(path, &uri)?),
Some(path) => Some(PolicyEngine::load_graph(path, &uri)?),
None => None,
};
if policy_engine.is_some() && bearer_tokens.is_empty() {
@ -1062,7 +1062,7 @@ async fn open_multi_graph_state(
// resource-model refactor maps to the singleton
// `Omnigraph::Server::"root"` entity at evaluation time.
let server_policy = match server_policy_file {
Some(path) => Some(PolicyEngine::load(path, "server")?),
Some(path) => Some(PolicyEngine::load_server(path)?),
None => None,
};
@ -1100,7 +1100,7 @@ async fn open_single_graph(cfg: GraphStartupConfig) -> Result<Arc<GraphHandle>>
let (policy_arc, db) = match &cfg.policy_file {
Some(path) => {
let policy = PolicyEngine::load(path, graph_id.as_str())?;
let policy = PolicyEngine::load_graph(path, graph_id.as_str())?;
let policy_arc: Arc<PolicyEngine> = Arc::new(policy);
let checker = Arc::clone(&policy_arc) as Arc<dyn omnigraph_policy::PolicyChecker>;
(Some(policy_arc), db.with_policy(checker))

View file

@ -3592,7 +3592,7 @@ async fn ingest_per_actor_admission_cap_returns_429() {
let policy_path = temp.path().join("policy.yaml");
fs::write(&policy_path, permit_all_policy_yaml(&["act-flooder"])).unwrap();
let policy_engine =
omnigraph_server::PolicyEngine::load(&policy_path, graph.to_string_lossy().as_ref())
omnigraph_server::PolicyEngine::load_graph(&policy_path, graph.to_string_lossy().as_ref())
.unwrap();
let state = AppState::new_single(
graph.to_string_lossy().to_string(),
@ -3716,7 +3716,7 @@ async fn engine_layer_policy_fires_via_direct_arc_omnigraph_from_new_single() {
let policy_path = temp.path().join("policy.yaml");
fs::write(&policy_path, permit_all_policy_yaml(&["act-allowed"])).unwrap();
let policy_engine =
omnigraph_server::PolicyEngine::load(&policy_path, graph.to_string_lossy().as_ref())
omnigraph_server::PolicyEngine::load_graph(&policy_path, graph.to_string_lossy().as_ref())
.unwrap();
let workload = omnigraph_server::workload::WorkloadController::new(100, 1_000_000_000);
@ -3940,7 +3940,7 @@ async fn build_parity_graph() -> (tempfile::TempDir, PathBuf, PathBuf) {
}
async fn sdk_change_decision(graph: &Path, policy_path: &Path, actor: &str) -> ParityDecision {
let policy = PolicyEngine::load(policy_path, graph.to_string_lossy().as_ref()).unwrap();
let policy = PolicyEngine::load_graph(policy_path, graph.to_string_lossy().as_ref()).unwrap();
let db = Omnigraph::open(graph.to_str().unwrap())
.await
.unwrap()
@ -4008,7 +4008,7 @@ async fn http_change_decision(
}
async fn sdk_merge_decision(graph: &Path, policy_path: &Path, actor: &str) -> ParityDecision {
let policy = PolicyEngine::load(policy_path, graph.to_string_lossy().as_ref()).unwrap();
let policy = PolicyEngine::load_graph(policy_path, graph.to_string_lossy().as_ref()).unwrap();
let db = Omnigraph::open(graph.to_str().unwrap())
.await
.unwrap()
@ -4878,7 +4878,7 @@ rules:
"#,
)
.unwrap();
let server_policy = PolicyEngine::load(&policy_path, "server").unwrap();
let server_policy = PolicyEngine::load_server(&policy_path).unwrap();
let tokens = vec![
("act-andrew".to_string(), "andrew-token".to_string()),