1use std::sync::Arc;
4
5use tracing::{debug, info};
6use typesec_core::{
7 Capability, Permission, Resource,
8 policy::{CapabilityError, PolicyEngine, mint_capability},
9 typestate::{Agent, AgentError, AgentState, Authenticated, Credentials, Unauthenticated},
10};
11
12pub struct SecureAgent<S: AgentState> {
22 inner: Agent<S>,
23}
24
25impl SecureAgent<Unauthenticated> {
26 pub fn new(engine: Arc<dyn PolicyEngine>) -> Self {
28 Self {
29 inner: Agent::new(engine),
30 }
31 }
32
33 pub fn authenticate(
39 self,
40 credentials: Credentials,
41 ) -> Result<SecureAgent<Authenticated>, AgentError> {
42 let inner = self.inner.authenticate(credentials)?;
43 Ok(SecureAgent { inner })
44 }
45}
46
47impl SecureAgent<Authenticated> {
48 pub fn subject(&self) -> &str {
50 self.inner.subject()
51 }
52
53 pub fn engine(&self) -> Arc<dyn PolicyEngine> {
57 self.inner.engine().clone()
58 }
59
60 pub async fn request_capability<P: Permission, R: Resource>(
69 &self,
70 resource: &R,
71 ) -> Result<Capability<P, R>, CapabilityError> {
72 let subject = self.subject();
73 let action = P::name();
74 let resource_id = resource.resource_id();
75
76 debug!(subject, action, resource_id, "requesting capability");
77
78 let cap = mint_capability::<P, R>(self.inner.engine().as_ref(), subject, resource)?;
79
80 info!(subject, action, resource_id, "capability granted");
81
82 Ok(cap)
83 }
84
85 pub async fn execute<P, R, F, Fut>(
101 &self,
102 cap: &Capability<P, R>,
103 resource: &R,
104 action: F,
105 ) -> Result<(), crate::executor::TaskError>
106 where
107 P: Permission,
108 R: Resource,
109 F: FnOnce(&R) -> Fut,
110 Fut: std::future::Future<Output = Result<(), crate::executor::TaskError>>,
111 {
112 info!(
113 subject = %self.subject(),
114 permission = %Capability::<P, R>::permission_name(),
115 resource = %cap.resource_id(),
116 "executing with capability"
117 );
118
119 action(resource).await
120 }
121}
122
123impl<S: AgentState> std::fmt::Debug for SecureAgent<S> {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(f, "SecureAgent({:?})", self.inner)
126 }
127}
128
129pub struct AgentBuilder {
131 engine: Option<Arc<dyn PolicyEngine>>,
132}
133
134impl AgentBuilder {
135 pub fn new() -> Self {
137 Self { engine: None }
138 }
139
140 pub fn with_engine(mut self, engine: Arc<dyn PolicyEngine>) -> Self {
142 self.engine = Some(engine);
143 self
144 }
145
146 pub fn with_composed_engine(
155 mut self,
156 primary: Arc<dyn PolicyEngine>,
157 fallback: Arc<dyn PolicyEngine>,
158 ) -> Self {
159 use typesec_core::combinator::{CombineStrategy, PolicyEngineBuilder};
160 let engine = PolicyEngineBuilder::new()
161 .add_engine(primary)
162 .add_engine(fallback)
163 .strategy(CombineStrategy::PriorityOrder)
164 .build();
165 self.engine = Some(Arc::new(engine));
166 self
167 }
168
169 pub fn build(self) -> Result<SecureAgent<Unauthenticated>, String> {
171 let engine = self.engine.ok_or("no policy engine configured")?;
172 Ok(SecureAgent::new(engine))
173 }
174}
175
176impl Default for AgentBuilder {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::sync::Arc;
186 use typesec_core::{permissions::CanRead, policy::PolicyResult, resource::GenericResource};
187
188 struct AllowAll;
189 impl PolicyEngine for AllowAll {
190 fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
191 PolicyResult::Allow
192 }
193 }
194
195 struct DenyAll;
196 impl PolicyEngine for DenyAll {
197 fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
198 PolicyResult::Deny("DenyAll".into())
199 }
200 }
201
202 #[tokio::test]
203 async fn full_flow_allow() {
204 let agent = SecureAgent::new(Arc::new(AllowAll));
205 let agent = agent
206 .authenticate(Credentials::new("agent:test", "tok"))
207 .expect("auth ok");
208 let resource = GenericResource::new("reports/q1", "report");
209 let cap: Capability<CanRead, GenericResource> = agent
210 .request_capability(&resource)
211 .await
212 .expect("should get cap");
213 assert_eq!(cap.subject(), "agent:test");
214 }
215
216 #[tokio::test]
217 async fn denied_request_returns_error() {
218 let agent = SecureAgent::new(Arc::new(DenyAll));
219 let agent = agent
220 .authenticate(Credentials::new("agent:test", "tok"))
221 .expect("auth ok");
222 let resource = GenericResource::new("reports/q1", "report");
223 let result: Result<Capability<CanRead, GenericResource>, _> =
224 agent.request_capability(&resource).await;
225 assert!(result.is_err());
226 }
227
228 #[tokio::test]
229 async fn execute_requires_capability() {
230 let agent = SecureAgent::new(Arc::new(AllowAll));
231 let agent = agent
232 .authenticate(Credentials::new("agent:test", "tok"))
233 .expect("auth ok");
234 let resource = GenericResource::new("reports/q1", "report");
235 let cap: Capability<CanRead, GenericResource> =
236 agent.request_capability(&resource).await.expect("cap ok");
237
238 let executed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
239 let executed_clone = executed.clone();
240
241 agent
242 .execute(&cap, &resource, |_r| {
243 let flag = executed_clone.clone();
244 Box::pin(async move {
245 flag.store(true, std::sync::atomic::Ordering::SeqCst);
246 Ok(())
247 })
248 })
249 .await
250 .expect("execute ok");
251
252 assert!(executed.load(std::sync::atomic::Ordering::SeqCst));
253 }
254}