Skip to main content

xidl_parser/rest_hir/semantics/security/
mod.rs

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