use async_trait::async_trait;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Principal {
pub id: String,
pub roles: Vec<String>,
}
impl Principal {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
roles: Vec::new(),
}
}
pub fn with_roles(id: impl Into<String>, roles: Vec<String>) -> Self {
Self {
id: id.into(),
roles,
}
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AuthError {
#[error("authorization denied: {0}")]
Denied(String),
#[error("authorization hook is missing")]
MissingHook,
#[error("authorization hook internal error: {0}")]
Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
}
impl AuthError {
pub fn internal<E>(err: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Internal(Box::new(err))
}
}
#[async_trait]
pub trait AuthHook: Send + Sync {
async fn authorize_agent(
&self,
principal: &Principal,
tool_name: &str,
) -> Result<(), AuthError>;
async fn authorize_operator(
&self,
principal: &Principal,
tool_name: &str,
) -> Result<(), AuthError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DenyAllAuthHook;
#[async_trait]
impl AuthHook for DenyAllAuthHook {
async fn authorize_agent(
&self,
_principal: &Principal,
_tool_name: &str,
) -> Result<(), AuthError> {
Err(AuthError::MissingHook)
}
async fn authorize_operator(
&self,
_principal: &Principal,
_tool_name: &str,
) -> Result<(), AuthError> {
Err(AuthError::MissingHook)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn principal_role_membership() {
let p = Principal::with_roles("user-1", vec!["agent".into(), "oncall".into()]);
assert!(p.has_role("agent"));
assert!(p.has_role("oncall"));
assert!(!p.has_role("operator"));
}
#[tokio::test]
async fn deny_all_hook_returns_missing_for_both_surfaces() {
let hook = DenyAllAuthHook;
let p = Principal::new("anyone");
assert!(matches!(
hook.authorize_agent(&p, "clean").await,
Err(AuthError::MissingHook)
));
assert!(matches!(
hook.authorize_operator(&p, "restore").await,
Err(AuthError::MissingHook)
));
}
}