mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-27 02:39:38 +02:00
mr-668: Cedar resource-model refactor (PR 6a/10)
PR 6a of the MR-668 multi-graph server work. Policy-crate-only refactor —
no HTTP handler changes, no operator-supplied policy.yaml changes. Sets
up the chassis that PR 6b's `GET /graphs` consumes.
Two new `PolicyAction` variants:
- `GraphCreate` — gates `POST /graphs` (deferred behavioral PR).
- `GraphList` — gates `GET /graphs` (lands in PR 6b).
Note: `GraphDelete` is intentionally NOT added in this PR. `DELETE
/graphs/{id}` is deferred from MR-668's v0.7.0 scope to bound complexity
(no `delete_prefix`, no tombstone, no `RegistryLookup::Tombstoned`).
Adding the Cedar action without a consumer would be the same kind of
"dead vocabulary" trap the `Admin` variant already documents.
New `PolicyResourceKind { Graph, Server }` enum, plus a
`PolicyAction::resource_kind()` method that classifies every action.
Per-graph actions (Read, Change, BranchCreate, …) bind to
`Omnigraph::Graph::"<graph_label>"`; server-scoped actions
(GraphCreate, GraphList) bind to the singleton
`Omnigraph::Server::"root"`. `Admin` stays classified as per-graph for
now — MR-724 will pick the final shape when the first consumer surface
ships.
Cedar schema string additions:
- `entity Server;`
- `action "graph_create" appliesTo { principal: Actor, resource: Server, ... }`
- `action "graph_list" appliesTo { principal: Actor, resource: Server, ... }`
Compiler updates:
- `compile_policy_source` picks the resource literal based on the
action's `resource_kind`. Existing graph-only policies generate
the same Cedar source as before — pinned by
`per_graph_rules_continue_to_work_alongside_server_rules`.
- `compile_entities` includes the `Server::"root"` entity only when
a rule references a server-scoped action. Keeps test assertions
for graph-only policies tight.
- `PolicyEngine::authorize` builds the right resource UID at
request time based on `request.action.resource_kind()`.
Validation rules added to `PolicyConfig::validate`:
- A rule may not mix server-scoped and per-graph actions (different
resource kinds need different `permit` clauses).
- Server-scoped actions cannot have `branch_scope` or
`target_branch_scope` — there's no branch context at the server
level.
Operator impact: zero. The Cedar schema `Omnigraph::Server` entity is
internally referenced by `compile_policy_source`; operator policy.yaml
files only declare actions in `rules[].allow.actions` and never
reference the resource entity directly. Decision 6's "internal rename
only; operator policies unaffected" contract is preserved and pinned
by `per_graph_rules_continue_to_work_alongside_server_rules`.
Tests: 5 new (11 policy tests total, up from 6):
- `graph_list_action_authorizes_against_server_resource`
- `graph_create_action_authorizes_against_server_resource`
- `server_scoped_rule_cannot_use_branch_scope`
- `rule_mixing_server_and_per_graph_actions_is_rejected`
- `per_graph_rules_continue_to_work_alongside_server_rules`
No regression: 145 server tests (74 lib + 71 integration) still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4df361d152
commit
0e5aa036f4
1 changed files with 301 additions and 4 deletions
|
|
@ -39,6 +39,25 @@ pub enum PolicyAction {
|
||||||
/// future shape. Avoid writing such rules until the first consumer
|
/// future shape. Avoid writing such rules until the first consumer
|
||||||
/// endpoint ships to prevent confusion.
|
/// endpoint ships to prevent confusion.
|
||||||
Admin,
|
Admin,
|
||||||
|
/// MR-668: management actions that operate on the server's graph
|
||||||
|
/// registry, not on a single graph's contents. Cedar `appliesTo`
|
||||||
|
/// declarations bind these to `resource: Server` instead of the
|
||||||
|
/// per-graph `resource: Graph`. Operators authorize a group with:
|
||||||
|
/// ```yaml
|
||||||
|
/// rules:
|
||||||
|
/// - id: admins-can-list-graphs
|
||||||
|
/// allow:
|
||||||
|
/// actors: { group: admins }
|
||||||
|
/// actions: [graph_list]
|
||||||
|
/// ```
|
||||||
|
/// `branch_scope` and `target_branch_scope` are NOT supported for
|
||||||
|
/// these actions — there's no branch context at the server level.
|
||||||
|
/// `graph_delete` is intentionally omitted from PR 6a; it lands
|
||||||
|
/// alongside `DELETE /graphs/{id}` in a future release.
|
||||||
|
GraphCreate,
|
||||||
|
/// See `GraphCreate`. Currently the only `Server`-scoped action
|
||||||
|
/// wired into an HTTP endpoint (`GET /graphs`).
|
||||||
|
GraphList,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PolicyAction {
|
impl PolicyAction {
|
||||||
|
|
@ -52,6 +71,8 @@ impl PolicyAction {
|
||||||
Self::BranchDelete => "branch_delete",
|
Self::BranchDelete => "branch_delete",
|
||||||
Self::BranchMerge => "branch_merge",
|
Self::BranchMerge => "branch_merge",
|
||||||
Self::Admin => "admin",
|
Self::Admin => "admin",
|
||||||
|
Self::GraphCreate => "graph_create",
|
||||||
|
Self::GraphList => "graph_list",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +86,36 @@ impl PolicyAction {
|
||||||
Self::BranchCreate | Self::SchemaApply | Self::BranchDelete | Self::BranchMerge
|
Self::BranchCreate | Self::SchemaApply | Self::BranchDelete | Self::BranchMerge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which Cedar resource entity governs this action.
|
||||||
|
/// Per-graph actions (Read, Change, …) apply to `Omnigraph::Graph::"<id>"`.
|
||||||
|
/// Server-scoped management actions (GraphCreate, GraphList) apply to
|
||||||
|
/// `Omnigraph::Server::"root"`. `Admin` is reserved without a current
|
||||||
|
/// call site; classified as per-graph until MR-724 picks a shape.
|
||||||
|
pub fn resource_kind(self) -> PolicyResourceKind {
|
||||||
|
match self {
|
||||||
|
Self::GraphCreate | Self::GraphList => PolicyResourceKind::Server,
|
||||||
|
Self::Read
|
||||||
|
| Self::Export
|
||||||
|
| Self::Change
|
||||||
|
| Self::SchemaApply
|
||||||
|
| Self::BranchCreate
|
||||||
|
| Self::BranchDelete
|
||||||
|
| Self::BranchMerge
|
||||||
|
| Self::Admin => PolicyResourceKind::Graph,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which Cedar entity an action's policies apply to. Internal to
|
||||||
|
/// `omnigraph-policy` — drives the `compile_policy_source` template
|
||||||
|
/// and the request-time resource UID construction.
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum PolicyResourceKind {
|
||||||
|
/// `Omnigraph::Graph::"<graph_label>"` — per-graph actions.
|
||||||
|
Graph,
|
||||||
|
/// `Omnigraph::Server::"root"` — management actions.
|
||||||
|
Server,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PolicyAction {
|
impl fmt::Display for PolicyAction {
|
||||||
|
|
@ -86,6 +137,8 @@ impl FromStr for PolicyAction {
|
||||||
"branch_delete" => Ok(Self::BranchDelete),
|
"branch_delete" => Ok(Self::BranchDelete),
|
||||||
"branch_merge" => Ok(Self::BranchMerge),
|
"branch_merge" => Ok(Self::BranchMerge),
|
||||||
"admin" => Ok(Self::Admin),
|
"admin" => Ok(Self::Admin),
|
||||||
|
"graph_create" => Ok(Self::GraphCreate),
|
||||||
|
"graph_list" => Ok(Self::GraphList),
|
||||||
other => bail!("unknown policy action '{other}'"),
|
other => bail!("unknown policy action '{other}'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,6 +315,35 @@ impl PolicyConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// MR-668: server-scoped actions have no branch context and
|
||||||
|
// must not be mixed with per-graph actions in the same
|
||||||
|
// rule (each rule generates one Cedar `permit` referencing
|
||||||
|
// a specific resource kind).
|
||||||
|
let mut server_scoped = false;
|
||||||
|
let mut graph_scoped = false;
|
||||||
|
for action in &rule.allow.actions {
|
||||||
|
match action.resource_kind() {
|
||||||
|
PolicyResourceKind::Server => server_scoped = true,
|
||||||
|
PolicyResourceKind::Graph => graph_scoped = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if server_scoped && graph_scoped {
|
||||||
|
bail!(
|
||||||
|
"policy rule '{}' mixes server-scoped actions (graph_create, graph_list) \
|
||||||
|
with per-graph actions; split into separate rules",
|
||||||
|
rule.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if server_scoped
|
||||||
|
&& (rule.allow.branch_scope.is_some()
|
||||||
|
|| rule.allow.target_branch_scope.is_some())
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"policy rule '{}' uses branch_scope/target_branch_scope with a \
|
||||||
|
server-scoped action; server-scoped actions have no branch context",
|
||||||
|
rule.id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -349,7 +431,14 @@ impl PolicyEngine {
|
||||||
|
|
||||||
let principal = entity_uid("Actor", &request.actor_id)?;
|
let principal = entity_uid("Actor", &request.actor_id)?;
|
||||||
let action = entity_uid("Action", request.action.as_str())?;
|
let action = entity_uid("Action", request.action.as_str())?;
|
||||||
let resource = entity_uid("Graph", &self.graph_id)?;
|
// MR-668 PR 6a: pick the resource entity based on the action's
|
||||||
|
// `resource_kind`. Server-scoped actions (`graph_create`,
|
||||||
|
// `graph_list`) bind to `Omnigraph::Server::"root"`; per-graph
|
||||||
|
// actions bind to `Omnigraph::Graph::"<graph_label>"`.
|
||||||
|
let resource = match request.action.resource_kind() {
|
||||||
|
PolicyResourceKind::Server => entity_uid("Server", SERVER_RESOURCE_ID)?,
|
||||||
|
PolicyResourceKind::Graph => entity_uid("Graph", &self.graph_id)?,
|
||||||
|
};
|
||||||
let context_value = json!({
|
let context_value = json!({
|
||||||
"has_branch": request.branch.is_some(),
|
"has_branch": request.branch.is_some(),
|
||||||
"branch": request.branch.clone().unwrap_or_default(),
|
"branch": request.branch.clone().unwrap_or_default(),
|
||||||
|
|
@ -505,6 +594,26 @@ fn compile_entities(config: &PolicyConfig, graph_id: &str, schema: &Schema) -> R
|
||||||
entities.extend(group_entities);
|
entities.extend(group_entities);
|
||||||
entities.extend(actor_entities);
|
entities.extend(actor_entities);
|
||||||
entities.push(graph_entity);
|
entities.push(graph_entity);
|
||||||
|
|
||||||
|
// MR-668 PR 6a: include the `Omnigraph::Server::"root"` entity
|
||||||
|
// whenever any rule references a server-scoped action. Cedar's
|
||||||
|
// schema validator will otherwise reject the policy. Keeping this
|
||||||
|
// conditional (rather than always-on) avoids polluting test
|
||||||
|
// assertions for graph-only policies.
|
||||||
|
let any_server_scoped = config.rules.iter().any(|rule| {
|
||||||
|
rule.allow
|
||||||
|
.actions
|
||||||
|
.iter()
|
||||||
|
.any(|action| action.resource_kind() == PolicyResourceKind::Server)
|
||||||
|
});
|
||||||
|
if any_server_scoped {
|
||||||
|
entities.push(Entity::new(
|
||||||
|
entity_uid("Server", SERVER_RESOURCE_ID)?,
|
||||||
|
HashMap::new(),
|
||||||
|
HashSet::<EntityUid>::new(),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Entities::from_entities(entities, Some(schema))?)
|
Ok(Entities::from_entities(entities, Some(schema))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -543,16 +652,27 @@ fn compile_policy_source(rule: &PolicyRule, action: &PolicyAction, graph_id: &st
|
||||||
format!("\nwhen {{ {} }}", conditions.join(" && "))
|
format!("\nwhen {{ {} }}", conditions.join(" && "))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MR-668 PR 6a: emit the resource literal that matches the action's
|
||||||
|
// `resource_kind`. Per-graph actions reference the engine's
|
||||||
|
// `Omnigraph::Graph::"<graph_label>"` instance; server-scoped
|
||||||
|
// actions reference the singleton `Omnigraph::Server::"root"`.
|
||||||
|
let resource_literal = match action.resource_kind() {
|
||||||
|
PolicyResourceKind::Graph => {
|
||||||
|
format!("Omnigraph::Graph::{}", cedar_literal(graph_id))
|
||||||
|
}
|
||||||
|
PolicyResourceKind::Server => {
|
||||||
|
format!("Omnigraph::Server::{}", cedar_literal(SERVER_RESOURCE_ID))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
r#"permit (
|
r#"permit (
|
||||||
principal in Omnigraph::Group::{group},
|
principal in Omnigraph::Group::{group},
|
||||||
action == Omnigraph::Action::{action},
|
action == Omnigraph::Action::{action},
|
||||||
resource == Omnigraph::Graph::{graph}
|
resource == {resource_literal}
|
||||||
){when};"#,
|
){when};"#,
|
||||||
group = cedar_literal(&rule.allow.actors.group),
|
group = cedar_literal(&rule.allow.actors.group),
|
||||||
action = cedar_literal(action.as_str()),
|
action = cedar_literal(action.as_str()),
|
||||||
graph = cedar_literal(graph_id),
|
|
||||||
when = when,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -581,6 +701,11 @@ fn target_branch_scope_condition(scope: PolicyBranchScope) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn policy_schema_source() -> &'static str {
|
fn policy_schema_source() -> &'static str {
|
||||||
|
// MR-668 PR 6a: `entity Server;` plus `graph_create`/`graph_list`
|
||||||
|
// actions that bind to it. Per-graph actions stay bound to `Graph`.
|
||||||
|
// The Cedar schema string lives here (not on a fixture file) so any
|
||||||
|
// omnigraph-policy build picks up the new vocabulary in lock-step
|
||||||
|
// with the Rust code.
|
||||||
r#"
|
r#"
|
||||||
namespace Omnigraph {
|
namespace Omnigraph {
|
||||||
type RequestContext = {
|
type RequestContext = {
|
||||||
|
|
@ -595,6 +720,7 @@ namespace Omnigraph {
|
||||||
entity Actor in [Group];
|
entity Actor in [Group];
|
||||||
entity Group;
|
entity Group;
|
||||||
entity Graph;
|
entity Graph;
|
||||||
|
entity Server;
|
||||||
|
|
||||||
action "read" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
action "read" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
||||||
action "export" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
action "export" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
||||||
|
|
@ -604,10 +730,18 @@ namespace Omnigraph {
|
||||||
action "branch_delete" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
action "branch_delete" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
||||||
action "branch_merge" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
action "branch_merge" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
||||||
action "admin" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
action "admin" appliesTo { principal: Actor, resource: Graph, context: RequestContext };
|
||||||
|
|
||||||
|
action "graph_create" appliesTo { principal: Actor, resource: Server, context: RequestContext };
|
||||||
|
action "graph_list" appliesTo { principal: Actor, resource: Server, context: RequestContext };
|
||||||
}
|
}
|
||||||
"#
|
"#
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canonical id of the `Omnigraph::Server` Cedar entity. There's only one
|
||||||
|
/// (the running server); the id is fixed at `"root"` so Cedar rules can
|
||||||
|
/// reference it unambiguously: `resource == Omnigraph::Server::"root"`.
|
||||||
|
const SERVER_RESOURCE_ID: &str = "root";
|
||||||
|
|
||||||
fn entity_uid(entity_type: &str, id: &str) -> Result<EntityUid> {
|
fn entity_uid(entity_type: &str, id: &str) -> Result<EntityUid> {
|
||||||
let typename = EntityTypeName::from_str(&format!("Omnigraph::{entity_type}"))?;
|
let typename = EntityTypeName::from_str(&format!("Omnigraph::{entity_type}"))?;
|
||||||
let entity_id = EntityId::from_str(id).map_err(|err| eyre!(err.to_string()))?;
|
let entity_id = EntityId::from_str(id).map_err(|err| eyre!(err.to_string()))?;
|
||||||
|
|
@ -997,4 +1131,167 @@ rules:
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!deny.allowed);
|
assert!(!deny.allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── MR-668 PR 6a — server-scoped actions (graph_create, graph_list) ─
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graph_list_action_authorizes_against_server_resource() {
|
||||||
|
let policy: PolicyConfig = serde_yaml::from_str(
|
||||||
|
r#"
|
||||||
|
version: 1
|
||||||
|
groups:
|
||||||
|
admins: [act-andrew]
|
||||||
|
viewers: [act-bruno]
|
||||||
|
rules:
|
||||||
|
- id: admins-list-graphs
|
||||||
|
allow:
|
||||||
|
actors: { group: admins }
|
||||||
|
actions: [graph_list]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The graph_label passed at compile time is irrelevant for
|
||||||
|
// server-scoped actions — they resolve against
|
||||||
|
// `Omnigraph::Server::"root"` regardless. We pass a sentinel
|
||||||
|
// so it's obvious the value isn't used.
|
||||||
|
let engine = PolicyCompiler::compile(&policy, "ignored").unwrap();
|
||||||
|
|
||||||
|
let allow = engine
|
||||||
|
.authorize(&PolicyRequest {
|
||||||
|
actor_id: "act-andrew".to_string(),
|
||||||
|
action: PolicyAction::GraphList,
|
||||||
|
branch: None,
|
||||||
|
target_branch: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(allow.allowed);
|
||||||
|
assert_eq!(allow.matched_rule_id.as_deref(), Some("admins-list-graphs"));
|
||||||
|
|
||||||
|
// Different actor, same policy → deny.
|
||||||
|
let deny = engine
|
||||||
|
.authorize(&PolicyRequest {
|
||||||
|
actor_id: "act-bruno".to_string(),
|
||||||
|
action: PolicyAction::GraphList,
|
||||||
|
branch: None,
|
||||||
|
target_branch: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(!deny.allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graph_create_action_authorizes_against_server_resource() {
|
||||||
|
let policy: PolicyConfig = serde_yaml::from_str(
|
||||||
|
r#"
|
||||||
|
version: 1
|
||||||
|
groups:
|
||||||
|
admins: [act-andrew]
|
||||||
|
rules:
|
||||||
|
- id: admins-create-graphs
|
||||||
|
allow:
|
||||||
|
actors: { group: admins }
|
||||||
|
actions: [graph_create, graph_list]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let engine = PolicyCompiler::compile(&policy, "ignored").unwrap();
|
||||||
|
|
||||||
|
for action in [PolicyAction::GraphCreate, PolicyAction::GraphList] {
|
||||||
|
let allow = engine
|
||||||
|
.authorize(&PolicyRequest {
|
||||||
|
actor_id: "act-andrew".to_string(),
|
||||||
|
action,
|
||||||
|
branch: None,
|
||||||
|
target_branch: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(allow.allowed, "act-andrew must be allowed {action}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_scoped_rule_cannot_use_branch_scope() {
|
||||||
|
let policy: PolicyConfig = serde_yaml::from_str(
|
||||||
|
r#"
|
||||||
|
version: 1
|
||||||
|
groups:
|
||||||
|
admins: [act-andrew]
|
||||||
|
rules:
|
||||||
|
- id: bad-branch-scope-on-graph-list
|
||||||
|
allow:
|
||||||
|
actors: { group: admins }
|
||||||
|
actions: [graph_list]
|
||||||
|
branch_scope: any
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let err = policy.validate().unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("branch_scope") || msg.contains("server-scoped"),
|
||||||
|
"expected branch_scope rejection for server-scoped action; got: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_mixing_server_and_per_graph_actions_is_rejected() {
|
||||||
|
// A single rule must reference exactly one resource kind.
|
||||||
|
// `graph_list` (Server) + `read` (Graph) in one allow block
|
||||||
|
// is invalid — operators must split the rule.
|
||||||
|
let policy: PolicyConfig = serde_yaml::from_str(
|
||||||
|
r#"
|
||||||
|
version: 1
|
||||||
|
groups:
|
||||||
|
admins: [act-andrew]
|
||||||
|
rules:
|
||||||
|
- id: mixed-resource-kinds
|
||||||
|
allow:
|
||||||
|
actors: { group: admins }
|
||||||
|
actions: [graph_list, read]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let err = policy.validate().unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("server-scoped") || msg.contains("split into separate rules"),
|
||||||
|
"expected mix-resource-kinds rejection; got: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn per_graph_rules_continue_to_work_alongside_server_rules() {
|
||||||
|
// Decision 6 contract: existing operator policies (which only
|
||||||
|
// reference per-graph actions) keep compiling and authorizing
|
||||||
|
// as before, even when the compiled-in schema now declares
|
||||||
|
// `Server` + `graph_*` actions. This pins the "Cedar refactor
|
||||||
|
// is operator-invisible" promise.
|
||||||
|
let policy: PolicyConfig = serde_yaml::from_str(
|
||||||
|
r#"
|
||||||
|
version: 1
|
||||||
|
groups:
|
||||||
|
team: [act-andrew]
|
||||||
|
protected_branches: [main]
|
||||||
|
rules:
|
||||||
|
- id: team-read
|
||||||
|
allow:
|
||||||
|
actors: { group: team }
|
||||||
|
actions: [read, export]
|
||||||
|
branch_scope: any
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let engine = PolicyCompiler::compile(&policy, "graph").unwrap();
|
||||||
|
let allow = engine
|
||||||
|
.authorize(&PolicyRequest {
|
||||||
|
actor_id: "act-andrew".to_string(),
|
||||||
|
action: PolicyAction::Read,
|
||||||
|
branch: Some("main".to_string()),
|
||||||
|
target_branch: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(allow.allowed);
|
||||||
|
assert_eq!(allow.matched_rule_id.as_deref(), Some("team-read"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue