1use std::sync::Arc;
4
5use tracing::{debug, info};
6use typesec_core::{
7 Capability, Permission, Resource,
8 policy::{CapabilityError, MintOptions, PolicyEngine, mint_capability_for_id},
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_with(
40 self,
41 credentials: Credentials,
42 authenticator: &dyn typesec_core::typestate::Authenticator,
43 ) -> Result<SecureAgent<Authenticated>, AgentError> {
44 let inner = self.inner.authenticate_with(credentials, authenticator)?;
45 Ok(SecureAgent { inner })
46 }
47
48 pub fn authenticate_unverified(
52 self,
53 credentials: Credentials,
54 ) -> Result<SecureAgent<Authenticated>, AgentError> {
55 let inner = self.inner.authenticate_unverified(credentials)?;
56 Ok(SecureAgent { inner })
57 }
58}
59
60impl SecureAgent<Authenticated> {
61 pub fn subject(&self) -> &str {
63 self.inner.subject()
64 }
65
66 pub fn engine(&self) -> Arc<dyn PolicyEngine> {
70 self.inner.engine().clone()
71 }
72
73 pub async fn request_capability<P: Permission, R: Resource>(
85 &self,
86 resource: &R,
87 ) -> Result<Capability<P, R>, CapabilityError> {
88 self.request_capability_with(resource, MintOptions::default())
89 .await
90 }
91
92 pub async fn request_capability_with<P: Permission, R: Resource>(
97 &self,
98 resource: &R,
99 options: MintOptions,
100 ) -> Result<Capability<P, R>, CapabilityError> {
101 let subject = self.subject().to_owned();
102 let action = P::name();
103 let resource_id = resource.resource_id().to_owned();
104 let engine = self.inner.engine().clone();
105
106 debug!(%subject, action, %resource_id, "requesting capability");
107
108 let cap = {
109 let subject = subject.clone();
110 let resource_id = resource_id.clone();
111 tokio::task::spawn_blocking(move || {
112 mint_capability_for_id::<P, R>(engine.as_ref(), &subject, &resource_id, &options)
113 })
114 .await
115 .map_err(|join_err| {
116 CapabilityError::EngineError(format!("policy check task failed: {join_err}"))
117 })??
118 };
119
120 info!(%subject, action, %resource_id, "capability granted");
121
122 Ok(cap)
123 }
124
125 pub async fn execute<P, R, F, Fut>(
147 &self,
148 cap: &Capability<P, R>,
149 resource: &R,
150 action: F,
151 ) -> Result<(), crate::executor::TaskError>
152 where
153 P: Permission,
154 R: Resource,
155 F: FnOnce(&R) -> Fut,
156 Fut: std::future::Future<Output = Result<(), crate::executor::TaskError>>,
157 {
158 if cap.subject() != self.subject() {
159 return Err(crate::executor::TaskError::CapabilityMismatch(format!(
160 "capability was minted for subject '{}', not '{}'",
161 cap.subject(),
162 self.subject()
163 )));
164 }
165 if cap.resource_id() != resource.resource_id() {
166 return Err(crate::executor::TaskError::CapabilityMismatch(format!(
167 "capability covers resource '{}', not '{}'",
168 cap.resource_id(),
169 resource.resource_id()
170 )));
171 }
172 cap.ensure_active()?;
173
174 info!(
175 subject = %self.subject(),
176 permission = %Capability::<P, R>::permission_name(),
177 resource = %cap.resource_id(),
178 "executing with capability"
179 );
180
181 action(resource).await
182 }
183}
184
185impl<S: AgentState> std::fmt::Debug for SecureAgent<S> {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 write!(f, "SecureAgent({:?})", self.inner)
188 }
189}
190
191pub struct AgentBuilder {
193 engine: Option<Arc<dyn PolicyEngine>>,
194}
195
196impl AgentBuilder {
197 pub fn new() -> Self {
199 Self { engine: None }
200 }
201
202 pub fn with_engine(mut self, engine: Arc<dyn PolicyEngine>) -> Self {
204 self.engine = Some(engine);
205 self
206 }
207
208 pub fn with_composed_engine(
217 mut self,
218 primary: Arc<dyn PolicyEngine>,
219 fallback: Arc<dyn PolicyEngine>,
220 ) -> Self {
221 use typesec_core::combinator::{CombineStrategy, PolicyEngineBuilder};
222 let engine = PolicyEngineBuilder::new()
223 .add_engine(primary)
224 .add_engine(fallback)
225 .strategy(CombineStrategy::PriorityOrder)
226 .build();
227 self.engine = Some(Arc::new(engine));
228 self
229 }
230
231 pub fn build(self) -> Result<SecureAgent<Unauthenticated>, String> {
233 let engine = self.engine.ok_or("no policy engine configured")?;
234 Ok(SecureAgent::new(engine))
235 }
236}
237
238impl Default for AgentBuilder {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use std::sync::Arc;
248 use typesec_core::{permissions::CanRead, policy::PolicyResult, resource::GenericResource};
249
250 struct AllowAll;
251 impl PolicyEngine for AllowAll {
252 fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
253 PolicyResult::Allow
254 }
255 }
256
257 struct DenyAll;
258 impl PolicyEngine for DenyAll {
259 fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
260 PolicyResult::Deny("DenyAll".into())
261 }
262 }
263
264 #[tokio::test]
265 async fn full_flow_allow() {
266 let agent = SecureAgent::new(Arc::new(AllowAll));
267 let agent = agent
268 .authenticate_unverified(Credentials::new("agent:test", "tok"))
269 .expect("auth ok");
270 let resource = GenericResource::new("reports/q1", "report");
271 let cap: Capability<CanRead, GenericResource> = agent
272 .request_capability(&resource)
273 .await
274 .expect("should get cap");
275 assert_eq!(cap.subject(), "agent:test");
276 }
277
278 #[tokio::test]
279 async fn denied_request_returns_error() {
280 let agent = SecureAgent::new(Arc::new(DenyAll));
281 let agent = agent
282 .authenticate_unverified(Credentials::new("agent:test", "tok"))
283 .expect("auth ok");
284 let resource = GenericResource::new("reports/q1", "report");
285 let result: Result<Capability<CanRead, GenericResource>, _> =
286 agent.request_capability(&resource).await;
287 assert!(result.is_err());
288 }
289
290 #[tokio::test]
291 async fn execute_requires_capability() {
292 let agent = SecureAgent::new(Arc::new(AllowAll));
293 let agent = agent
294 .authenticate_unverified(Credentials::new("agent:test", "tok"))
295 .expect("auth ok");
296 let resource = GenericResource::new("reports/q1", "report");
297 let cap: Capability<CanRead, GenericResource> =
298 agent.request_capability(&resource).await.expect("cap ok");
299
300 let executed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
301 let executed_clone = executed.clone();
302
303 agent
304 .execute(&cap, &resource, |_r| {
305 let flag = executed_clone.clone();
306 Box::pin(async move {
307 flag.store(true, std::sync::atomic::Ordering::SeqCst);
308 Ok(())
309 })
310 })
311 .await
312 .expect("execute ok");
313
314 assert!(executed.load(std::sync::atomic::Ordering::SeqCst));
315 }
316
317 #[tokio::test]
318 async fn execute_rejects_capability_for_other_resource() {
319 let agent = SecureAgent::new(Arc::new(AllowAll));
320 let agent = agent
321 .authenticate_unverified(Credentials::new("agent:test", "tok"))
322 .expect("auth ok");
323 let q1 = GenericResource::new("reports/q1", "report");
324 let q2 = GenericResource::new("reports/q2", "report");
325 let cap: Capability<CanRead, GenericResource> =
326 agent.request_capability(&q1).await.expect("cap ok");
327
328 let result = agent
330 .execute(&cap, &q2, |_r| Box::pin(async { Ok(()) }))
331 .await;
332 assert!(matches!(
333 result,
334 Err(crate::executor::TaskError::CapabilityMismatch(_))
335 ));
336 }
337
338 #[tokio::test]
339 async fn execute_rejects_capability_for_other_subject() {
340 let resource = GenericResource::new("reports/q1", "report");
341
342 let other = SecureAgent::new(Arc::new(AllowAll))
344 .authenticate_unverified(Credentials::new("agent:other", "tok"))
345 .expect("auth ok");
346 let cap: Capability<CanRead, GenericResource> =
347 other.request_capability(&resource).await.expect("cap ok");
348
349 let agent = SecureAgent::new(Arc::new(AllowAll))
351 .authenticate_unverified(Credentials::new("agent:test", "tok"))
352 .expect("auth ok");
353 let result = agent
354 .execute(&cap, &resource, |_r| Box::pin(async { Ok(()) }))
355 .await;
356 assert!(matches!(
357 result,
358 Err(crate::executor::TaskError::CapabilityMismatch(_))
359 ));
360 }
361}