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