use std::sync::Arc;
use tracing::{debug, info};
use typesec_core::{
Capability, Permission, Resource,
policy::{CapabilityError, MintOptions, PolicyEngine, mint_capability_for_id_async},
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_with(
self,
credentials: Credentials,
authenticator: &dyn typesec_core::typestate::Authenticator,
) -> Result<SecureAgent<Authenticated>, AgentError> {
let inner = self.inner.authenticate_with(credentials, authenticator)?;
Ok(SecureAgent { inner })
}
pub fn authenticate_unverified(
self,
credentials: Credentials,
) -> Result<SecureAgent<Authenticated>, AgentError> {
let inner = self.inner.authenticate_unverified(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> {
self.request_capability_with(resource, MintOptions::default())
.await
}
pub async fn request_capability_with<P: Permission, R: Resource>(
&self,
resource: &R,
options: MintOptions,
) -> Result<Capability<P, R>, CapabilityError> {
let subject = self.subject().to_owned();
let action = P::name();
let resource_id = resource.resource_id().to_owned();
let engine = self.inner.engine().clone();
debug!(%subject, action, %resource_id, "requesting capability");
let cap = mint_capability_for_id_async::<P, R>(
engine.as_ref(),
subject.as_str(),
resource_id.as_str(),
&options,
)
.await?;
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>>,
{
if cap.subject() != self.subject() {
return Err(crate::executor::TaskError::CapabilityMismatch(format!(
"capability was minted for subject '{}', not '{}'",
cap.subject(),
self.subject()
)));
}
if cap.resource_id() != resource.resource_id() {
return Err(crate::executor::TaskError::CapabilityMismatch(format!(
"capability covers resource '{}', not '{}'",
cap.resource_id(),
resource.resource_id()
)));
}
cap.ensure_active()?;
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(
self,
primary: Arc<dyn PolicyEngine>,
fallback: Arc<dyn PolicyEngine>,
) -> Self {
self.with_composed_engine_strategy(
primary,
fallback,
typesec_core::combinator::CombineStrategy::PriorityOrder,
)
}
pub fn with_composed_engine_strategy(
mut self,
primary: Arc<dyn PolicyEngine>,
fallback: Arc<dyn PolicyEngine>,
strategy: typesec_core::combinator::CombineStrategy,
) -> Self {
use typesec_core::combinator::PolicyEngineBuilder;
let engine = PolicyEngineBuilder::new()
.add_engine(primary)
.add_engine(fallback)
.strategy(strategy)
.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;