agentic_identity/trust/
capability.rs1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Capability {
16 pub uri: String,
18 pub description: Option<String>,
20 pub constraints: Option<serde_json::Value>,
22}
23
24impl Capability {
25 pub fn new(uri: impl Into<String>) -> Self {
27 Self {
28 uri: uri.into(),
29 description: None,
30 constraints: None,
31 }
32 }
33
34 pub fn with_description(uri: impl Into<String>, description: impl Into<String>) -> Self {
36 Self {
37 uri: uri.into(),
38 description: Some(description.into()),
39 constraints: None,
40 }
41 }
42
43 pub fn covers(&self, requested: &str) -> bool {
51 capability_uri_covers(&self.uri, requested)
52 }
53}
54
55impl PartialEq for Capability {
56 fn eq(&self, other: &Self) -> bool {
57 self.uri == other.uri
58 }
59}
60
61impl Eq for Capability {}
62
63pub fn capability_uri_covers(granted: &str, requested: &str) -> bool {
67 if granted == "*" {
69 return true;
70 }
71
72 if granted == requested {
74 return true;
75 }
76
77 if let Some(prefix) = granted.strip_suffix(":*") {
79 if requested == prefix {
81 return true;
82 }
83 if requested.starts_with(prefix) && requested.as_bytes().get(prefix.len()) == Some(&b':') {
84 return true;
85 }
86 }
87
88 if let Some(prefix) = granted.strip_suffix("/*") {
90 if requested == prefix {
91 return true;
92 }
93 if requested.starts_with(prefix) && requested.as_bytes().get(prefix.len()) == Some(&b'/') {
94 return true;
95 }
96 }
97
98 false
99}
100
101pub fn capabilities_cover(granted: &[Capability], requested: &str) -> bool {
103 granted.iter().any(|cap| cap.covers(requested))
104}
105
106pub fn capabilities_cover_all(granted: &[Capability], requested: &[&str]) -> bool {
108 requested.iter().all(|req| capabilities_cover(granted, req))
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_exact_match() {
117 assert!(capability_uri_covers("read:calendar", "read:calendar"));
118 assert!(!capability_uri_covers("read:calendar", "write:calendar"));
119 }
120
121 #[test]
122 fn test_universal_wildcard() {
123 assert!(capability_uri_covers("*", "read:calendar"));
124 assert!(capability_uri_covers("*", "write:anything:at:all"));
125 assert!(capability_uri_covers("*", "*"));
126 }
127
128 #[test]
129 fn test_action_wildcard() {
130 assert!(capability_uri_covers("read:*", "read:calendar"));
131 assert!(capability_uri_covers("read:*", "read:email"));
132 assert!(capability_uri_covers("read:*", "read:anything:nested"));
133 assert!(!capability_uri_covers("read:*", "write:calendar"));
134 assert!(!capability_uri_covers("read:*", "reading:calendar"));
135 }
136
137 #[test]
138 fn test_nested_wildcard() {
139 assert!(capability_uri_covers(
140 "execute:deploy:*",
141 "execute:deploy:production"
142 ));
143 assert!(capability_uri_covers(
144 "execute:deploy:*",
145 "execute:deploy:staging"
146 ));
147 assert!(!capability_uri_covers(
148 "execute:deploy:*",
149 "execute:build:production"
150 ));
151 }
152
153 #[test]
154 fn test_path_wildcard() {
155 assert!(capability_uri_covers("storage/*", "storage/files"));
156 assert!(capability_uri_covers(
157 "storage/*",
158 "storage/files/readme.md"
159 ));
160 assert!(!capability_uri_covers("storage/*", "other/files"));
161 }
162
163 #[test]
164 fn test_no_partial_prefix_match() {
165 assert!(!capability_uri_covers("read:*", "reading:calendar"));
167 assert!(!capability_uri_covers("read:cal", "read:calendar"));
169 }
170
171 #[test]
172 fn test_capabilities_cover_set() {
173 let caps = vec![Capability::new("read:*"), Capability::new("write:calendar")];
174 assert!(capabilities_cover(&caps, "read:email"));
175 assert!(capabilities_cover(&caps, "write:calendar"));
176 assert!(!capabilities_cover(&caps, "write:email"));
177 }
178
179 #[test]
180 fn test_capabilities_cover_all_set() {
181 let caps = vec![Capability::new("read:*"), Capability::new("write:calendar")];
182 assert!(capabilities_cover_all(
183 &caps,
184 &["read:email", "write:calendar"]
185 ));
186 assert!(!capabilities_cover_all(
187 &caps,
188 &["read:email", "write:email"]
189 ));
190 }
191
192 #[test]
193 fn test_capability_equality() {
194 let a = Capability::new("read:calendar");
195 let b = Capability::new("read:calendar");
196 let c = Capability::with_description("read:calendar", "Can read calendar events");
197 assert_eq!(a, b);
198 assert_eq!(a, c);
200 }
201}