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;