1use parking_lot::RwLock;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::observability::AuditLog;
8use crate::security::capability::{Capability, CapabilitySet, CapabilitySubject};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum DefaultPolicy {
13 DenyAll,
15 AllowAll,
17}
18
19pub struct Authorizer {
28 grants: Arc<RwLock<HashMap<CapabilitySubject, CapabilitySet>>>,
30 roles: Arc<RwLock<HashMap<String, CapabilitySet>>>,
32 role_bindings: Arc<RwLock<HashMap<String, Vec<String>>>>,
34 default_policy: DefaultPolicy,
36 audit: Arc<AuditLog>,
38}
39
40impl Authorizer {
41 pub fn new(audit: Arc<AuditLog>) -> Self {
43 Self {
44 grants: Arc::new(RwLock::new(HashMap::new())),
45 roles: Arc::new(RwLock::new(HashMap::new())),
46 role_bindings: Arc::new(RwLock::new(HashMap::new())),
47 default_policy: DefaultPolicy::DenyAll,
48 audit,
49 }
50 }
51
52 pub fn new_permissive(audit: Arc<AuditLog>) -> Self {
54 Self {
55 grants: Arc::new(RwLock::new(HashMap::new())),
56 roles: Arc::new(RwLock::new(HashMap::new())),
57 role_bindings: Arc::new(RwLock::new(HashMap::new())),
58 default_policy: DefaultPolicy::AllowAll,
59 audit,
60 }
61 }
62
63 pub fn grant(&self, subject: CapabilitySubject, caps: CapabilitySet) {
67 self.grants.write().insert(subject, caps);
68 }
69
70 pub fn grant_one(&self, subject: CapabilitySubject, cap: Capability) {
72 let mut grants = self.grants.write();
73 grants
74 .entry(subject)
75 .and_modify(|set| {
76 let mut new_caps = CapabilitySet::new(set.capabilities().to_vec());
77 new_caps.add(cap.clone());
78 *set = new_caps;
79 })
80 .or_insert_with(|| CapabilitySet::new(vec![cap]));
81 }
82
83 pub fn revoke(&self, subject: &CapabilitySubject) {
85 self.grants.write().remove(subject);
86 }
87
88 pub fn define_role(&self, role_name: &str, caps: CapabilitySet) {
92 self.roles.write().insert(role_name.to_string(), caps);
93 }
94
95 pub fn bind_role(&self, agent_id: &str, role_name: &str) {
97 self.role_bindings
98 .write()
99 .entry(agent_id.to_string())
100 .or_default()
101 .push(role_name.to_string());
102 }
103
104 pub fn unbind_role(&self, agent_id: &str, role_name: &str) {
106 if let Some(roles) = self.role_bindings.write().get_mut(agent_id) {
107 roles.retain(|r| r != role_name);
108 }
109 }
110
111 pub fn check(&self, subject: &CapabilitySubject, required: &Capability) -> bool {
115 let result = self.evaluate(subject, required);
116 self.audit
117 .log(crate::observability::AuditEntry::security_decision(
118 subject.to_string(),
119 format!("{:?}", required),
120 result,
121 ));
122 result
123 }
124
125 pub fn require(
127 &self,
128 subject: &CapabilitySubject,
129 required: &Capability,
130 ) -> Result<(), crate::error::SdkError> {
131 if self.check(subject, required) {
132 Ok(())
133 } else {
134 Err(crate::error::SdkError::PermissionDenied {
135 subject: subject.to_string(),
136 capability: format!("{:?}", required),
137 })
138 }
139 }
140
141 fn evaluate(&self, subject: &CapabilitySubject, required: &Capability) -> bool {
144 let grants = self.grants.read();
146 if let Some(set) = grants.get(subject)
147 && !set.is_expired()
148 && set.satisfies(required)
149 {
150 return true;
151 }
152 drop(grants);
153
154 if let CapabilitySubject::Agent(id) = subject {
156 let bindings = self.role_bindings.read();
157 if let Some(roles) = bindings.get(id) {
158 let role_defs = self.roles.read();
159 for role_name in roles {
160 if let Some(role_caps) = role_defs.get(role_name)
161 && !role_caps.is_expired()
162 && role_caps.satisfies(required)
163 {
164 return true;
165 }
166 }
167 }
168 }
169
170 matches!(self.default_policy, DefaultPolicy::AllowAll)
172 }
173}
174
175impl std::fmt::Debug for Authorizer {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.debug_struct("Authorizer")
178 .field("default_policy", &self.default_policy)
179 .field("grant_count", &self.grants.read().len())
180 .field("role_count", &self.roles.read().len())
181 .finish()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn test_authorizer() -> Authorizer {
190 Authorizer::new(Arc::new(AuditLog::new(64)))
191 }
192
193 #[test]
194 fn direct_grant_check() {
195 let auth = test_authorizer();
196 auth.grant(
197 CapabilitySubject::Agent("a1".into()),
198 CapabilitySet::coding("/workspace"),
199 );
200 assert!(auth.check(
201 &CapabilitySubject::Agent("a1".into()),
202 &Capability::FileRead {
203 path_pattern: "/workspace/src/main.rs".into()
204 },
205 ));
206 assert!(!auth.check(
207 &CapabilitySubject::Agent("a1".into()),
208 &Capability::FileWrite {
209 path_pattern: "/etc/passwd".into()
210 },
211 ));
212 }
213
214 #[test]
215 fn grant_one_adds_capability() {
216 let auth = test_authorizer();
217 auth.grant_one(
218 CapabilitySubject::Agent("a1".into()),
219 Capability::FileRead {
220 path_pattern: "/ws/**".into(),
221 },
222 );
223 assert!(auth.check(
224 &CapabilitySubject::Agent("a1".into()),
225 &Capability::FileRead {
226 path_pattern: "/ws/file".into()
227 },
228 ));
229 }
230
231 #[test]
232 fn revoke_removes_all() {
233 let auth = test_authorizer();
234 let subject = CapabilitySubject::Agent("a1".into());
235 auth.grant(subject.clone(), CapabilitySet::coding("/ws"));
236 auth.revoke(&subject);
237 assert!(!auth.check(
238 &subject,
239 &Capability::FileRead {
240 path_pattern: "/ws/file".into()
241 },
242 ));
243 }
244
245 #[test]
246 fn role_based_access() {
247 let auth = test_authorizer();
248 auth.define_role("coder", CapabilitySet::coding("/workspace"));
249 auth.bind_role("agent-001", "coder");
250 assert!(auth.check(
251 &CapabilitySubject::Agent("agent-001".into()),
252 &Capability::FileRead {
253 path_pattern: "/workspace/any".into()
254 },
255 ));
256 assert!(!auth.check(
257 &CapabilitySubject::Agent("agent-001".into()),
258 &Capability::FileWrite {
259 path_pattern: "/etc/passwd".into()
260 },
261 ));
262 }
263
264 #[test]
265 fn multi_role_binding() {
266 let auth = test_authorizer();
267 auth.define_role("coder", CapabilitySet::coding("/ws"));
268 auth.define_role("browser", CapabilitySet::browser("/ws"));
269 auth.bind_role("agent-001", "coder");
270 auth.bind_role("agent-001", "browser");
271 assert!(auth.check(
273 &CapabilitySubject::Agent("agent-001".into()),
274 &Capability::FileWrite {
275 path_pattern: "/ws/src/main.rs".into()
276 },
277 ));
278 assert!(auth.check(
280 &CapabilitySubject::Agent("agent-001".into()),
281 &Capability::WebBrowse {
282 allowed_domains: vec!["*".into()]
283 },
284 ));
285 }
286
287 #[test]
288 fn unbind_role() {
289 let auth = test_authorizer();
290 auth.define_role("coder", CapabilitySet::coding("/ws"));
291 auth.bind_role("a1", "coder");
292 auth.unbind_role("a1", "coder");
293 assert!(!auth.check(
294 &CapabilitySubject::Agent("a1".into()),
295 &Capability::FileRead {
296 path_pattern: "/ws/any".into()
297 },
298 ));
299 }
300
301 #[test]
302 fn deny_by_default() {
303 let auth = test_authorizer();
304 assert!(!auth.check(
305 &CapabilitySubject::Agent("unknown".into()),
306 &Capability::FileRead {
307 path_pattern: "/any".into()
308 },
309 ));
310 }
311
312 #[test]
313 fn permissive_allows_all() {
314 let auth = Authorizer::new_permissive(Arc::new(AuditLog::new(64)));
315 assert!(auth.check(
316 &CapabilitySubject::Agent("anyone".into()),
317 &Capability::FileRead {
318 path_pattern: "/any".into()
319 },
320 ));
321 }
322
323 #[test]
324 fn require_success() {
325 let auth = test_authorizer();
326 auth.grant(
327 CapabilitySubject::Agent("a1".into()),
328 CapabilitySet::read_only("/ws"),
329 );
330 assert!(
331 auth.require(
332 &CapabilitySubject::Agent("a1".into()),
333 &Capability::FileRead {
334 path_pattern: "/ws/file".into()
335 },
336 )
337 .is_ok()
338 );
339 }
340
341 #[test]
342 fn require_failure() {
343 let auth = test_authorizer();
344 let result = auth.require(
345 &CapabilitySubject::Agent("a1".into()),
346 &Capability::FileRead {
347 path_pattern: "/ws/file".into(),
348 },
349 );
350 assert!(result.is_err());
351 let err = result.unwrap_err();
352 assert!(err.to_string().contains("permission denied"));
353 }
354}