xidl_parser/rest_hir/semantics/security/
mod.rs1use crate::hir::{self, HttpParameterLocation};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeSet;
4
5use super::annotations::{annotation_name, annotation_params, normalize_annotation_params};
6
7#[cfg(test)]
8mod tests;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum HttpSecurityRequirement {
12 HttpBasic,
13 HttpBearer,
14 ApiKey {
15 location: HttpParameterLocation,
16 name: String,
17 },
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum HttpSecurityOrigin {
22 Interface,
23 Method,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct HttpSecurityProfile {
28 pub origin: HttpSecurityOrigin,
29 pub requirements: Vec<HttpSecurityRequirement>,
30}
31
32pub fn effective_security(
33 interface_annotations: &[hir::Annotation],
34 method_annotations: &[hir::Annotation],
35) -> Result<Option<Vec<HttpSecurityRequirement>>, String> {
36 let method_security = collect_security(method_annotations)?;
37 if method_security.explicit_none {
38 return Ok(Some(Vec::new()));
39 }
40 if !method_security.requirements.is_empty() {
41 return Ok(Some(method_security.requirements));
42 }
43 let interface_security = collect_security(interface_annotations)?;
44 if interface_security.explicit_none {
45 return Ok(Some(Vec::new()));
46 }
47 if interface_security.requirements.is_empty() {
48 Ok(None)
49 } else {
50 Ok(Some(interface_security.requirements))
51 }
52}
53
54pub fn effective_security_with_origin(
55 interface_annotations: &[hir::Annotation],
56 method_annotations: &[hir::Annotation],
57) -> Result<Option<HttpSecurityProfile>, String> {
58 let method_security = collect_security(method_annotations)?;
59 if method_security.explicit_none {
60 return Ok(Some(HttpSecurityProfile {
61 origin: HttpSecurityOrigin::Method,
62 requirements: Vec::new(),
63 }));
64 }
65 if !method_security.requirements.is_empty() {
66 return Ok(Some(HttpSecurityProfile {
67 origin: HttpSecurityOrigin::Method,
68 requirements: method_security.requirements,
69 }));
70 }
71 let interface_security = collect_security(interface_annotations)?;
72 if interface_security.explicit_none {
73 return Ok(Some(HttpSecurityProfile {
74 origin: HttpSecurityOrigin::Interface,
75 requirements: Vec::new(),
76 }));
77 }
78 if interface_security.requirements.is_empty() {
79 Ok(None)
80 } else {
81 Ok(Some(HttpSecurityProfile {
82 origin: HttpSecurityOrigin::Interface,
83 requirements: interface_security.requirements,
84 }))
85 }
86}
87
88pub(crate) struct SecurityCollection {
89 pub(crate) explicit_none: bool,
90 pub(crate) requirements: Vec<HttpSecurityRequirement>,
91}
92
93pub(crate) fn collect_security(
94 annotations: &[hir::Annotation],
95) -> Result<SecurityCollection, String> {
96 let mut explicit_none = false;
97 let mut requirements = Vec::new();
98 let mut singleton_names = BTreeSet::new();
99 for annotation in annotations {
100 let Some(name) = annotation_name(annotation) else {
101 continue;
102 };
103 if name.eq_ignore_ascii_case("no_security") {
104 explicit_none = true;
105 continue;
106 }
107 let requirement = if name.eq_ignore_ascii_case("http_basic") {
108 ensure_singleton(&mut singleton_names, "http_basic")?;
109 Some(HttpSecurityRequirement::HttpBasic)
110 } else if name.eq_ignore_ascii_case("http_bearer") {
111 ensure_singleton(&mut singleton_names, "http_bearer")?;
112 Some(HttpSecurityRequirement::HttpBearer)
113 } else if name.eq_ignore_ascii_case("api_key") {
114 Some(parse_api_key(annotation)?)
115 } else {
116 None
117 };
118 if let Some(requirement) = requirement {
119 requirements.push(requirement);
120 }
121 }
122 if explicit_none && !requirements.is_empty() {
123 return Err("@no_security cannot be combined with other security annotations".to_string());
124 }
125 Ok(SecurityCollection {
126 explicit_none,
127 requirements,
128 })
129}
130
131fn ensure_singleton(names: &mut BTreeSet<&'static str>, value: &'static str) -> Result<(), String> {
132 if !names.insert(value) {
133 return Err(format!("duplicate @{value} annotation"));
134 }
135 Ok(())
136}
137
138fn parse_api_key(annotation: &hir::Annotation) -> Result<HttpSecurityRequirement, String> {
139 let params = annotation_params(annotation)
140 .ok_or_else(|| "@api_key requires in=... and name=...".to_string())?;
141 let params = normalize_annotation_params(params);
142 let location = params
143 .get("in")
144 .ok_or_else(|| "@api_key requires non-empty in=...".to_string())?;
145 let location = HttpParameterLocation::parse(location).ok_or_else(|| {
146 format!(
147 "@api_key(in=...) must be one of header|query|cookie, got '{}'",
148 location.to_ascii_lowercase()
149 )
150 })?;
151 let name = params
152 .get("name")
153 .cloned()
154 .filter(|value| !value.is_empty())
155 .ok_or_else(|| "@api_key requires non-empty name=...".to_string())?;
156 Ok(HttpSecurityRequirement::ApiKey { location, name })
157}