Skip to main content

oapi_codegen/lower/
security.rs

1//! Lowering OpenAPI security schemes and requirements into the IR.
2//!
3//! Only the shared lowering pass runs here: it catalogues the document's
4//! `components.securitySchemes` and resolves each operation's *effective*
5//! security requirement. Both emitters read the result. The client turns
6//! supported schemes into credential fields and rejects operations that require
7//! an [`crate::ir::SecuritySchemeKind::Unsupported`] scheme. The server names
8//! the requirement in each trait method's doc comment, but emits no check: only
9//! the application knows how to verify a credential.
10
11use openapiv3::APIKeyLocation;
12use openapiv3::ReferenceOr;
13use openapiv3::SecurityRequirement;
14use openapiv3::SecurityScheme as OasSecurityScheme;
15
16use crate::ir::SecurityScheme;
17use crate::ir::SecuritySchemeKind;
18use crate::loader::Spec;
19use crate::naming::Case;
20use crate::naming::to_ident;
21
22/// Build the ordered catalogue of security schemes declared in the document,
23/// in `components.securitySchemes` declaration order.
24///
25/// `$ref` scheme entries, OAuth2, and OpenID Connect are catalogued as
26/// [`SecuritySchemeKind::Unsupported`] so an operation that requires one is
27/// rejected with a clear message during client emit, rather than silently
28/// sending no credential.
29pub fn scheme_catalogue(spec: &Spec) -> Vec<SecurityScheme> {
30    let mut schemes = Vec::new();
31    for (key, entry) in spec.security_schemes() {
32        let (kind, doc) = match entry {
33            ReferenceOr::Item(scheme) => lower_scheme(scheme),
34            ReferenceOr::Reference { reference } => (
35                SecuritySchemeKind::Unsupported(format!(
36                    "security scheme `{key}` is a `$ref` (`{reference}`), which is not supported"
37                )),
38                None,
39            ),
40        };
41        schemes.push(SecurityScheme {
42            key: key.clone(),
43            field: to_ident(key, Case::Snake),
44            kind,
45            doc,
46        });
47    }
48    return schemes;
49}
50
51/// Convert a single OpenAPI security scheme into its IR kind and doc string.
52fn lower_scheme(scheme: &OasSecurityScheme) -> (SecuritySchemeKind, Option<String>) {
53    match scheme {
54        OasSecurityScheme::HTTP {
55            scheme, description, ..
56        } => {
57            let kind = if scheme.eq_ignore_ascii_case("bearer") {
58                SecuritySchemeKind::HttpBearer
59            } else if scheme.eq_ignore_ascii_case("basic") {
60                SecuritySchemeKind::HttpBasic
61            } else {
62                SecuritySchemeKind::Unsupported(format!("HTTP authentication scheme `{scheme}` is not supported"))
63            };
64            return (kind, description.clone());
65        }
66        OasSecurityScheme::APIKey {
67            location,
68            name,
69            description,
70            ..
71        } => {
72            let kind = match location {
73                APIKeyLocation::Header => SecuritySchemeKind::ApiKeyHeader(name.clone()),
74                APIKeyLocation::Query => SecuritySchemeKind::ApiKeyQuery(name.clone()),
75                APIKeyLocation::Cookie => SecuritySchemeKind::ApiKeyCookie(name.clone()),
76            };
77            return (kind, description.clone());
78        }
79        OasSecurityScheme::OAuth2 { description, .. } => {
80            return (
81                SecuritySchemeKind::Unsupported("OAuth2 security is not supported by the client generator".to_owned()),
82                description.clone(),
83            );
84        }
85        OasSecurityScheme::OpenIDConnect { description, .. } => {
86            return (
87                SecuritySchemeKind::Unsupported(
88                    "OpenID Connect security is not supported by the client generator".to_owned(),
89                ),
90                description.clone(),
91            );
92        }
93    }
94}
95
96/// The security requirements in effect for an operation: its own `security` if
97/// present (an empty list explicitly disables auth), otherwise the document's
98/// global `security`.
99pub fn effective_requirements<'a>(
100    operation_security: Option<&'a [SecurityRequirement]>,
101    global_security: Option<&'a [SecurityRequirement]>,
102) -> Option<&'a [SecurityRequirement]> {
103    return operation_security.or(global_security);
104}
105
106/// The distinct scheme keys required by `requirements`, in first-seen order.
107///
108/// Alternatives (the outer list) and conjunctions (each map's keys) are
109/// flattened to their union: the client sends every configured, required
110/// credential, which is correct for the common single-scheme case and harmless
111/// otherwise.
112pub fn required_keys(requirements: &[SecurityRequirement]) -> Vec<String> {
113    let mut keys: Vec<String> = Vec::new();
114    for requirement in requirements {
115        for key in requirement.keys() {
116            if !keys.iter().any(|existing| return existing == key) {
117                keys.push(key.clone());
118            }
119        }
120    }
121    return keys;
122}