The branch had bumped workspace versions to 0.7.0 and added a dedicated `docs/releases/v0.7.0.md` for the multi-graph work. Per scope decision: ship the graph-rename and the multi-graph mode in one v0.6.0 release. Changes: * Workspace versions bumped 0.7.0 → 0.6.0 in every crate manifest (`omnigraph`, `omnigraph-compiler`, `omnigraph-policy`, `omnigraph-server`, `omnigraph-cli`) and their internal `path = ..., version = "..."` dependency constraints. * `docs/releases/v0.7.0.md` content merged into `docs/releases/v0.6.0.md`, retargeted to a single coherent v0.6.0 release note covering both the graph terminology rename and the multi-graph server mode. The original v0.7.0.md is deleted. * All `v0.7.0` / `0.7.0` doc and comment references throughout `crates/`, `docs/`, `AGENTS.md`, and `openapi.json` retargeted to `v0.6.0` / `0.6.0`. `Cargo.lock` regenerated to match. * OpenAPI spec regenerated via `OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date` — `"version": "0.6.0"` now. Verification: * `cargo build --workspace` — clean (6 pre-existing engine warnings only). * `cargo test --workspace --locked` — zero failures across all 39 test result groups. * `bash scripts/check-agents-md.sh` — passes (34 links / 33 docs). * `grep -rn "0\.7\.0\|v0\.7\.0" --include='*.rs' --include='*.md' --include='*.json' --include='*.toml' .` returns no workspace hits. The three remaining `0.7.0` strings in `Cargo.lock` belong to unrelated 3rd-party crates (`pem-rfc7468`, `radium`, `rand_xoshiro`). The git tag and crates.io publish happen later — this commit just consolidates the surface so the eventual release is one coherent v0.6.0 covering all the work since v0.5.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.4 KiB
Authorization (Cedar policy)
OmniGraph integrates AWS Cedar (cedar-policy = 4.9) for ABAC.
Policy actions
Per-graph actions (bind to Omnigraph::Graph::"<graph_id>"):
read— query / snapshot / list branches & commitsexport— NDJSON exportchange— mutationsschema_apply— apply schema migrationsbranch_createbranch_deletebranch_mergeadmin— reserved for policy-management surfaces (hot reload, audit log, approvals). No call site today; see MR-724 for the reservation rationale.
Server-scoped action (v0.6.0+; binds to Omnigraph::Server::"root"):
graph_list—GET /graphsregistry enumeration (multi-graph mode)
Server-scoped actions cannot use branch_scope or target_branch_scope — they operate on the registry, not on a graph's branches. A rule cannot mix server-scoped and per-graph actions; split into separate rules. (Runtime graph_create / graph_delete are reserved but not shipped in v0.6.0; operators add/remove graphs by editing omnigraph.yaml and restarting.)
Scope kinds
branch_scope— applied to source branch (read,export,change)target_branch_scope— applied to destination (schema_apply, branch ops, run ops)protected_branches— named list with special rules; rule scopes areany | protected | unprotected
Per-graph vs. server-level policy (multi-graph mode)
In multi mode (omnigraph.yaml with a non-empty graphs: map), policy files attach at two levels:
server:
policy:
file: ./server-policy.yaml # server-level: graph_list
graphs:
alpha:
uri: s3://tenant-bucket/alpha
policy:
file: ./policies/alpha.yaml # per-graph: read, change, branch_*, schema_apply
beta:
uri: s3://tenant-bucket/beta
# no per-graph policy → no engine-layer Cedar enforcement on beta
Each graph's HTTP request flows through its own per-graph policy. The management endpoint (GET /graphs) flows through the server-level policy. When server.policy.file is unset and bearer tokens are configured, GET /graphs falls through to MR-723 default-deny (only read-equivalent actions allowed for authenticated actors — and graph_list is not read) → 403. So the operator must explicitly authorize via server-policy.yaml to expose /graphs.
Example server-level policy:
version: 1
groups:
admins: [act-andrew]
rules:
- id: admins-can-list-graphs
allow:
actors: { group: admins }
actions: [graph_list]
Configuration
omnigraph.yaml:
policy:
file: ./policy.yaml # Cedar rules + groups
tests: ./policy.tests.yaml # declarative test cases
cli:
actor: act-andrew # default actor for CLI direct-engine writes
Each per-graph rule must use exactly one of branch_scope or target_branch_scope. Server-scoped rules (graph_list) take neither — they have no branch context.
cli.actor is the default actor identity for CLI direct-engine writes
when policy.file is configured. Override per-invocation with --as <ACTOR> (top-level flag) — --as wins, otherwise cli.actor is used,
otherwise no actor. With policy configured and neither set, the
engine-layer footgun guard intentionally denies the write (silent bypass
via "I forgot the actor" is exactly what the guard prevents). Remote
HTTP writes ignore both — they resolve their actor server-side from the
bearer token.
CLI
omnigraph policy validate— parse + count actors, exit 1 on parse error.omnigraph policy test— run cases inpolicy.tests.yaml, exit 1 on any expectation mismatch.omnigraph policy explain --actor … --action … [--branch …] [--target-branch …]— show decision and matched rule.omnigraph --as <ACTOR> <subcommand>— set the actor for the duration of one invocation. Effective forchange,load,ingest,branch create|delete|merge, andschema applyagainst local URIs. No-op against remote HTTP URIs (actor is bearer-token-resolved server-side).
Enforcement
Policy is a property of the engine, not the transport. Every mutating
write — mutate_as, load_as, ingest_as, apply_schema_as,
branch_create_as, branch_create_from_as, branch_delete_as,
branch_merge_as — calls Omnigraph::enforce(action, scope, actor) at
the head of the method. The gate fires identically whether the call
originates from the HTTP server, the CLI, or an embedded SDK consumer.
When no PolicyChecker is installed (the dev/embedded default) the gate
is a strict no-op; when one is installed and the call site forgets to
thread an actor through, the gate fails closed rather than silently
bypassing.
Server runtime states (MR-723)
The HTTP server classifies its startup configuration into one of three
states based on whether bearer tokens are configured and whether a
policy file is set. The state determines what happens to a request that
reaches authorize_request() without a matching policy permit.
| State | Tokens | Policy file | Behavior |
|---|---|---|---|
| Open | no | no | Every request is permitted. Refuses to start unless --unauthenticated or OMNIGRAPH_UNAUTHENTICATED=1 is set — the operator must explicitly opt in. |
| DefaultDeny | yes | no | Every authenticated request for an action other than read is rejected with HTTP 403. Closes the "tokens but forgot the policy file" trap — an operator who sets up auth and forgot to point at a policy file used to ship the illusion of protection. |
| PolicyEnabled | any | yes | Every request is evaluated by Cedar against the configured policy. |
The classifier is classify_server_runtime_state in
crates/omnigraph-server/src/lib.rs; it returns Err for the "no
tokens, no policy, no flag" cell so the server refuses to start instead
of silently shipping an open instance. Tests pin every cell of the
matrix and the State-2 deny path.
Server-side, authorize_request() still runs at the HTTP boundary —
that's where actor identity is resolved from the bearer token and where
admission control / per-actor rate limits live. Engine-layer enforcement
is the defense in depth layer: it catches CLI direct-engine writes,
embedded SDK consumers, and any future transport that hasn't (or won't)
re-implement HTTP's authorize_request. Both layers consult the same
Cedar policy via the same PolicyChecker trait, so decisions cannot
disagree.
Coarse vs. fine enforcement
There are two enforcement points, each with non-overlapping responsibilities:
| Layer | Question it answers | Where it fires |
|---|---|---|
| Engine-layer (coarse) | Can this actor invoke this action against this branch / branch-transition? | Omnigraph::enforce(action, scope, actor) at the head of every _as writer; one Cedar decision per call. |
| Query-layer (fine) | For the rows / types this action actually touches, which can the actor see or modify? | Per-row predicates pushed into DataFusion at plan time. Not yet implemented — see MR-725. |
The engine-layer gate keeps ResourceScope deliberately at branch
granularity (Graph, Branch, TargetBranch, BranchTransition).
Per-type and per-row authority is the query-layer's job; conflating them
in ResourceScope would create two places per-type policy could be
evaluated and a drift surface between them.
Actor identity (signed-claim-only)
The actor identity used for every policy decision comes from the matched bearer token — never from a client-supplied request header, query parameter, or body field. The server resolves the token at the auth middleware boundary, looks up the actor it was minted for, and overwrites whatever the handler may have placed in the policy request. Clients cannot set actor_id directly.
This is intentional. Trusting client-supplied identity for authorization is "asking the attacker if they're an admin" — Supabase's RLS history names the same footgun. The chokepoint lives in authorize_request in crates/omnigraph-server/src/lib.rs and is named in docs/dev/invariants.md Hard Invariant 11. A regression test asserts the contract: a request with Authorization: Bearer <token-for-actor-A> plus X-Actor-Id: actor-B always evaluates as actor A, never as actor B.
If you find yourself wanting to let clients override actor_id for impersonation, delegation, or service-account flows — that's a feature, but it needs explicit design (e.g., signed delegation claims, an On-Behalf-Of audit trail). It is not a convenience knob.