Skip to main content

xidl_parser/rest_hir/semantics/
security.rs

1use 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}