use crate::audit::{AuditEvent, AuditOutcome, AuditSink};
use crate::error::{AccessDenied, AuthError};
use crate::permission::Permission;
use crate::role::Role;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub struct AccessControl {
roles: HashMap<String, Role>,
user_roles: HashMap<String, Vec<String>>,
audit: Option<Arc<dyn AuditSink>>,
}
impl AccessControl {
pub fn builder() -> AccessControlBuilder {
AccessControlBuilder::default()
}
pub fn check(&self, user: &str, permission: &Permission) -> Result<(), AccessDenied> {
let Some(role_names) = self.user_roles.get(user) else {
return Err(AccessDenied::new(user, permission.to_string()));
};
if self.check_roles(role_names, permission) {
Ok(())
} else {
Err(AccessDenied::new(user, permission.to_string()))
}
}
pub async fn check_and_audit(
&self,
user: &str,
permission: &Permission,
) -> Result<(), AuthError> {
let result = self.check(user, permission);
if let Some(audit) = &self.audit {
let outcome = if result.is_ok() { AuditOutcome::Allowed } else { AuditOutcome::Denied };
let event = match permission {
Permission::Tool(name) => AuditEvent::tool_access(user, name.as_str(), outcome),
Permission::AllTools => AuditEvent::tool_access(user, "*", outcome),
Permission::Agent(name) => AuditEvent::agent_access(user, name.as_str(), outcome),
Permission::AllAgents => AuditEvent::agent_access(user, "*", outcome),
};
audit.log(event).await?;
}
result.map_err(AuthError::from)
}
pub fn user_roles(&self, user: &str) -> Vec<&Role> {
self.user_roles
.get(user)
.map(|names| names.iter().filter_map(|name| self.roles.get(name)).collect())
.unwrap_or_default()
}
pub fn role_names(&self) -> Vec<&str> {
self.roles.keys().map(|s| s.as_str()).collect()
}
pub fn get_role(&self, name: &str) -> Option<&Role> {
self.roles.get(name)
}
pub(crate) fn check_roles(&self, role_names: &[String], permission: &Permission) -> bool {
let roles: Vec<&Role> =
role_names.iter().filter_map(|role_name| self.roles.get(role_name)).collect();
for role in &roles {
if role.denied_permissions().iter().any(|denied| denied.covers(permission)) {
return false;
}
}
roles
.into_iter()
.any(|role| role.allowed_permissions().iter().any(|allowed| allowed.covers(permission)))
}
}
#[derive(Default)]
pub struct AccessControlBuilder {
roles: HashMap<String, Role>,
user_roles: HashMap<String, Vec<String>>,
audit: Option<Arc<dyn AuditSink>>,
}
impl AccessControlBuilder {
pub fn role(mut self, role: Role) -> Self {
self.roles.insert(role.name.clone(), role);
self
}
pub fn assign(mut self, user: impl Into<String>, role: impl Into<String>) -> Self {
self.user_roles.entry(user.into()).or_default().push(role.into());
self
}
pub fn audit_sink(mut self, sink: impl AuditSink + 'static) -> Self {
self.audit = Some(Arc::new(sink));
self
}
pub fn build(self) -> Result<AccessControl, AuthError> {
for (user, roles) in &self.user_roles {
for role in roles {
if !self.roles.contains_key(role) {
return Err(AuthError::RoleNotFound(format!(
"Role '{}' assigned to user '{}' does not exist",
role, user
)));
}
}
}
Ok(AccessControl { roles: self.roles, user_roles: self.user_roles, audit: self.audit })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_ac() -> AccessControl {
let admin = Role::new("admin").allow(Permission::AllTools).allow(Permission::AllAgents);
let user = Role::new("user")
.allow(Permission::Tool("search".into()))
.deny(Permission::Tool("exec".into()));
AccessControl::builder()
.role(admin)
.role(user)
.assign("alice", "admin")
.assign("bob", "user")
.build()
.unwrap()
}
#[test]
fn test_admin_has_full_access() {
let ac = setup_ac();
assert!(ac.check("alice", &Permission::Tool("anything".into())).is_ok());
assert!(ac.check("alice", &Permission::AllTools).is_ok());
assert!(ac.check("alice", &Permission::Agent("any".into())).is_ok());
}
#[test]
fn test_user_limited_access() {
let ac = setup_ac();
assert!(ac.check("bob", &Permission::Tool("search".into())).is_ok());
assert!(ac.check("bob", &Permission::Tool("exec".into())).is_err());
assert!(ac.check("bob", &Permission::Tool("other".into())).is_err());
}
#[test]
fn test_unknown_user_denied() {
let ac = setup_ac();
assert!(ac.check("unknown", &Permission::Tool("search".into())).is_err());
}
#[test]
fn test_invalid_role_assignment() {
let result = AccessControl::builder()
.role(Role::new("admin"))
.assign("alice", "nonexistent")
.build();
assert!(result.is_err());
}
#[test]
fn test_multi_role_user() {
let roles = [
Role::new("reader").allow(Permission::Tool("read".into())),
Role::new("writer").allow(Permission::Tool("write".into())),
];
let ac = AccessControl::builder()
.role(roles[0].clone())
.role(roles[1].clone())
.assign("bob", "reader")
.assign("bob", "writer")
.build()
.unwrap();
assert!(ac.check("bob", &Permission::Tool("read".into())).is_ok());
assert!(ac.check("bob", &Permission::Tool("write".into())).is_ok());
}
#[test]
fn test_multi_role_deny_precedence_is_order_independent() {
let editor = Role::new("editor").allow(Permission::AllTools);
let restricted = Role::new("restricted").deny(Permission::Tool("code_exec".into()));
let editor_first = AccessControl::builder()
.role(editor.clone())
.role(restricted.clone())
.assign("bob", "editor")
.assign("bob", "restricted")
.build()
.unwrap();
let restricted_first = AccessControl::builder()
.role(editor)
.role(restricted)
.assign("bob", "restricted")
.assign("bob", "editor")
.build()
.unwrap();
assert!(editor_first.check("bob", &Permission::Tool("code_exec".into())).is_err());
assert!(restricted_first.check("bob", &Permission::Tool("code_exec".into())).is_err());
assert!(editor_first.check("bob", &Permission::Tool("search".into())).is_ok());
assert!(restricted_first.check("bob", &Permission::Tool("search".into())).is_ok());
}
}