Skip to main content

ferrin_policy/
rego.rs

1//! In-process Rego evaluation with `regorus`.
2
3use std::fmt;
4
5use ferrin_spec::BoxFuture;
6use ferrin_spec::JsonValue;
7use regorus::Engine;
8use regorus::Value;
9
10use crate::client::PolicyClient;
11use crate::error::PolicyError;
12use crate::path::PolicyPath;
13
14/// Evaluates Rego policies in-process.
15///
16/// Policies and data documents are loaded once through the builder; every
17/// evaluation clones the prepared engine, sets the input and evaluates the
18/// rule `data.<path>`. An undefined rule value yields `JsonValue::Null`
19/// (not applicable); a rule path that does not exist is an
20/// [`PolicyError::Engine`] error, which approval policies treat as a
21/// denial.
22pub struct RegoPolicyClient {
23    engine: Engine,
24    packages: Vec<String>,
25}
26
27impl fmt::Debug for RegoPolicyClient {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.debug_struct("RegoPolicyClient")
30            .field("packages", &self.packages)
31            .finish_non_exhaustive()
32    }
33}
34
35impl RegoPolicyClient {
36    /// Starts building a client.
37    #[must_use]
38    pub fn builder() -> RegoPolicyClientBuilder {
39        RegoPolicyClientBuilder::default()
40    }
41
42    /// Creates a client from a single policy module.
43    ///
44    /// # Errors
45    ///
46    /// Returns [`PolicyError::Engine`] when the policy does not parse.
47    pub fn from_policy(
48        name: impl Into<String>,
49        source: impl Into<String>,
50    ) -> Result<Self, PolicyError> {
51        Self::builder().policy(name, source).build()
52    }
53
54    /// The `data.<package>` paths of the loaded policy modules.
55    #[must_use]
56    pub fn packages(&self) -> &[String] {
57        &self.packages
58    }
59
60    #[tracing::instrument(skip_all, fields(path))]
61    fn evaluate_sync(&self, path: &str, input: JsonValue) -> Result<JsonValue, PolicyError> {
62        let path = PolicyPath::parse(path)?;
63        let mut engine = self.engine.clone();
64        engine.set_input(Value::from(input));
65        let value = engine.eval_rule(path.rego_rule()).map_err(engine_error)?;
66        to_json(value)
67    }
68}
69
70impl PolicyClient for RegoPolicyClient {
71    fn evaluate<'a>(
72        &'a self,
73        path: &'a str,
74        input: JsonValue,
75    ) -> BoxFuture<'a, Result<JsonValue, PolicyError>> {
76        let result = self.evaluate_sync(path, input);
77        Box::pin(async move { result })
78    }
79}
80
81fn engine_error(error: impl fmt::Display) -> PolicyError {
82    PolicyError::Engine {
83        message: error.to_string(),
84    }
85}
86
87fn to_json(value: Value) -> Result<JsonValue, PolicyError> {
88    if matches!(value, Value::Undefined) {
89        return Ok(JsonValue::Null);
90    }
91    serde_json::to_value(&value).map_err(|error| PolicyError::Engine {
92        message: format!("could not convert the decision to JSON: {error}"),
93    })
94}
95
96/// Builder of a [`RegoPolicyClient`].
97#[derive(Debug, Default)]
98pub struct RegoPolicyClientBuilder {
99    policies: Vec<(String, String)>,
100    data: Vec<JsonValue>,
101}
102
103impl RegoPolicyClientBuilder {
104    /// Adds a policy module; `name` labels it in error messages.
105    #[must_use]
106    pub fn policy(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
107        self.policies.push((name.into(), source.into()));
108        self
109    }
110
111    /// Adds a data document (merged into `data` with earlier documents).
112    #[must_use]
113    pub fn data(mut self, data: JsonValue) -> Self {
114        self.data.push(data);
115        self
116    }
117
118    /// Parses the policies and loads the data.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`PolicyError::Engine`] when a policy does not parse or a data
123    /// document cannot be merged.
124    pub fn build(self) -> Result<RegoPolicyClient, PolicyError> {
125        let mut engine = Engine::new();
126        let mut packages = Vec::with_capacity(self.policies.len());
127        for (name, source) in self.policies {
128            packages.push(engine.add_policy(name, source).map_err(engine_error)?);
129        }
130        for data in self.data {
131            engine.add_data(Value::from(data)).map_err(engine_error)?;
132        }
133        Ok(RegoPolicyClient { engine, packages })
134    }
135}