adk_auth/
access_control.rs

1//! Access control with role-based permissions.
2
3use 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/// Access control for checking permissions.
11#[derive(Clone)]
12pub struct AccessControl {
13    /// Roles by name.
14    roles: HashMap<String, Role>,
15    /// User to role assignments.
16    user_roles: HashMap<String, Vec<String>>,
17    /// Optional audit sink.
18    audit: Option<Arc<dyn AuditSink>>,
19}
20
21impl AccessControl {
22    /// Create a new builder.
23    pub fn builder() -> AccessControlBuilder {
24        AccessControlBuilder::default()
25    }
26
27    /// Check if a user has access to a permission.
28    pub fn check(&self, user: &str, permission: &Permission) -> Result<(), AccessDenied> {
29        let role_names = self.user_roles.get(user);
30
31        if let Some(role_names) = role_names {
32            for role_name in role_names {
33                if let Some(role) = self.roles.get(role_name) {
34                    if role.can_access(permission) {
35                        return Ok(());
36                    }
37                }
38            }
39        }
40
41        Err(AccessDenied::new(user, permission.to_string()))
42    }
43
44    /// Check and log the access attempt.
45    pub async fn check_and_audit(
46        &self,
47        user: &str,
48        permission: &Permission,
49    ) -> Result<(), AuthError> {
50        let result = self.check(user, permission);
51
52        // Log to audit sink if configured
53        if let Some(audit) = &self.audit {
54            let outcome = if result.is_ok() { AuditOutcome::Allowed } else { AuditOutcome::Denied };
55
56            let event = match permission {
57                Permission::Tool(name) => AuditEvent::tool_access(user, name.as_str(), outcome),
58                Permission::AllTools => AuditEvent::tool_access(user, "*", outcome),
59                Permission::Agent(name) => AuditEvent::agent_access(user, name.as_str(), outcome),
60                Permission::AllAgents => AuditEvent::agent_access(user, "*", outcome),
61            };
62
63            audit.log(event).await?;
64        }
65
66        result.map_err(AuthError::from)
67    }
68
69    /// Get all roles assigned to a user.
70    pub fn user_roles(&self, user: &str) -> Vec<&Role> {
71        self.user_roles
72            .get(user)
73            .map(|names| names.iter().filter_map(|name| self.roles.get(name)).collect())
74            .unwrap_or_default()
75    }
76
77    /// Get all role names.
78    pub fn role_names(&self) -> Vec<&str> {
79        self.roles.keys().map(|s| s.as_str()).collect()
80    }
81
82    /// Get a role by name.
83    pub fn get_role(&self, name: &str) -> Option<&Role> {
84        self.roles.get(name)
85    }
86}
87
88/// Builder for AccessControl.
89#[derive(Default)]
90pub struct AccessControlBuilder {
91    roles: HashMap<String, Role>,
92    user_roles: HashMap<String, Vec<String>>,
93    audit: Option<Arc<dyn AuditSink>>,
94}
95
96impl AccessControlBuilder {
97    /// Add a role.
98    pub fn role(mut self, role: Role) -> Self {
99        self.roles.insert(role.name.clone(), role);
100        self
101    }
102
103    /// Assign a role to a user.
104    pub fn assign(mut self, user: impl Into<String>, role: impl Into<String>) -> Self {
105        self.user_roles.entry(user.into()).or_default().push(role.into());
106        self
107    }
108
109    /// Set the audit sink.
110    pub fn audit_sink(mut self, sink: impl AuditSink + 'static) -> Self {
111        self.audit = Some(Arc::new(sink));
112        self
113    }
114
115    /// Build the AccessControl.
116    pub fn build(self) -> Result<AccessControl, AuthError> {
117        // Validate all assigned roles exist
118        for (user, roles) in &self.user_roles {
119            for role in roles {
120                if !self.roles.contains_key(role) {
121                    return Err(AuthError::RoleNotFound(format!(
122                        "Role '{}' assigned to user '{}' does not exist",
123                        role, user
124                    )));
125                }
126            }
127        }
128
129        Ok(AccessControl { roles: self.roles, user_roles: self.user_roles, audit: self.audit })
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    fn setup_ac() -> AccessControl {
138        let admin = Role::new("admin").allow(Permission::AllTools).allow(Permission::AllAgents);
139
140        let user = Role::new("user")
141            .allow(Permission::Tool("search".into()))
142            .deny(Permission::Tool("exec".into()));
143
144        AccessControl::builder()
145            .role(admin)
146            .role(user)
147            .assign("alice", "admin")
148            .assign("bob", "user")
149            .build()
150            .unwrap()
151    }
152
153    #[test]
154    fn test_admin_has_full_access() {
155        let ac = setup_ac();
156        assert!(ac.check("alice", &Permission::Tool("anything".into())).is_ok());
157        assert!(ac.check("alice", &Permission::AllTools).is_ok());
158        assert!(ac.check("alice", &Permission::Agent("any".into())).is_ok());
159    }
160
161    #[test]
162    fn test_user_limited_access() {
163        let ac = setup_ac();
164        // Can access search
165        assert!(ac.check("bob", &Permission::Tool("search".into())).is_ok());
166        // Cannot access exec (denied)
167        assert!(ac.check("bob", &Permission::Tool("exec".into())).is_err());
168        // Cannot access other tools
169        assert!(ac.check("bob", &Permission::Tool("other".into())).is_err());
170    }
171
172    #[test]
173    fn test_unknown_user_denied() {
174        let ac = setup_ac();
175        assert!(ac.check("unknown", &Permission::Tool("search".into())).is_err());
176    }
177
178    #[test]
179    fn test_invalid_role_assignment() {
180        let result = AccessControl::builder()
181            .role(Role::new("admin"))
182            .assign("alice", "nonexistent")
183            .build();
184
185        assert!(result.is_err());
186    }
187
188    #[test]
189    fn test_multi_role_user() {
190        let roles = [
191            Role::new("reader").allow(Permission::Tool("read".into())),
192            Role::new("writer").allow(Permission::Tool("write".into())),
193        ];
194
195        let ac = AccessControl::builder()
196            .role(roles[0].clone())
197            .role(roles[1].clone())
198            .assign("bob", "reader")
199            .assign("bob", "writer")
200            .build()
201            .unwrap();
202
203        // Bob has both roles, can access both
204        assert!(ac.check("bob", &Permission::Tool("read".into())).is_ok());
205        assert!(ac.check("bob", &Permission::Tool("write".into())).is_ok());
206    }
207}