typesec_rbac/graph_policy/
engine.rs1use grust::prelude::{Graph, GraphSchema};
4use serde::Deserialize;
5use serde_json::Value as JsonValue;
6use tracing::debug;
7use typesec_core::{
8 ResourceId, SubjectId,
9 glob::GlobPattern,
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)]
21pub struct GraphPolicyDocument {
22 pub graph_policy: GraphPolicy,
24}
25
26impl GraphPolicyDocument {
27 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
29 let value: serde_yaml::Value =
30 serde_yaml::from_str(yaml).map_err(|err| format!("YAML parse error: {err}"))?;
31 let value =
32 serde_json::to_value(value).map_err(|err| format!("YAML conversion error: {err}"))?;
33 Self::from_json_value(value)
34 }
35
36 pub fn from_json(json: &str) -> Result<Self, String> {
38 let value = serde_json::from_str(json).map_err(|err| format!("JSON parse error: {err}"))?;
39 Self::from_json_value(value)
40 }
41
42 pub fn from_json_value(value: JsonValue) -> Result<Self, String> {
44 let raw: RawGraphPolicyDocument = serde_json::from_value(value)
45 .map_err(|err| format!("Graph policy schema error: {err}"))?;
46 let graph = build_typed_graph(&raw.graph_policy.graph)?;
47 Ok(Self {
48 graph_policy: GraphPolicy {
49 graph,
50 rules: raw.graph_policy.rules,
51 },
52 })
53 }
54
55 pub fn validate(&self) -> Result<(), String> {
58 validate_graph(&self.graph_policy.graph)?;
59 company_graph_schema()
65 .validate_graph(&self.graph_policy.graph)
66 .map_err(|err| err.to_string())?;
67 if self.graph_policy.rules.is_empty() {
68 return Err("graph policy must contain at least one rule".to_string());
69 }
70 for rule in &self.graph_policy.rules {
71 GlobPattern::compile(&rule.resource, "resource")?;
72 }
73 Ok(())
74 }
75
76 pub fn graph_schema(&self) -> GraphSchema {
78 company_graph_schema()
79 }
80}
81
82#[derive(Debug, Clone, Deserialize)]
84pub struct GraphPolicy {
85 #[serde(deserialize_with = "deserialize_graph")]
87 pub graph: Graph,
88 #[serde(default)]
90 pub rules: Vec<GraphRule>,
91}
92
93pub struct GraphPolicyEngine {
95 doc: GraphPolicyDocument,
96}
97
98impl GraphPolicyEngine {
99 pub fn new(doc: GraphPolicyDocument) -> Result<Self, String> {
101 doc.validate()?;
102 Ok(Self { doc })
103 }
104
105 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
107 let doc = GraphPolicyDocument::from_yaml(yaml)
108 .map_err(|err| format!("Graph policy YAML parse error: {err}"))?;
109 Self::new(doc)
110 }
111}
112
113impl PolicyEngine for GraphPolicyEngine {
114 fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
115 let subject = subject.as_str();
116 let resource = resource.as_str();
117 debug!(subject, action, resource, "graph policy check");
118
119 let graph = &self.doc.graph_policy.graph;
120 let mut allow = None;
121
122 for rule in &self.doc.graph_policy.rules {
123 if !rule_matches(graph, rule, subject, action, resource) {
124 continue;
125 }
126
127 match rule.effect {
128 RuleEffect::Deny => {
129 return PolicyResult::Deny(format!(
130 "graph policy deny rule matched '{action}' on '{resource}'"
131 ));
132 }
133 RuleEffect::Allow => {
134 allow = Some(PolicyResult::Allow);
135 }
136 }
137 }
138
139 allow.unwrap_or_else(|| {
140 PolicyResult::Deny(format!(
141 "no graph rule grants '{subject}' permission '{action}' on '{resource}'"
142 ))
143 })
144 }
145}