zino_auth/
rego_engine.rs

1use parking_lot::Mutex;
2use regorus::{Engine, Value};
3use std::{fs, io::ErrorKind};
4use zino_core::{
5    LazyLock,
6    application::{Agent, Application},
7    error::Error,
8};
9
10/// Rego evaluation engine.
11pub struct RegoEngine {
12    /// The engine.
13    engine: Mutex<Engine>,
14}
15
16impl RegoEngine {
17    /// Creates a new instance.
18    #[inline]
19    pub fn new() -> Self {
20        Self {
21            engine: Mutex::new(Engine::default()),
22        }
23    }
24
25    /// Adds a policy.
26    #[inline]
27    pub fn add_policy(
28        &self,
29        path: impl Into<String>,
30        rego: impl Into<String>,
31    ) -> Result<String, Error> {
32        self.engine
33            .lock()
34            .add_policy(path.into(), rego.into())
35            .map_err(|err| Error::new(err.to_string()))
36    }
37
38    /// Adds the data document.
39    #[inline]
40    pub fn add_data(&self, value: impl Into<Value>) -> Result<(), Error> {
41        self.engine
42            .lock()
43            .add_data(value.into())
44            .map_err(|err| Error::new(err.to_string()))
45    }
46
47    /// Adds the data document in the JSON format.
48    #[inline]
49    pub fn add_data_json(&self, data_json: &str) -> Result<(), Error> {
50        self.engine
51            .lock()
52            .add_data_json(data_json)
53            .map_err(|err| Error::new(err.to_string()))
54    }
55
56    /// Clears the data document.
57    #[inline]
58    pub fn clear_data(&self) {
59        self.engine.lock().clear_data()
60    }
61
62    /// Sets the input document.
63    #[inline]
64    pub fn set_input(&self, input: impl Into<Value>) {
65        self.engine.lock().set_input(input.into())
66    }
67
68    /// Sets the input document in the JSON format.
69    #[inline]
70    pub fn set_input_json(&self, input_json: &str) -> Result<(), Error> {
71        self.engine
72            .lock()
73            .set_input_json(input_json)
74            .map_err(|err| Error::new(err.to_string()))
75    }
76
77    /// Evaluates a rule at the given path.
78    #[inline]
79    pub fn eval_rule(&self, path: impl Into<String>) -> Result<Value, Error> {
80        self.engine
81            .lock()
82            .eval_rule(path.into())
83            .map_err(|err| Error::new(err.to_string()))
84    }
85
86    /// Evaluates a Rego query that produces a boolean value.
87    #[inline]
88    pub fn eval_bool_query(&self, query: impl Into<String>) -> Result<bool, Error> {
89        self.engine
90            .lock()
91            .eval_bool_query(query.into(), false)
92            .map_err(|err| Error::new(err.to_string()))
93    }
94
95    /// Evaluates an `allow` query.
96    #[inline]
97    pub fn eval_allow_query(&self, query: impl Into<String>) -> bool {
98        self.engine.lock().eval_allow_query(query.into(), false)
99    }
100
101    /// Evaluates a `deny` query.
102    #[inline]
103    pub fn eval_deny_query(&self, query: impl Into<String>) -> bool {
104        self.engine.lock().eval_deny_query(query.into(), false)
105    }
106
107    /// Returns a reference to the shared Rego engine.
108    #[inline]
109    pub fn shared() -> &'static Self {
110        &SHARED_REGO_ENGINE
111    }
112}
113
114/// Shared Rego evaluation engine.
115static SHARED_REGO_ENGINE: LazyLock<RegoEngine> = LazyLock::new(|| {
116    let engine = RegoEngine::new();
117    let opa_dir = Agent::config_dir().join("opa");
118    match fs::read_dir(opa_dir) {
119        Ok(entries) => {
120            let files = entries.filter_map(|entry| entry.ok());
121            for file in files {
122                let opa_file = file.path();
123                if opa_file.extension().is_some_and(|ext| ext == "rego") {
124                    let opa_policy = fs::read_to_string(&opa_file).unwrap_or_else(|err| {
125                        let opa_file = opa_file.display();
126                        panic!("fail to read the policy file `{opa_file}`: {err}");
127                    });
128                    let file_name = opa_file
129                        .file_name()
130                        .map(|s| s.to_string_lossy().into_owned())
131                        .unwrap_or_default();
132                    engine
133                        .add_policy(file_name, opa_policy)
134                        .unwrap_or_else(|err| {
135                            let opa_file = opa_file.display();
136                            panic!("fail to read the policy file `{opa_file}`: {err}");
137                        });
138                } else {
139                    let opa_data = fs::read_to_string(&opa_file).unwrap_or_else(|err| {
140                        let opa_file = opa_file.display();
141                        panic!("fail to read the data file `{opa_file}`: {err}");
142                    });
143                    engine
144                        .add_data_json(&opa_data)
145                        .expect("fail to add the data document for the OPA");
146                }
147            }
148        }
149        Err(err) => {
150            if err.kind() != ErrorKind::NotFound {
151                tracing::error!("{err}");
152            }
153        }
154    }
155    engine
156});