adk_auth/secrets/
authorizing.rs1use std::collections::HashMap;
14use std::sync::Arc;
15
16use adk_core::{AdkError, Result, SecretRequest, SecretService};
17use async_trait::async_trait;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct SecretGrant {
22 names: Vec<String>,
24 prefixes: Vec<String>,
26}
27
28impl SecretGrant {
29 pub fn none() -> Self {
31 Self::default()
32 }
33
34 #[must_use]
36 pub fn name(mut self, name: impl Into<String>) -> Self {
37 self.names.push(name.into());
38 self
39 }
40
41 #[must_use]
46 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
47 self.prefixes.push(prefix.into());
48 self
49 }
50
51 fn allows(&self, name: &str) -> bool {
53 self.names.iter().any(|allowed| allowed == name)
54 || self.prefixes.iter().any(|prefix| name.starts_with(prefix.as_str()))
55 }
56}
57
58pub trait SecretAuditSink: Send + Sync {
63 fn record(&self, decision: SecretAccessDecision<'_>);
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct SecretAccessDecision<'a> {
70 pub allowed: bool,
72 pub name: &'a str,
74 pub tool_name: Option<&'a str>,
76 pub user_id: Option<&'a str>,
78 pub invocation_id: Option<&'a str>,
80 pub reason: &'static str,
82}
83
84pub struct AuthorizingSecretService {
101 inner: Arc<dyn SecretService>,
102 grants: HashMap<String, SecretGrant>,
103 untooled: SecretGrant,
105 audit: Option<Arc<dyn SecretAuditSink>>,
106}
107
108impl AuthorizingSecretService {
109 pub fn new(inner: Arc<dyn SecretService>) -> Self {
111 Self { inner, grants: HashMap::new(), untooled: SecretGrant::none(), audit: None }
112 }
113
114 #[must_use]
116 pub fn grant(mut self, tool_name: impl Into<String>, grant: SecretGrant) -> Self {
117 self.grants.insert(tool_name.into(), grant);
118 self
119 }
120
121 #[must_use]
127 pub fn grant_untooled(mut self, grant: SecretGrant) -> Self {
128 self.untooled = grant;
129 self
130 }
131
132 #[must_use]
134 pub fn with_audit_sink(mut self, sink: Arc<dyn SecretAuditSink>) -> Self {
135 self.audit = Some(sink);
136 self
137 }
138
139 fn decide(&self, request: &SecretRequest) -> (bool, &'static str) {
141 match &request.tool_name {
142 Some(tool_name) => match self.grants.get(tool_name) {
143 Some(grant) if grant.allows(&request.name) => (true, "granted to tool"),
144 Some(_) => (false, "secret not in the tool's grant"),
145 None => (false, "no grant for tool"),
146 },
147 None => {
148 if self.untooled.allows(&request.name) {
149 (true, "granted without tool identity")
150 } else {
151 (false, "no grant for a request without tool identity")
152 }
153 }
154 }
155 }
156
157 fn record(&self, request: &SecretRequest, allowed: bool, reason: &'static str) {
158 let decision = SecretAccessDecision {
159 allowed,
160 name: &request.name,
161 tool_name: request.tool_name.as_deref(),
162 user_id: request.user_id.as_deref(),
163 invocation_id: request.invocation_id.as_deref(),
164 reason,
165 };
166 if allowed {
167 tracing::info!(
168 secret.name = %decision.name,
169 tool.name = decision.tool_name.unwrap_or("<none>"),
170 user.id = decision.user_id.unwrap_or("<unknown>"),
171 invocation.id = decision.invocation_id.unwrap_or("<unknown>"),
172 decision.reason = reason,
173 "secret access allowed"
174 );
175 } else {
176 tracing::warn!(
177 secret.name = %decision.name,
178 tool.name = decision.tool_name.unwrap_or("<none>"),
179 user.id = decision.user_id.unwrap_or("<unknown>"),
180 invocation.id = decision.invocation_id.unwrap_or("<unknown>"),
181 decision.reason = reason,
182 "secret access denied"
183 );
184 }
185 if let Some(sink) = &self.audit {
186 sink.record(decision);
187 }
188 }
189}
190
191impl std::fmt::Debug for AuthorizingSecretService {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("AuthorizingSecretService")
194 .field("granted_tools", &self.grants.keys().collect::<Vec<_>>())
195 .field("audited", &self.audit.is_some())
196 .finish()
197 }
198}
199
200#[async_trait]
201impl SecretService for AuthorizingSecretService {
202 async fn get_secret(&self, name: &str) -> Result<String> {
210 let request = SecretRequest::new(name);
211 self.record(&request, false, "no identity supplied");
212 Err(AdkError::unauthorized(
213 adk_core::ErrorComponent::Tool,
214 "secret.no_identity",
215 format!("secret '{name}' was requested without identity, so it cannot be authorized"),
216 ))
217 }
218
219 async fn get_secret_for(&self, request: &SecretRequest) -> Result<String> {
220 let (allowed, reason) = self.decide(request);
221 self.record(request, allowed, reason);
222 if !allowed {
223 return Err(AdkError::unauthorized(
225 adk_core::ErrorComponent::Tool,
226 "secret.access_denied",
227 format!(
228 "tool {} is not permitted to read secret '{}': {reason}",
229 request.tool_name.as_deref().unwrap_or("<none>"),
230 request.name
231 ),
232 ));
233 }
234 self.inner.get_secret_for(request).await
235 }
236}