Skip to main content

kynos_openapi/model/security/
mod.rs

1//! The Security Scheme, OAuth Flows and Security Requirement Objects.
2
3pub mod oauth;
4pub mod requirement;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::model::{extensions::Extensions, parameter::ParameterIn, security::oauth::OAuthFlows};
10
11/// A security scheme the API can use.
12///
13/// The variants are the five `type` values the specification defines. Modelling
14/// them as an enum rather than one struct with conditionally-required fields
15/// means an unusable combination — an `apiKey` scheme with OAuth flows, say —
16/// cannot be constructed.
17/// `#[non_exhaustive]` because OpenAPI 3.2 adds to this and the addition is
18/// `#[cfg]`-gated. Cargo unifies features across a dependency graph, so any
19/// crate enabling `openapi32` enables it for every crate in the build -- and
20/// without this attribute that would turn a downstream exhaustive `match` into
21/// a compile error, which is not what "purely additive" is supposed to mean.
22///
23/// # Every variant is sealed too
24///
25/// The attribute above covers a variant being *added*. 3.2 also adds a field
26/// to every variant already here — `deprecated`, and `oauth2MetadataUrl` on
27/// [`OAuth2`](Self::OAuth2) — so each variant carries the attribute as well.
28/// The enum's does not reach a variant's field list, and a field list is what
29/// a pattern names.
30///
31/// So a pattern takes `..`, and reads the same in either build:
32///
33/// ```
34/// # use kynos_openapi::SecurityScheme;
35/// fn scheme_of(security: &SecurityScheme) -> Option<&str> {
36///     match security {
37///         SecurityScheme::Http { scheme, .. } => Some(scheme),
38///         _ => None,
39///     }
40/// }
41/// # assert_eq!(scheme_of(&SecurityScheme::basic()), Some("basic"));
42/// ```
43///
44/// Without it, naming every field is a compile error even when the list is
45/// complete for this build — which is the guarantee. It is the error a
46/// downstream crate would otherwise have met the day something else in its
47/// build turned `openapi32` on.
48///
49/// ```compile_fail
50/// # use kynos_openapi::SecurityScheme;
51/// fn scheme_of(security: &SecurityScheme) -> Option<&str> {
52///     match security {
53///         SecurityScheme::Http {
54///             scheme,
55///             bearer_format,
56///             description,
57///             deprecated,
58///             extensions,
59///         } => Some(scheme),
60///         _ => None,
61///     }
62/// }
63/// ```
64///
65/// Construction goes through the constructors for the same reason:
66/// [`http`](Self::http), [`bearer`](Self::bearer), [`basic`](Self::basic), the
67/// three `api_key_*`, [`mutual_tls`](Self::mutual_tls),
68/// [`oauth2`](Self::oauth2) and [`open_id_connect`](Self::open_id_connect),
69/// then [`with_description`](Self::with_description),
70/// [`with_extension`](Self::with_extension) and the rest.
71#[non_exhaustive]
72#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(tag = "type")]
74pub enum SecurityScheme {
75    /// A key carried in a header, query parameter or cookie.
76    #[non_exhaustive]
77    #[serde(rename = "apiKey")]
78    ApiKey {
79        /// The name of the header, query parameter or cookie.
80        name: String,
81        /// Where the key is carried. Only query, header and cookie are legal.
82        #[serde(rename = "in")]
83        location: ParameterIn,
84        /// A description of the scheme. [CommonMark] syntax may be used.
85        ///
86        /// [CommonMark]: https://spec.commonmark.org/
87        #[serde(default, skip_serializing_if = "Option::is_none")]
88        description: Option<String>,
89        /// Whether the scheme is deprecated.
90        ///
91        /// Introduced in OpenAPI 3.2.
92        #[cfg(feature = "openapi32")]
93        #[serde(default, skip_serializing_if = "Option::is_none")]
94        deprecated: Option<bool>,
95        /// Specification extensions.
96        #[serde(flatten)]
97        extensions: Extensions,
98    },
99
100    /// An RFC 7235 `Authorization` header scheme.
101    #[non_exhaustive]
102    #[serde(rename = "http")]
103    Http {
104        /// The registered authorization scheme name, such as `bearer`.
105        scheme: String,
106        /// A hint about the bearer token's format, such as `JWT`.
107        #[serde(
108            rename = "bearerFormat",
109            default,
110            skip_serializing_if = "Option::is_none"
111        )]
112        bearer_format: Option<String>,
113        /// A description of the scheme.
114        #[serde(default, skip_serializing_if = "Option::is_none")]
115        description: Option<String>,
116        /// Whether the scheme is deprecated.
117        ///
118        /// Introduced in OpenAPI 3.2.
119        #[cfg(feature = "openapi32")]
120        #[serde(default, skip_serializing_if = "Option::is_none")]
121        deprecated: Option<bool>,
122        /// Specification extensions.
123        #[serde(flatten)]
124        extensions: Extensions,
125    },
126
127    /// Mutual TLS client certificate authentication.
128    ///
129    /// Kynos declares this automatically when the listener is configured to
130    /// verify client certificates, so enabling mTLS cannot leave the
131    /// description silent about it.
132    #[non_exhaustive]
133    #[serde(rename = "mutualTLS")]
134    MutualTls {
135        /// A description of the scheme.
136        #[serde(default, skip_serializing_if = "Option::is_none")]
137        description: Option<String>,
138        /// Whether the scheme is deprecated.
139        ///
140        /// Introduced in OpenAPI 3.2.
141        #[cfg(feature = "openapi32")]
142        #[serde(default, skip_serializing_if = "Option::is_none")]
143        deprecated: Option<bool>,
144        /// Specification extensions.
145        #[serde(flatten)]
146        extensions: Extensions,
147    },
148
149    /// OAuth 2.0.
150    #[non_exhaustive]
151    #[serde(rename = "oauth2")]
152    OAuth2 {
153        /// The supported flows.
154        flows: Box<OAuthFlows>,
155        /// A URL to the RFC 8414 authorization server metadata.
156        ///
157        /// Introduced in OpenAPI 3.2.
158        #[cfg(feature = "openapi32")]
159        #[serde(
160            rename = "oauth2MetadataUrl",
161            default,
162            skip_serializing_if = "Option::is_none"
163        )]
164        oauth2_metadata_url: Option<String>,
165        /// A description of the scheme.
166        #[serde(default, skip_serializing_if = "Option::is_none")]
167        description: Option<String>,
168        /// Whether the scheme is deprecated.
169        ///
170        /// Introduced in OpenAPI 3.2.
171        #[cfg(feature = "openapi32")]
172        #[serde(default, skip_serializing_if = "Option::is_none")]
173        deprecated: Option<bool>,
174        /// Specification extensions.
175        #[serde(flatten)]
176        extensions: Extensions,
177    },
178
179    /// OpenID Connect Discovery.
180    #[non_exhaustive]
181    #[serde(rename = "openIdConnect")]
182    OpenIdConnect {
183        /// The OpenID Connect Discovery URL.
184        #[serde(rename = "openIdConnectUrl")]
185        open_id_connect_url: String,
186        /// A description of the scheme.
187        #[serde(default, skip_serializing_if = "Option::is_none")]
188        description: Option<String>,
189        /// Whether the scheme is deprecated.
190        ///
191        /// Introduced in OpenAPI 3.2.
192        #[cfg(feature = "openapi32")]
193        #[serde(default, skip_serializing_if = "Option::is_none")]
194        deprecated: Option<bool>,
195        /// Specification extensions.
196        #[serde(flatten)]
197        extensions: Extensions,
198    },
199}
200
201impl SecurityScheme {
202    /// An HTTP authentication scheme, named by its RFC 7235 scheme token.
203    ///
204    /// [`bearer`](Self::bearer) and [`basic`](Self::basic) are the two worth
205    /// naming; this is for the rest of the IANA registry, and for a scheme
206    /// read out of a description someone else wrote.
207    #[must_use]
208    pub fn http(scheme: impl Into<String>, bearer_format: Option<String>) -> Self {
209        Self::Http {
210            scheme: scheme.into(),
211            bearer_format,
212            description: None,
213            #[cfg(feature = "openapi32")]
214            deprecated: None,
215            extensions: Extensions::new(),
216        }
217    }
218
219    /// An HTTP bearer token scheme.
220    #[must_use]
221    pub fn bearer(bearer_format: Option<String>) -> Self {
222        Self::http("bearer", bearer_format)
223    }
224
225    /// An HTTP basic authentication scheme.
226    #[must_use]
227    pub fn basic() -> Self {
228        Self::http("basic", None)
229    }
230
231    /// An API key carried in a header.
232    pub fn api_key_header(name: impl Into<String>) -> Self {
233        Self::ApiKey {
234            name: name.into(),
235            location: ParameterIn::Header,
236            description: None,
237            #[cfg(feature = "openapi32")]
238            deprecated: None,
239            extensions: Extensions::new(),
240        }
241    }
242
243    /// An API key carried in a query parameter.
244    pub fn api_key_query(name: impl Into<String>) -> Self {
245        Self::ApiKey {
246            name: name.into(),
247            location: ParameterIn::Query,
248            description: None,
249            #[cfg(feature = "openapi32")]
250            deprecated: None,
251            extensions: Extensions::new(),
252        }
253    }
254
255    /// An API key carried in a cookie.
256    pub fn api_key_cookie(name: impl Into<String>) -> Self {
257        Self::ApiKey {
258            name: name.into(),
259            location: ParameterIn::Cookie,
260            description: None,
261            #[cfg(feature = "openapi32")]
262            deprecated: None,
263            extensions: Extensions::new(),
264        }
265    }
266
267    /// Mutual TLS client certificate authentication.
268    #[must_use]
269    pub fn mutual_tls() -> Self {
270        Self::MutualTls {
271            description: None,
272            #[cfg(feature = "openapi32")]
273            deprecated: None,
274            extensions: Extensions::new(),
275        }
276    }
277
278    /// OAuth 2.0 with the given flows.
279    ///
280    /// A constructor rather than a struct literal, so the `#[cfg]`-gated
281    /// fields are written down once here instead of at every call site — which
282    /// is what a caller in a crate that cannot see the feature needs.
283    #[must_use]
284    pub fn oauth2(flows: OAuthFlows) -> Self {
285        Self::OAuth2 {
286            flows: Box::new(flows),
287            #[cfg(feature = "openapi32")]
288            oauth2_metadata_url: None,
289            description: None,
290            #[cfg(feature = "openapi32")]
291            deprecated: None,
292            extensions: Extensions::new(),
293        }
294    }
295
296    /// OpenID Connect Discovery, against the given metadata URL.
297    pub fn open_id_connect(url: impl Into<String>) -> Self {
298        Self::OpenIdConnect {
299            open_id_connect_url: url.into(),
300            description: None,
301            #[cfg(feature = "openapi32")]
302            deprecated: None,
303            extensions: Extensions::new(),
304        }
305    }
306
307    /// Sets the scheme's description.
308    #[must_use]
309    pub fn with_description(mut self, description: impl Into<String>) -> Self {
310        let slot = match &mut self {
311            Self::ApiKey { description, .. }
312            | Self::Http { description, .. }
313            | Self::MutualTls { description, .. }
314            | Self::OAuth2 { description, .. }
315            | Self::OpenIdConnect { description, .. } => description,
316        };
317        *slot = Some(description.into());
318        self
319    }
320
321    /// Attaches a specification extension.
322    ///
323    /// Every variant is `#[non_exhaustive]`, so a caller outside this crate
324    /// cannot reach `extensions` through a struct literal; this is how one
325    /// arrives. Reading them back needs no method — a pattern with `..` still
326    /// binds the field.
327    #[must_use]
328    pub fn with_extension(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
329        let slot = match &mut self {
330            Self::ApiKey { extensions, .. }
331            | Self::Http { extensions, .. }
332            | Self::MutualTls { extensions, .. }
333            | Self::OAuth2 { extensions, .. }
334            | Self::OpenIdConnect { extensions, .. } => extensions,
335        };
336        slot.insert(key, value);
337        self
338    }
339
340    /// States whether the scheme is deprecated.
341    ///
342    /// [`deprecate`](Self::deprecate) is the common case. This exists because
343    /// `deprecated: false` is a thing a description can say and a round trip
344    /// has to keep saying, which a method that only ever writes `true` cannot
345    /// express.
346    ///
347    /// Introduced in OpenAPI 3.2, and a blocker for emitting the document as
348    /// 3.1 — see [`emit`](crate::emit).
349    #[cfg(feature = "openapi32")]
350    #[must_use]
351    pub fn with_deprecated(mut self, deprecated: bool) -> Self {
352        let slot = match &mut self {
353            Self::ApiKey { deprecated, .. }
354            | Self::Http { deprecated, .. }
355            | Self::MutualTls { deprecated, .. }
356            | Self::OAuth2 { deprecated, .. }
357            | Self::OpenIdConnect { deprecated, .. } => deprecated,
358        };
359        *slot = Some(deprecated);
360        self
361    }
362
363    /// Marks the scheme deprecated.
364    ///
365    /// Introduced in OpenAPI 3.2, and a blocker for emitting the document as
366    /// 3.1 — see [`emit`](crate::emit).
367    #[cfg(feature = "openapi32")]
368    #[must_use]
369    pub fn deprecate(mut self) -> Self {
370        let slot = match &mut self {
371            Self::ApiKey { deprecated, .. }
372            | Self::Http { deprecated, .. }
373            | Self::MutualTls { deprecated, .. }
374            | Self::OAuth2 { deprecated, .. }
375            | Self::OpenIdConnect { deprecated, .. } => deprecated,
376        };
377        *slot = Some(true);
378        self
379    }
380
381    /// Sets the RFC 8414 authorization server metadata URL.
382    ///
383    /// Ignored by any scheme that is not OAuth 2.0, because no other kind has
384    /// the field. Introduced in OpenAPI 3.2.
385    #[cfg(feature = "openapi32")]
386    #[must_use]
387    pub fn with_oauth2_metadata_url(mut self, url: impl Into<String>) -> Self {
388        if let Self::OAuth2 {
389            oauth2_metadata_url,
390            ..
391        } = &mut self
392        {
393            *oauth2_metadata_url = Some(url.into());
394        }
395        self
396    }
397}
398
399#[cfg(test)]
400mod tests;