use std::sync::Arc;
use tracing::{debug, info};
use typesec_core::{
Capability, Permission, Resource,
policy::{CapabilityError, PolicyEngine, mint_capability},
typestate::{Agent, AgentError, AgentState, Authenticated, Credentials, Unauthenticated},
};
pub struct SecureAgent<S: AgentState> {
inner: Agent<S>,
}
impl SecureAgent<Unauthenticated> {
pub fn new(engine: Arc<dyn PolicyEngine>) -> Self {
Self {
inner: Agent::new(engine),
}
}
pub fn authenticate(
self,
credentials: Credentials,
) -> Result<SecureAgent<Authenticated>, AgentError> {
let inner = self.inner.authenticate(credentials)?;
Ok(SecureAgent { inner })
}
}
impl SecureAgent<Authenticated> {
pub fn subject(&self) -> &str {
self.inner.subject()
}
pub fn engine(&self) -> Arc<dyn PolicyEngine> {
self.inner.engine().clone()
}
pub async fn request_capability<P: Permission, R: Resource>(
&self,
resource: &R,
) -> Result<Capability<P, R>, CapabilityError> {
let subject = self.subject();
let action = P::name();
let resource_id = resource.resource_id();
debug!(subject, action, resource_id, "requesting capability");
let cap = mint_capability::<P, R>(self.inner.engine().as_ref(), subject, resource)?;
info!(subject, action, resource_id, "capability granted");
Ok(cap)
}
pub async fn execute<P, R, F, Fut>(
&self,
cap: &Capability<P, R>,
resource: &R,
action: F,
) -> Result<(), crate::executor::TaskError>
where
P: Permission,
R: Resource,
F: FnOnce(&R) -> Fut,
Fut: std::future::Future<Output = Result<(), crate::executor::TaskError>>,
{
info!(
subject = %self.subject(),
permission = %Capability::<P, R>::permission_name(),
resource = %cap.resource_id(),
"executing with capability"
);
action(resource).await
}
}
impl<S: AgentState> std::fmt::Debug for SecureAgent<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SecureAgent({:?})", self.inner)
}
}
pub struct AgentBuilder {
engine: Option<Arc<dyn PolicyEngine>>,
}
impl AgentBuilder {
pub fn new() -> Self {
Self { engine: None }
}
pub fn with_engine(mut self, engine: Arc<dyn PolicyEngine>) -> Self {
self.engine = Some(engine);
self
}
pub fn with_composed_engine(
mut self,
primary: Arc<dyn PolicyEngine>,
fallback: Arc<dyn PolicyEngine>,
) -> Self {
use typesec_core::combinator::{CombineStrategy, PolicyEngineBuilder};
let engine = PolicyEngineBuilder::new()
.add_engine(primary)
.add_engine(fallback)
.strategy(CombineStrategy::PriorityOrder)
.build();
self.engine = Some(Arc::new(engine));
self
}
pub fn build(self) -> Result<SecureAgent<Unauthenticated>, String> {
let engine = self.engine.ok_or("no policy engine configured")?;
Ok(SecureAgent::new(engine))
}
}
impl Default for AgentBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use typesec_core::{permissions::CanRead, policy::PolicyResult, resource::GenericResource};
struct AllowAll;
impl PolicyEngine for AllowAll {
fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
PolicyResult::Allow
}
}
struct DenyAll;
impl PolicyEngine for DenyAll {
fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
PolicyResult::Deny("DenyAll".into())
}
}
#[tokio::test]
async fn full_flow_allow() {
let agent = SecureAgent::new(Arc::new(AllowAll));
let agent = agent
.authenticate(Credentials::new("agent:test", "tok"))
.expect("auth ok");
let resource = GenericResource::new("reports/q1", "report");
let cap: Capability<CanRead, GenericResource> = agent
.request_capability(&resource)
.await
.expect("should get cap");
assert_eq!(cap.subject(), "agent:test");
}
#[tokio::test]
async fn denied_request_returns_error() {
let agent = SecureAgent::new(Arc::new(DenyAll));
let agent = agent
.authenticate(Credentials::new("agent:test", "tok"))
.expect("auth ok");
let resource = GenericResource::new("reports/q1", "report");
let result: Result<Capability<CanRead, GenericResource>, _> =
agent.request_capability(&resource).await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_requires_capability() {
let agent = SecureAgent::new(Arc::new(AllowAll));
let agent = agent
.authenticate(Credentials::new("agent:test", "tok"))
.expect("auth ok");
let resource = GenericResource::new("reports/q1", "report");
let cap: Capability<CanRead, GenericResource> =
agent.request_capability(&resource).await.expect("cap ok");
let executed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let executed_clone = executed.clone();
agent
.execute(&cap, &resource, |_r| {
let flag = executed_clone.clone();
Box::pin(async move {
flag.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
})
})
.await
.expect("execute ok");
assert!(executed.load(std::sync::atomic::Ordering::SeqCst));
}
}