Skip to main content

typesec_rbac/graph_policy/
rule.rs

1//! The graph-policy rule and condition AST.
2
3use std::collections::BTreeMap;
4
5use grust::prelude::{Graph, Value};
6use serde::{Deserialize, Deserializer, Serialize};
7
8/// Deserialize a Grust [`Graph`] from an inline YAML/JSON `nodes`/`edges` map.
9pub fn deserialize_graph<'de, D>(deserializer: D) -> Result<Graph, D::Error>
10where
11    D: Deserializer<'de>,
12{
13    let value = serde_yaml::Value::deserialize(deserializer)?;
14    let yaml = serde_yaml::to_string(&value).map_err(serde::de::Error::custom)?;
15    Graph::from_yaml(&yaml).map_err(serde::de::Error::custom)
16}
17
18/// One authored graph-policy rule: a subject/action/resource match plus optional
19/// graph-shaped conditions, yielding an allow or deny effect.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct GraphRule {
22    /// Whether a match grants (`allow`, the default) or forbids (`deny`).
23    #[serde(default = "allow_effect")]
24    pub effect: RuleEffect,
25    /// Match a literal subject id. Mutually informative with `subject_has_role`.
26    #[serde(default)]
27    pub subject: Option<String>,
28    /// Match any subject holding this role (via a `HAS_ROLE` edge in the graph).
29    #[serde(default)]
30    pub subject_has_role: Option<String>,
31    /// Action this rule governs (e.g. `write`).
32    pub action: String,
33    /// Resource glob this rule governs (e.g. `employee/private/*`).
34    pub resource: String,
35    /// Optional graph-shaped conditions (the `where` block) that must also hold.
36    #[serde(default, rename = "where")]
37    pub conditions: GraphConditions,
38}
39
40/// Whether a matching [`GraphRule`] grants or forbids the action.
41#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum RuleEffect {
44    /// Grant the action.
45    Allow,
46    /// Forbid the action; deny overrides any matching allow.
47    Deny,
48}
49
50fn allow_effect() -> RuleEffect {
51    RuleEffect::Allow
52}
53
54/// The `where` block: graph-shaped conditions that must all hold for a rule to
55/// match. An empty block always holds.
56#[derive(Debug, Clone, Default, Serialize, Deserialize)]
57pub struct GraphConditions {
58    /// Constrain the resource node being acted on.
59    #[serde(default)]
60    pub target: Option<TargetCondition>,
61    /// Constrain a relationship (edge) the resource encodes.
62    #[serde(default)]
63    pub relationship: Option<RelationshipCondition>,
64    /// Require a path between two nodes in the graph.
65    #[serde(default)]
66    pub path_exists: Option<PathCondition>,
67}
68
69/// Constraints on the target resource node identified by `resource_prefix`.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct TargetCondition {
72    /// Resource-id prefix that selects the node id from the request resource.
73    pub resource_prefix: String,
74    /// Required node label, if any.
75    #[serde(default)]
76    pub label: Option<String>,
77    /// Node properties that must equal these values.
78    #[serde(default)]
79    pub property_equals: BTreeMap<String, Scalar>,
80    /// Node properties that must not equal these values.
81    #[serde(default)]
82    pub property_not_equals: BTreeMap<String, Scalar>,
83}
84
85/// Constraints on a relationship (edge) addressed by `resource_prefix`.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RelationshipCondition {
88    /// Resource-id prefix that selects the `from`/`to` endpoints from the request.
89    pub resource_prefix: String,
90    /// Required edge label (e.g. `REPORTS_TO`).
91    pub edge_label: String,
92    /// Required label of the `from` endpoint, if any.
93    #[serde(default)]
94    pub from_label: Option<String>,
95    /// Required label of the `to` endpoint, if any.
96    #[serde(default)]
97    pub to_label: Option<String>,
98    /// Reject the write if it would introduce a cycle along `edge_label`.
99    #[serde(default)]
100    pub no_cycle: bool,
101    /// `from`-endpoint properties that must equal these values.
102    #[serde(default)]
103    pub from_property_equals: BTreeMap<String, Scalar>,
104    /// `to`-endpoint properties that must equal these values.
105    #[serde(default)]
106    pub to_property_equals: BTreeMap<String, Scalar>,
107    /// `from`-endpoint properties that must not equal these values.
108    #[serde(default)]
109    pub from_property_not_equals: BTreeMap<String, Scalar>,
110    /// `to`-endpoint properties that must not equal these values.
111    #[serde(default)]
112    pub to_property_not_equals: BTreeMap<String, Scalar>,
113}
114
115/// Require a path from `from` to `to` along `edge` in the given `direction`.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PathCondition {
118    /// Source node id (supports `$subject` expansion).
119    pub from: String,
120    /// Destination node id.
121    pub to: String,
122    /// Edge label to traverse.
123    pub edge: String,
124    /// Traversal direction (default `out`).
125    #[serde(default)]
126    pub direction: PathDirection,
127}
128
129/// Direction in which a [`PathCondition`] traverses its edge.
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132pub enum PathDirection {
133    /// Follow edges in their stored `from → to` direction.
134    #[default]
135    Out,
136    /// Follow edges against their stored direction.
137    In,
138    /// Follow edges in either direction.
139    Both,
140}
141
142/// A scalar property value in a condition, matching the graph's value types.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144#[serde(untagged)]
145pub enum Scalar {
146    /// Boolean value.
147    Bool(bool),
148    /// Integer value.
149    Int(i64),
150    /// Floating-point value.
151    Float(f64),
152    /// String value.
153    String(String),
154}
155
156impl Scalar {
157    pub(crate) fn as_value(&self) -> Value {
158        match self {
159            Self::Bool(value) => Value::from(*value),
160            Self::Int(value) => Value::from(*value),
161            Self::Float(value) => Value::from(*value),
162            Self::String(value) => Value::from(value),
163        }
164    }
165}