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}