typesec_rbac/graph_policy/
engine.rs1use glob::Pattern;
4use grust::prelude::{Graph, GraphSchema};
5use serde::Deserialize;
6use serde_json::Value as JsonValue;
7use tracing::debug;
8use typesec_core::{
9 ResourceId, SubjectId,
10 policy::{PolicyEngine, PolicyResult},
11};
12
13use super::authored::RawGraphPolicyDocument;
14use super::eval::{rule_matches, validate_graph};
15use super::rule::{GraphRule, RuleEffect, deserialize_graph};
16use super::schema::company_graph_schema;
17use super::typed_graph::build_typed_graph;
18
19#[derive(Debug, Clone, Deserialize)]
20pub struct GraphPolicyDocument {
21 pub graph_policy: GraphPolicy,
22}
23
24impl GraphPolicyDocument {
25 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
26 let value: serde_yaml::Value =
27 serde_yaml::from_str(yaml).map_err(|err| format!("YAML parse error: {err}"))?;
28 let value =
29 serde_json::to_value(value).map_err(|err| format!("YAML conversion error: {err}"))?;
30 Self::from_json_value(value)
31 }
32
33 pub fn from_json(json: &str) -> Result<Self, String> {
34 let value = serde_json::from_str(json).map_err(|err| format!("JSON parse error: {err}"))?;
35 Self::from_json_value(value)
36 }
37
38 pub fn from_json_value(value: JsonValue) -> Result<Self, String> {
39 let raw: RawGraphPolicyDocument = serde_json::from_value(value)
40 .map_err(|err| format!("Graph policy schema error: {err}"))?;
41 let graph = build_typed_graph(&raw.graph_policy.graph)?;
42 Ok(Self {
43 graph_policy: GraphPolicy {
44 graph,
45 rules: raw.graph_policy.rules,
46 },
47 })
48 }
49
50 pub fn validate(&self) -> Result<(), String> {
51 validate_graph(&self.graph_policy.graph)?;
52 if self.graph_policy.rules.is_empty() {
53 return Err("graph policy must contain at least one rule".to_string());
54 }
55 for rule in &self.graph_policy.rules {
56 Pattern::new(&rule.resource)
57 .map_err(|err| format!("invalid resource pattern '{}': {err}", rule.resource))?;
58 }
59 Ok(())
60 }
61
62 pub fn graph_schema(&self) -> GraphSchema {
63 company_graph_schema()
64 }
65}
66
67#[derive(Debug, Clone, Deserialize)]
68pub struct GraphPolicy {
69 #[serde(deserialize_with = "deserialize_graph")]
70 pub graph: Graph,
71 #[serde(default)]
72 pub rules: Vec<GraphRule>,
73}
74
75pub struct GraphPolicyEngine {
76 doc: GraphPolicyDocument,
77}
78
79impl GraphPolicyEngine {
80 pub fn new(doc: GraphPolicyDocument) -> Result<Self, String> {
81 doc.validate()?;
82 Ok(Self { doc })
83 }
84
85 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
86 let doc = GraphPolicyDocument::from_yaml(yaml)
87 .map_err(|err| format!("Graph policy YAML parse error: {err}"))?;
88 Self::new(doc)
89 }
90}
91
92impl PolicyEngine for GraphPolicyEngine {
93 fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
94 let subject = subject.as_str();
95 let resource = resource.as_str();
96 debug!(subject, action, resource, "graph policy check");
97
98 let graph = &self.doc.graph_policy.graph;
99 let mut allow = None;
100
101 for rule in &self.doc.graph_policy.rules {
102 if !rule_matches(graph, rule, subject, action, resource) {
103 continue;
104 }
105
106 match rule.effect {
107 RuleEffect::Deny => {
108 return PolicyResult::Deny(format!(
109 "graph policy deny rule matched '{action}' on '{resource}'"
110 ));
111 }
112 RuleEffect::Allow => {
113 allow = Some(PolicyResult::Allow);
114 }
115 }
116 }
117
118 allow.unwrap_or_else(|| {
119 PolicyResult::Deny(format!(
120 "no graph rule grants '{subject}' permission '{action}' on '{resource}'"
121 ))
122 })
123 }
124}