core_policy/
builder.rs

1//! Builder pattern for ergonomic policy construction
2
3use crate::context_expr::ContextExpr;
4use crate::error::{PolicyError, Result};
5use crate::policy::{Action, Policy, PolicyRule, Resource};
6use alloc::collections::BTreeMap;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9
10/// Builder for creating `PolicyRule` instances with a fluent API
11///
12/// # Examples
13///
14/// ```
15/// use core_policy::builder::PolicyRuleBuilder;
16/// use core_policy::{Action, Resource};
17///
18/// # fn example() -> Result<(), core_policy::PolicyError> {
19/// // Basic RBAC rule
20/// let rule = PolicyRuleBuilder::new()
21///     .for_peer("12D3KooWXYZ...")
22///     .allow(Action::Read)
23///     .on(Resource::File("/docs/*".into()))
24///     .build()
25///     .unwrap();
26///
27/// // ABAC rule with expiration
28/// let rule = PolicyRuleBuilder::new()
29///     .for_peer("technician")
30///     .allow(Action::Read)
31///     .on(Resource::File("/logs/*".into()))
32///     .expires_at(1762348800)
33///     .build()
34///     .unwrap();
35///
36/// // ABAC rule with attributes
37/// let rule = PolicyRuleBuilder::new()
38///     .for_peer("alice")
39///     .allow(Action::Write)
40///     .on(Resource::File("/shared/*".into()))
41///     .with_attribute("location", "office")
42///     .with_attribute("security_level", "high")
43///     .build()
44///     .unwrap();
45///
46/// // ABAC rule with context expression (advanced)
47/// let rule = PolicyRuleBuilder::new()
48///     .for_peer("alice")
49///     .allow(Action::Read)
50///     .on(Resource::File("/sensitive/*".into()))
51///     .with_context_expr("role == \"admin\" AND department == \"IT\"")?  // Returns Result
52///     .build()
53///     .unwrap();
54/// # Ok(())
55/// # }
56/// ```
57#[derive(Debug, Default)]
58pub struct PolicyRuleBuilder {
59    peer_id: Option<String>,
60    action: Option<Action>,
61    resource: Option<Resource>,
62    expires_at: Option<u64>,
63    attributes: BTreeMap<String, String>,
64    context_expr: Option<ContextExpr>,
65}
66
67impl PolicyRuleBuilder {
68    /// Create a new builder
69    #[must_use]
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Set the peer ID that this rule applies to
75    #[must_use]
76    pub fn for_peer(mut self, peer_id: impl Into<String>) -> Self {
77        self.peer_id = Some(peer_id.into());
78        self
79    }
80
81    /// Set the action allowed by this rule
82    #[must_use]
83    pub fn allow(mut self, action: Action) -> Self {
84        self.action = Some(action);
85        self
86    }
87
88    /// Set the resource this rule applies to
89    #[must_use]
90    pub fn on(mut self, resource: Resource) -> Self {
91        self.resource = Some(resource);
92        self
93    }
94
95    /// Set the expiration timestamp (Unix seconds) - ABAC
96    #[must_use]
97    pub const fn expires_at(mut self, timestamp: u64) -> Self {
98        self.expires_at = Some(timestamp);
99        self
100    }
101
102    /// Add an attribute for contextual access control - ABAC (legacy)
103    #[must_use]
104    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
105        self.attributes.insert(key.into(), value.into());
106        self
107    }
108
109    /// Add a context expression for advanced ABAC (boolean logic)
110    ///
111    /// # Arguments
112    ///
113    /// * `expr` - Expression string to parse (e.g., "role == \"admin\" AND department == \"IT\"")
114    ///
115    /// # Errors
116    ///
117    /// Returns `PolicyError::InvalidExpression` if the expression syntax is invalid
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use core_policy::builder::PolicyRuleBuilder;
123    /// use core_policy::{Action, Resource};
124    ///
125    /// # fn example() -> Result<(), core_policy::PolicyError> {
126    /// let rule = PolicyRuleBuilder::new()
127    ///     .for_peer("alice")
128    ///     .allow(Action::Read)
129    ///     .on(Resource::All)
130    ///     .with_context_expr("role == \"admin\" AND active == \"true\"")?  // Returns Result
131    ///     .build()
132    ///     .unwrap();
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub fn with_context_expr(mut self, expr: impl AsRef<str>) -> Result<Self> {
137        let parsed = ContextExpr::parse(expr.as_ref())?;
138        self.context_expr = Some(parsed);
139        Ok(self)
140    }
141
142    /// Build the `PolicyRule`, returning an error if required fields are missing
143    ///
144    /// # Errors
145    ///
146    /// Returns `PolicyError::InvalidRule` if any required field is missing:
147    /// - `peer_id`
148    /// - `action`
149    /// - `resource`
150    pub fn build(self) -> Result<PolicyRule> {
151        let peer_id = self
152            .peer_id
153            .ok_or_else(|| PolicyError::InvalidRule("peer_id is required".to_string()))?;
154
155        let action = self
156            .action
157            .ok_or_else(|| PolicyError::InvalidRule("action is required".to_string()))?;
158
159        let resource = self
160            .resource
161            .ok_or_else(|| PolicyError::InvalidRule("resource is required".to_string()))?;
162
163        Ok(PolicyRule {
164            peer_id,
165            action,
166            resource,
167            expires_at: self.expires_at,
168            attributes: self.attributes,
169            context_expr: self.context_expr,
170        })
171    }
172}
173
174/// Builder for creating Policy instances with a fluent API
175///
176/// # Examples
177///
178/// ```
179/// use core_policy::{PolicyBuilder, PolicyRuleBuilder, Action, Resource};
180///
181/// let policy = PolicyBuilder::new("admin-policy")
182///     .add_rule_with(|rule| {
183///         rule.for_peer("12D3KooWAlice...")
184///             .allow(Action::All)
185///             .on(Resource::All)
186///     })
187///     .add_rule_with(|rule| {
188///         rule.for_peer("12D3KooWBob...")
189///             .allow(Action::Read)
190///             .on(Resource::File("/docs/*".into()))
191///     })
192///     .with_metadata("owner", "alice")
193///     .build()
194///     .unwrap();
195/// ```
196#[derive(Debug)]
197pub struct PolicyBuilder {
198    name: String,
199    valid_duration_secs: u64,
200    rules: Vec<PolicyRule>,
201    metadata: BTreeMap<String, String>,
202    timestamp: Option<u64>,
203}
204
205impl PolicyBuilder {
206    /// Create a new policy builder with default validity (30 days)
207    #[must_use]
208    pub fn new(name: impl Into<String>) -> Self {
209        Self {
210            timestamp: None,
211            name: name.into(),
212            rules: Vec::new(),
213            metadata: BTreeMap::new(),
214            valid_duration_secs: 30 * 24 * 60 * 60,
215        }
216    }
217
218    /// Set the policy issuance timestamp (Unix seconds).
219    ///
220    /// Required for valid time-based policies. If not set, defaults to 0.
221    #[must_use]
222    pub fn with_timestamp(mut self, timestamp: u64) -> Self {
223        self.timestamp = Some(timestamp);
224        self
225    }
226
227    /// Set policy validity duration in seconds
228    #[must_use]
229    pub const fn valid_for(mut self, duration_secs: u64) -> Self {
230        self.valid_duration_secs = duration_secs;
231        self
232    }
233
234    /// Add a rule using a builder function
235    #[must_use]
236    pub fn add_rule_with<F>(mut self, f: F) -> Self
237    where
238        F: FnOnce(PolicyRuleBuilder) -> PolicyRuleBuilder,
239    {
240        let builder = f(PolicyRuleBuilder::new());
241        if let Ok(rule) = builder.build() {
242            self.rules.push(rule);
243        }
244        self
245    }
246
247    /// Add a pre-constructed rule
248    #[must_use]
249    pub fn add_rule(mut self, rule: PolicyRule) -> Self {
250        self.rules.push(rule);
251        self
252    }
253
254    /// Add metadata to the policy
255    #[must_use]
256    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
257        self.metadata.insert(key.into(), value.into());
258        self
259    }
260
261    /// Build the Policy, returning an error if validation fails
262    ///
263    /// # Errors
264    ///
265    /// Returns an error if policy validation fails (see `Policy::validate()`)
266    pub fn build(self) -> Result<Policy> {
267        let now = self.timestamp.unwrap_or(0);
268        let mut policy = Policy::new(self.name, self.valid_duration_secs, now)?;
269
270        // Add all rules
271        for rule in self.rules {
272            policy = policy.add_rule(rule)?;
273        }
274
275        // Add all metadata
276        for (key, value) in self.metadata {
277            policy = policy.with_metadata(key, value);
278        }
279
280        policy.validate()?;
281        Ok(policy)
282    }
283}
284
285// Convenience methods for Action construction
286impl Action {
287    /// Create a Read action
288    #[must_use]
289    pub const fn read() -> Self {
290        Self::Read
291    }
292
293    /// Create a Write action
294    #[must_use]
295    pub const fn write() -> Self {
296        Self::Write
297    }
298
299    /// Create an Execute action
300    #[must_use]
301    pub const fn execute() -> Self {
302        Self::Execute
303    }
304
305    /// Create a Delete action
306    #[must_use]
307    pub const fn delete() -> Self {
308        Self::Delete
309    }
310
311    /// Create an All action (wildcard)
312    #[must_use]
313    pub const fn all() -> Self {
314        Self::All
315    }
316
317    /// Create a custom action
318    #[must_use]
319    pub fn custom(name: impl Into<String>) -> Self {
320        Self::Custom(name.into())
321    }
322}
323
324// Convenience methods for Resource construction
325impl Resource {
326    /// Create a File resource
327    #[must_use]
328    pub fn file(path: impl Into<String>) -> Self {
329        Self::File(path.into())
330    }
331
332    /// Create a USB device resource
333    #[must_use]
334    pub fn usb(device: impl Into<String>) -> Self {
335        Self::Usb(device.into())
336    }
337
338    /// Create a Tunnel resource
339    #[must_use]
340    pub fn tunnel(name: impl Into<String>) -> Self {
341        Self::Tunnel(name.into())
342    }
343
344    /// Create an All resource (wildcard)
345    #[must_use]
346    pub const fn all() -> Self {
347        Self::All
348    }
349
350    /// Create a custom resource
351    #[must_use]
352    pub fn custom(resource_type: impl Into<String>, path: impl Into<String>) -> Self {
353        Self::Custom {
354            resource_type: resource_type.into(),
355            path: path.into(),
356        }
357    }
358}