Skip to main content

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 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    /// Check and log the access attempt.
41    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        // Log to audit sink if configured
49        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    /// Get all roles assigned to a user.
66    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    /// Get all role names.
74    pub fn role_names(&self) -> Vec<&str> {
75        self.roles.keys().map(|s| s.as_str()).collect()
76    }
77
78    /// Get a role by name.
79    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/// Builder for AccessControl.
100#[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    /// Add a role.
109    pub fn role(mut self, role: Role) -> Self {
110        self.roles.insert(role.name.clone(), role);
111        self
112    }
113
114    /// Assign a role to a user.
115    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    /// Set the audit sink.
121    pub fn audit_sink(mut self, sink: impl AuditSink + 'static) -> Self {
122        self.audit = Some(Arc::new(sink));
123        self
124    }
125
126    /// Build the AccessControl.
127    pub fn build(self) -> Result<AccessControl, AuthError> {
128        // Validate all assigned roles exist
129        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        // Can access search
176        assert!(ac.check("bob", &Permission::Tool("search".into())).is_ok());
177        // Cannot access exec (denied)
178        assert!(ac.check("bob", &Permission::Tool("exec".into())).is_err());
179        // Cannot access other tools
180        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        // Bob has both roles, can access both
215        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}