Skip to main content

typesec_rbac/graph_policy/
engine.rs

1//! The graph-policy document, engine, and [`PolicyEngine`] implementation.
2
3use 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}