Skip to main content

xidl_parser/rest_hir/semantics/security/
mod.rs

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