1use serde::{Deserialize, Serialize};
2
3use crate::storage::{StorageBackend, StorageEntry, StorageError};
4
5const POLICY_PREFIX: &str = "sys/policy/";
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Capability {
10 Read,
11 Create,
12 Update,
13 Delete,
14 List,
15 Sudo,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PathRule {
20 pub prefix: String,
21 pub capabilities: Vec<Capability>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct Policy {
26 pub name: String,
27 pub rules: Vec<PathRule>,
28}
29
30impl Policy {
31 pub fn is_allowed(&self, path: &str, capability: Capability) -> bool {
33 self.rules
34 .iter()
35 .filter(|r| path.starts_with(&r.prefix))
36 .max_by_key(|r| r.prefix.len())
37 .is_some_and(|r| r.capabilities.contains(&capability))
38 }
39}
40
41pub fn evaluate(policies: &[Policy], path: &str, capability: Capability) -> bool {
42 policies.iter().any(|p| p.is_allowed(path, capability))
43}
44
45pub async fn store_policy(storage: &dyn StorageBackend, policy: &Policy) -> Result<(), StorageError> {
46 let path = format!("{POLICY_PREFIX}{}", policy.name);
47 let value = serde_json::to_vec(policy).map_err(|e| StorageError::Backend(e.to_string()))?;
48 storage
49 .put(
50 &path,
51 StorageEntry {
52 value,
53 expires_at: None,
54 },
55 )
56 .await
57}
58
59pub async fn get_policy(
60 storage: &dyn StorageBackend,
61 name: &str,
62) -> Result<Option<Policy>, StorageError> {
63 let path = format!("{POLICY_PREFIX}{name}");
64 let Some(entry) = storage.get(&path).await? else {
65 return Ok(None);
66 };
67 let policy: Policy =
68 serde_json::from_slice(&entry.value).map_err(|e| StorageError::Backend(e.to_string()))?;
69 Ok(Some(policy))
70}
71
72pub async fn delete_policy(storage: &dyn StorageBackend, name: &str) -> Result<(), StorageError> {
73 storage.delete(&format!("{POLICY_PREFIX}{name}")).await
74}
75
76pub async fn load_policies(
79 storage: &dyn StorageBackend,
80 names: &[String],
81) -> Result<Vec<Policy>, StorageError> {
82 let mut policies = Vec::with_capacity(names.len());
83 for name in names {
84 if let Some(policy) = get_policy(storage, name).await? {
85 policies.push(policy);
86 }
87 }
88 Ok(policies)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn policy(rules: Vec<(&str, Vec<Capability>)>) -> Policy {
96 Policy {
97 name: "test".into(),
98 rules: rules
99 .into_iter()
100 .map(|(prefix, capabilities)| PathRule {
101 prefix: prefix.to_string(),
102 capabilities,
103 })
104 .collect(),
105 }
106 }
107
108 #[test]
109 fn deny_by_default() {
110 let p = policy(vec![]);
111 assert!(!p.is_allowed("secret/foo", Capability::Read));
112 }
113
114 #[test]
115 fn longest_prefix_wins() {
116 let p = policy(vec![
117 ("secret/", vec![Capability::Read]),
118 ("secret/admin/", vec![Capability::Sudo]),
119 ]);
120 assert!(p.is_allowed("secret/foo", Capability::Read));
121 assert!(!p.is_allowed("secret/admin/x", Capability::Read));
122 assert!(p.is_allowed("secret/admin/x", Capability::Sudo));
123 }
124
125 #[test]
126 fn evaluate_across_multiple_policies() {
127 let a = policy(vec![("secret/", vec![Capability::Read])]);
128 let b = policy(vec![("sys/", vec![Capability::Sudo])]);
129 assert!(evaluate(&[a.clone(), b.clone()], "secret/foo", Capability::Read));
130 assert!(evaluate(&[a, b], "sys/policy", Capability::Sudo));
131 }
132}