adk_auth/
access_control.rs1use crate::audit::{AuditEvent, AuditOutcome, AuditSink};
4use crate::error::{AccessDenied, AuthError};
5use crate::permission::Permission;
6use crate::role::Role;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10#[derive(Clone)]
12pub struct AccessControl {
13 roles: HashMap<String, Role>,
15 user_roles: HashMap<String, Vec<String>>,
17 audit: Option<Arc<dyn AuditSink>>,
19}
20
21impl AccessControl {
22 pub fn builder() -> AccessControlBuilder {
24 AccessControlBuilder::default()
25 }
26
27 pub fn check(&self, user: &str, permission: &Permission) -> Result<(), AccessDenied> {
29 let Some(role_names) = self.user_roles.get(user) else {
30 return Err(AccessDenied::new(user, permission.to_string()));
31 };
32
33 if self.check_roles(role_names, permission) {
34 Ok(())
35 } else {
36 Err(AccessDenied::new(user, permission.to_string()))
37 }
38 }
39
40 pub async fn check_and_audit(
42 &self,
43 user: &str,
44 permission: &Permission,
45 ) -> Result<(), AuthError> {
46 let result = self.check(user, permission);
47
48 if let Some(audit) = &self.audit {
50 let outcome = if result.is_ok() { AuditOutcome::Allowed } else { AuditOutcome::Denied };
51
52 let event = match permission {
53 Permission::Tool(name) => AuditEvent::tool_access(user, name.as_str(), outcome),
54 Permission::AllTools => AuditEvent::tool_access(user, "*", outcome),
55 Permission::Agent(name) => AuditEvent::agent_access(user, name.as_str(), outcome),
56 Permission::AllAgents => AuditEvent::agent_access(user, "*", outcome),
57 };
58
59 audit.log(event).await?;
60 }
61
62 result.map_err(AuthError::from)
63 }
64
65 pub fn user_roles(&self, user: &str) -> Vec<&Role> {
67 self.user_roles
68 .get(user)
69 .map(|names| names.iter().filter_map(|name| self.roles.get(name)).collect())
70 .unwrap_or_default()
71 }
72
73 pub fn role_names(&self) -> Vec<&str> {
75 self.roles.keys().map(|s| s.as_str()).collect()
76 }
77
78 pub fn get_role(&self, name: &str) -> Option<&Role> {
80 self.roles.get(name)
81 }
82
83 pub(crate) fn check_roles(&self, role_names: &[String], permission: &Permission) -> bool {
84 let roles: Vec<&Role> =
85 role_names.iter().filter_map(|role_name| self.roles.get(role_name)).collect();
86
87 for role in &roles {
88 if role.denied_permissions().iter().any(|denied| denied.covers(permission)) {
89 return false;
90 }
91 }
92
93 roles
94 .into_iter()
95 .any(|role| role.allowed_permissions().iter().any(|allowed| allowed.covers(permission)))
96 }
97}
98
99#[derive(Default)]
101pub struct AccessControlBuilder {
102 roles: HashMap<String, Role>,
103 user_roles: HashMap<String, Vec<String>>,
104 audit: Option<Arc<dyn AuditSink>>,
105}
106
107impl AccessControlBuilder {
108 pub fn role(mut self, role: Role) -> Self {
110 self.roles.insert(role.name.clone(), role);
111 self
112 }
113
114 pub fn assign(mut self, user: impl Into<String>, role: impl Into<String>) -> Self {
116 self.user_roles.entry(user.into()).or_default().push(role.into());
117 self
118 }
119
120 pub fn audit_sink(mut self, sink: impl AuditSink + 'static) -> Self {
122 self.audit = Some(Arc::new(sink));
123 self
124 }
125
126 pub fn build(self) -> Result<AccessControl, AuthError> {
128 for (user, roles) in &self.user_roles {
130 for role in roles {
131 if !self.roles.contains_key(role) {
132 return Err(AuthError::RoleNotFound(format!(
133 "Role '{}' assigned to user '{}' does not exist",
134 role, user
135 )));
136 }
137 }
138 }
139
140 Ok(AccessControl { roles: self.roles, user_roles: self.user_roles, audit: self.audit })
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn setup_ac() -> AccessControl {
149 let admin = Role::new("admin").allow(Permission::AllTools).allow(Permission::AllAgents);
150
151 let user = Role::new("user")
152 .allow(Permission::Tool("search".into()))
153 .deny(Permission::Tool("exec".into()));
154
155 AccessControl::builder()
156 .role(admin)
157 .role(user)
158 .assign("alice", "admin")
159 .assign("bob", "user")
160 .build()
161 .unwrap()
162 }
163
164 #[test]
165 fn test_admin_has_full_access() {
166 let ac = setup_ac();
167 assert!(ac.check("alice", &Permission::Tool("anything".into())).is_ok());
168 assert!(ac.check("alice", &Permission::AllTools).is_ok());
169 assert!(ac.check("alice", &Permission::Agent("any".into())).is_ok());
170 }
171
172 #[test]
173 fn test_user_limited_access() {
174 let ac = setup_ac();
175 assert!(ac.check("bob", &Permission::Tool("search".into())).is_ok());
177 assert!(ac.check("bob", &Permission::Tool("exec".into())).is_err());
179 assert!(ac.check("bob", &Permission::Tool("other".into())).is_err());
181 }
182
183 #[test]
184 fn test_unknown_user_denied() {
185 let ac = setup_ac();
186 assert!(ac.check("unknown", &Permission::Tool("search".into())).is_err());
187 }
188
189 #[test]
190 fn test_invalid_role_assignment() {
191 let result = AccessControl::builder()
192 .role(Role::new("admin"))
193 .assign("alice", "nonexistent")
194 .build();
195
196 assert!(result.is_err());
197 }
198
199 #[test]
200 fn test_multi_role_user() {
201 let roles = [
202 Role::new("reader").allow(Permission::Tool("read".into())),
203 Role::new("writer").allow(Permission::Tool("write".into())),
204 ];
205
206 let ac = AccessControl::builder()
207 .role(roles[0].clone())
208 .role(roles[1].clone())
209 .assign("bob", "reader")
210 .assign("bob", "writer")
211 .build()
212 .unwrap();
213
214 assert!(ac.check("bob", &Permission::Tool("read".into())).is_ok());
216 assert!(ac.check("bob", &Permission::Tool("write".into())).is_ok());
217 }
218
219 #[test]
220 fn test_multi_role_deny_precedence_is_order_independent() {
221 let editor = Role::new("editor").allow(Permission::AllTools);
222 let restricted = Role::new("restricted").deny(Permission::Tool("code_exec".into()));
223
224 let editor_first = AccessControl::builder()
225 .role(editor.clone())
226 .role(restricted.clone())
227 .assign("bob", "editor")
228 .assign("bob", "restricted")
229 .build()
230 .unwrap();
231
232 let restricted_first = AccessControl::builder()
233 .role(editor)
234 .role(restricted)
235 .assign("bob", "restricted")
236 .assign("bob", "editor")
237 .build()
238 .unwrap();
239
240 assert!(editor_first.check("bob", &Permission::Tool("code_exec".into())).is_err());
241 assert!(restricted_first.check("bob", &Permission::Tool("code_exec".into())).is_err());
242 assert!(editor_first.check("bob", &Permission::Tool("search".into())).is_ok());
243 assert!(restricted_first.check("bob", &Permission::Tool("search".into())).is_ok());
244 }
245}