protocol_droid/domain/a2a/agent_card/
security_scheme.rs1use std::sync::Arc;
36
37use serde::{Deserialize, Serialize};
38use url::Url;
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(tag = "type", rename_all = "camelCase")]
51pub enum SecurityScheme {
52 #[serde(rename = "apiKey")]
56 ApiKey(APIKeySecurityScheme),
57 #[serde(rename = "http")]
61 Http(HTTPAuthSecurityScheme),
62 #[serde(rename = "oauth2")]
69 OAuth2(Box<super::oauth_flow::OAuth2SecurityScheme>),
70 #[serde(rename = "openIdConnect")]
75 OpenIdConnect(OpenIdConnectSecurityScheme),
76 #[serde(rename = "mutualTls")]
82 MutualTls(MutualTlsSecurityScheme),
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct OpenIdConnectSecurityScheme {
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub description: Option<Arc<str>>,
99 pub open_id_connect_url: Url,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct MutualTlsSecurityScheme {
115 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub description: Option<Arc<str>>,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct APIKeySecurityScheme {
130 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub description: Option<Arc<str>>,
134 #[serde(rename = "in")]
138 pub in_: APIKeyLocation,
139 pub name: Arc<str>,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[serde(rename_all = "lowercase")]
152pub enum APIKeyLocation {
153 Query,
156 Header,
159 Cookie,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct HTTPAuthSecurityScheme {
173 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub description: Option<Arc<str>>,
177 pub scheme: Arc<str>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub bearer_format: Option<Arc<str>>,
185}
186
187#[cfg(test)]
188mod tests {
189 use std::sync::Arc;
190
191 use super::{
192 APIKeyLocation, APIKeySecurityScheme, HTTPAuthSecurityScheme, MutualTlsSecurityScheme,
193 OpenIdConnectSecurityScheme, SecurityScheme,
194 };
195
196 type TestError = Box<dyn std::error::Error>;
197
198 #[test]
201 fn api_key_serialises_with_openapi_wire_form() -> Result<(), TestError> {
202 let scheme = SecurityScheme::ApiKey(APIKeySecurityScheme {
203 description: None,
204 in_: APIKeyLocation::Header,
205 name: Arc::from("X-API-Key"),
206 });
207 let value = serde_json::to_value(&scheme)?;
208 let Some(obj) = value.as_object() else {
209 return Err("a SecurityScheme must serialise to a JSON object"
210 .to_owned()
211 .into());
212 };
213 assert_eq!(
214 obj.get("type").and_then(serde_json::Value::as_str),
215 Some("apiKey"),
216 "the type field must be the OpenAPI literal \"apiKey\""
217 );
218 assert_eq!(
219 obj.get("in").and_then(serde_json::Value::as_str),
220 Some("header"),
221 "the in field must be the OpenAPI name for location"
222 );
223 assert_eq!(
224 obj.get("name").and_then(serde_json::Value::as_str),
225 Some("X-API-Key")
226 );
227 assert!(
228 !obj.contains_key("description"),
229 "an absent description must not serialise as null"
230 );
231 Ok(())
232 }
233
234 #[test]
235 fn api_key_round_trips_through_serde() -> Result<(), TestError> {
236 let scheme = SecurityScheme::ApiKey(APIKeySecurityScheme {
237 description: Some(Arc::from("API key for the public read endpoint.")),
238 in_: APIKeyLocation::Query,
239 name: Arc::from("api_key"),
240 });
241 let json = serde_json::to_string(&scheme)?;
242 let parsed: SecurityScheme = serde_json::from_str(&json)?;
243 assert_eq!(parsed, scheme);
244 Ok(())
245 }
246
247 #[test]
248 fn api_key_location_serialises_as_openapi_literal() {
249 let query = serde_json::to_value(APIKeyLocation::Query).ok();
253 let header = serde_json::to_value(APIKeyLocation::Header).ok();
254 let cookie = serde_json::to_value(APIKeyLocation::Cookie).ok();
255 assert_eq!(query.as_ref().and_then(|v| v.as_str()), Some("query"));
256 assert_eq!(header.as_ref().and_then(|v| v.as_str()), Some("header"));
257 assert_eq!(cookie.as_ref().and_then(|v| v.as_str()), Some("cookie"));
258 }
259
260 #[test]
263 fn http_bearer_serialises_with_openapi_wire_form() -> Result<(), TestError> {
264 let scheme = SecurityScheme::Http(HTTPAuthSecurityScheme {
265 description: None,
266 scheme: Arc::from("Bearer"),
267 bearer_format: Some(Arc::from("JWT")),
268 });
269 let value = serde_json::to_value(&scheme)?;
270 let Some(obj) = value.as_object() else {
271 return Err("a SecurityScheme must serialise to a JSON object"
272 .to_owned()
273 .into());
274 };
275 assert_eq!(
276 obj.get("type").and_then(serde_json::Value::as_str),
277 Some("http")
278 );
279 assert_eq!(
280 obj.get("scheme").and_then(serde_json::Value::as_str),
281 Some("Bearer")
282 );
283 assert_eq!(
284 obj.get("bearerFormat").and_then(serde_json::Value::as_str),
285 Some("JWT")
286 );
287 Ok(())
288 }
289
290 #[test]
291 fn http_basic_round_trips_through_serde() -> Result<(), TestError> {
292 let scheme = SecurityScheme::Http(HTTPAuthSecurityScheme {
293 description: Some(Arc::from("HTTP Basic auth.")),
294 scheme: Arc::from("Basic"),
295 bearer_format: None,
296 });
297 let json = serde_json::to_string(&scheme)?;
298 let parsed: SecurityScheme = serde_json::from_str(&json)?;
299 assert_eq!(parsed, scheme);
300 Ok(())
301 }
302
303 #[test]
306 fn open_id_connect_serialises_with_openapi_wire_form() -> Result<(), TestError> {
307 let scheme = SecurityScheme::OpenIdConnect(OpenIdConnectSecurityScheme {
308 description: Some(Arc::from("Login with the corporate OIDC provider.")),
309 open_id_connect_url: url::Url::parse(
310 "https://auth.example.com/.well-known/openid-configuration",
311 )?,
312 });
313 let value = serde_json::to_value(&scheme)?;
314 let Some(obj) = value.as_object() else {
315 return Err("a SecurityScheme must serialise to a JSON object"
316 .to_owned()
317 .into());
318 };
319 assert_eq!(
320 obj.get("type").and_then(serde_json::Value::as_str),
321 Some("openIdConnect"),
322 "the type field must be the OpenAPI literal \"openIdConnect\""
323 );
324 assert_eq!(
325 obj.get("openIdConnectUrl")
326 .and_then(serde_json::Value::as_str),
327 Some("https://auth.example.com/.well-known/openid-configuration")
328 );
329 Ok(())
330 }
331
332 #[test]
333 fn open_id_connect_round_trips_through_serde() -> Result<(), TestError> {
334 let scheme = SecurityScheme::OpenIdConnect(OpenIdConnectSecurityScheme {
335 description: None,
336 open_id_connect_url: url::Url::parse(
337 "https://auth.example.com/.well-known/openid-configuration",
338 )?,
339 });
340 let json = serde_json::to_string(&scheme)?;
341 let parsed: SecurityScheme = serde_json::from_str(&json)?;
342 assert_eq!(parsed, scheme);
343 Ok(())
344 }
345
346 #[test]
349 fn mutual_tls_serialises_with_openapi_wire_form() -> Result<(), TestError> {
350 let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme {
351 description: Some(Arc::from(
352 "Client certs validated against the corporate CA.",
353 )),
354 });
355 let value = serde_json::to_value(&scheme)?;
356 let Some(obj) = value.as_object() else {
357 return Err("a SecurityScheme must serialise to a JSON object"
358 .to_owned()
359 .into());
360 };
361 assert_eq!(
362 obj.get("type").and_then(serde_json::Value::as_str),
363 Some("mutualTls"),
364 "the type field must be the OpenAPI literal \"mutualTls\""
365 );
366 assert_eq!(
367 obj.get("description").and_then(serde_json::Value::as_str),
368 Some("Client certs validated against the corporate CA.")
369 );
370 Ok(())
371 }
372
373 #[test]
374 fn mutual_tls_round_trips_through_serde() -> Result<(), TestError> {
375 let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme { description: None });
376 let json = serde_json::to_string(&scheme)?;
377 let parsed: SecurityScheme = serde_json::from_str(&json)?;
378 assert_eq!(parsed, scheme);
379 Ok(())
380 }
381}