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