agentguard/commands/
authorize.rs1use agentguard_core::{
2 authorize::build_entities, decode_chain_secret, AgentRequest, Authorizer, DecisionLog,
3 PolicyStore,
4};
5use anyhow::{anyhow, Result};
6use std::path::Path;
7use tokio::io::AsyncReadExt;
8
9#[derive(Debug)]
11pub struct AuthorizeOutcome {
12 #[allow(dead_code)]
14 pub effect: agentguard_core::authorize::Effect,
15 pub decision_was_allow: bool,
17}
18
19#[allow(clippy::too_many_arguments)]
20pub async fn run(
21 store: impl AsRef<Path>,
22 audit: impl AsRef<Path>,
23 request: impl AsRef<Path>,
24 entities_path: Option<impl AsRef<Path>>,
25 no_audit: bool,
26 output: &str,
27 secret_file: Option<&Path>,
28) -> Result<AuthorizeOutcome> {
29 let mut req: AgentRequest = if request.as_ref().as_os_str() == "-" {
30 let mut buf = String::new();
31 tokio::io::stdin().read_to_string(&mut buf).await?;
32 serde_json::from_str(&buf)?
33 } else {
34 let text = tokio::fs::read_to_string(request.as_ref()).await?;
35 serde_json::from_str(&text)?
36 };
37
38 if let Ok(tp) = std::env::var("AGENTGUARD_TRACEPARENT") {
44 match tp.parse::<agentguard_core::observability::TraceContext>() {
45 Ok(ctx) => req.trace = Some(ctx),
46 Err(e) => tracing::warn!(
47 tp = %tp,
48 error = %e,
49 "AGENTGUARD_TRACEPARENT is not a valid trace context; ignoring"
50 ),
51 }
52 }
53
54 if std::env::var_os("AGENTGUARD_BEARER").is_some() {
59 tracing::debug!(
60 "AGENTGUARD_BEARER is set; the CLI evaluates in-process and does \
61 not currently forward this token. Use it when running an \
62 agentguard-server deployment instead."
63 );
64 }
65
66 let entities = load_entities(entities_path.as_ref().map(|p| p.as_ref())).await?;
67
68 let store_path = store.as_ref().to_path_buf();
72 let audit_path = audit.as_ref().to_path_buf();
73 let secret_path_buf = secret_file.map(|p| p.to_path_buf());
74 let req_for_blocking = req.clone();
75 let entities_for_blocking = entities.clone();
76 let decision =
77 tokio::task::spawn_blocking(move || -> Result<agentguard_core::authorize::Decision> {
78 let store = PolicyStore::open(&store_path)?;
79 let engine = Authorizer::new(store)?;
80 let decision = engine.authorize(&req_for_blocking, &entities_for_blocking)?;
81 Ok(decision)
82 })
83 .await
84 .map_err(|e| anyhow!("blocking task: {e}"))??;
85
86 if !no_audit {
87 let audit_path_for_blocking = audit_path.clone();
92 let secret_path_for_blocking = secret_path_buf.clone();
93 let decision_for_blocking = decision.clone();
94 tokio::task::spawn_blocking(move || -> Result<()> {
95 let log = match &secret_path_for_blocking {
96 Some(path) => {
97 let key = std::fs::read(path)
98 .map_err(|e| anyhow!("read chain secret {:?}: {}", path, e))?;
99 let key = decode_chain_secret(&key)
100 .ok_or_else(|| anyhow!("chain secret file {:?} is empty", path))?;
101 DecisionLog::open_with_chain(&audit_path_for_blocking, &key)?
102 }
103 None => DecisionLog::open(&audit_path_for_blocking)?,
104 };
105 log.append_decision(&decision_for_blocking)?;
106 Ok(())
107 })
108 .await
109 .map_err(|e| anyhow!("blocking task: {e}"))??;
110 }
111
112 if output == "json" {
117 println!("{}", serde_json::to_string_pretty(&decision)?);
118 } else {
119 let color = match decision.effect {
120 agentguard_core::authorize::Effect::Allow => "\x1b[32m",
121 _ => "\x1b[31m",
122 };
123 println!(
124 "{} {}\x1b[0m",
125 color,
126 format!("{:?}", decision.effect).to_uppercase()
127 );
128 println!("principal: {}", req.principal);
129 println!("action: {}", req.action);
130 println!("resource: {}", req.resource);
131 if !decision.policies.is_empty() {
132 println!("policies: {}", decision.policies.join(", "));
133 }
134 for r in &decision.reasons {
135 println!(" - {}", r);
136 }
137 }
138
139 let outcome = AuthorizeOutcome {
140 effect: decision.effect,
141 decision_was_allow: matches!(decision.effect, agentguard_core::authorize::Effect::Allow),
142 };
143 Ok(outcome)
144}
145
146async fn load_entities(path: Option<&Path>) -> Result<cedar_policy::Entities> {
147 let path = path.unwrap_or_else(|| Path::new(".agentguard/entities.json"));
148 if !path.exists() {
149 return Ok(cedar_policy::Entities::empty());
150 }
151 let path = path.to_path_buf();
152 tokio::task::spawn_blocking(move || -> Result<cedar_policy::Entities> {
153 let text = std::fs::read_to_string(&path)?;
154 let arr: Vec<serde_json::Value> = serde_json::from_str(&text).map_err(|e| {
155 anyhow!(
156 "entities file must be a JSON array of entity objects: {}",
157 e
158 )
159 })?;
160 build_entities(arr).map_err(Into::into)
161 })
162 .await
163 .map_err(|e| anyhow!("blocking task: {e}"))?
164}