salvo_oapi/openapi/
security.rs

1//! Implements [OpenAPI Security Schema][security] types.
2//!
3//! Refer to [`SecurityScheme`] for usage and more details.
4//!
5//! [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
6use std::collections::BTreeMap;
7use std::iter;
8
9use serde::{Deserialize, Serialize};
10
11use crate::PropMap;
12
13/// OpenAPI [security requirement][security] object.
14///
15/// Security requirement holds list of required [`SecurityScheme`] *names* and possible *scopes* required
16/// to execute the operation. They can be defined in [`#[salvo_oapi::endpoint(...)]`][endpoint].
17///
18/// Applying the security requirement to [`OpenApi`][openapi] will make it globally
19/// available to all operations. When applied to specific [`#[salvo_oapi::endpoint(...)]`][endpoint] will only
20/// make the security requirements available for that operation. Only one of the requirements must be
21/// satisfied.
22///
23/// [security]: https://spec.openapis.org/oas/latest.html#security-requirement-object
24/// [endpoint]: ../../attr.endpoint.html
25/// [openapi]: ../../derive.OpenApi.html
26#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, Default, Clone, PartialEq, Eq)]
27pub struct SecurityRequirement {
28    #[serde(flatten)]
29    pub(crate) value: BTreeMap<String, Vec<String>>,
30}
31
32impl SecurityRequirement {
33    /// Construct a new [`SecurityRequirement`]
34    ///
35    /// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
36    /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
37    /// Scopes must match to the ones defined in [`SecurityScheme`].
38    ///
39    /// # Examples
40    ///
41    /// Create new security requirement with scopes.
42    /// ```
43    /// # use salvo_oapi::security::SecurityRequirement;
44    /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
45    /// ```
46    ///
47    /// You can also create an empty security requirement with `Default::default()`.
48    /// ```
49    /// # use salvo_oapi::security::SecurityRequirement;
50    /// SecurityRequirement::default();
51    /// ```
52    pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
53        name: N,
54        scopes: S,
55    ) -> Self {
56        Self {
57            value: BTreeMap::from_iter(iter::once_with(|| {
58                (
59                    Into::<String>::into(name),
60                    scopes
61                        .into_iter()
62                        .map(|scope| Into::<String>::into(scope))
63                        .collect::<Vec<_>>(),
64                )
65            })),
66        }
67    }
68
69    /// Check if the security requirement is empty.
70    pub fn is_empty(&self) -> bool {
71        self.value.is_empty()
72    }
73
74    /// Allows to add multiple names to security requirement.
75    ///
76    /// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
77    /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
78    /// Scopes must match to the ones defined in [`SecurityScheme`].
79    pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
80        mut self,
81        name: N,
82        scopes: S,
83    ) -> Self {
84        self.value.insert(
85            Into::<String>::into(name),
86            scopes.into_iter().map(Into::<String>::into).collect(),
87        );
88
89        self
90    }
91}
92
93/// OpenAPI [security scheme][security] for path operations.
94///
95/// [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
96///
97/// # Examples
98///
99/// Create implicit oauth2 flow security schema for path operations.
100/// ```
101/// # use salvo_oapi::security::{SecurityScheme, OAuth2, Implicit, Flow, Scopes};
102/// SecurityScheme::OAuth2(
103///     OAuth2::with_description([Flow::Implicit(
104///         Implicit::new(
105///             "https://localhost/auth/dialog",
106///             Scopes::from_iter([
107///                 ("edit:items", "edit my items"),
108///                 ("read:items", "read my items")
109///             ]),
110///         ),
111///     )], "my oauth2 flow")
112/// );
113/// ```
114///
115/// Create JWT header authentication.
116/// ```
117/// # use salvo_oapi::security::{SecurityScheme, HttpAuthScheme, Http};
118/// SecurityScheme::Http(
119///     Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")
120/// );
121/// ```
122#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
123#[serde(tag = "type", rename_all = "camelCase")]
124pub enum SecurityScheme {
125    /// Oauth flow authentication.
126    #[serde(rename = "oauth2")]
127    OAuth2(OAuth2),
128    /// Api key authentication sent in *`header`*, *`cookie`* or *`query`*.
129    ApiKey(ApiKey),
130    /// Http authentication such as *`bearer`* or *`basic`*.
131    Http(Http),
132    /// Open id connect url to discover OAuth2 configuration values.
133    OpenIdConnect(OpenIdConnect),
134    /// Authentication is done via client side certificate.
135    ///
136    /// OpenApi 3.1 type
137    #[serde(rename = "mutualTLS")]
138    MutualTls {
139        /// Description information.
140        #[serde(skip_serializing_if = "Option::is_none")]
141        description: Option<String>,
142    },
143}
144impl From<OAuth2> for SecurityScheme {
145    fn from(oauth2: OAuth2) -> Self {
146        Self::OAuth2(oauth2)
147    }
148}
149impl From<ApiKey> for SecurityScheme {
150    fn from(api_key: ApiKey) -> Self {
151        Self::ApiKey(api_key)
152    }
153}
154impl From<OpenIdConnect> for SecurityScheme {
155    fn from(open_id_connect: OpenIdConnect) -> Self {
156        Self::OpenIdConnect(open_id_connect)
157    }
158}
159
160/// Api key authentication [`SecurityScheme`].
161#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
162#[serde(tag = "in", rename_all = "lowercase")]
163pub enum ApiKey {
164    /// Create api key which is placed in HTTP header.
165    Header(ApiKeyValue),
166    /// Create api key which is placed in query parameters.
167    Query(ApiKeyValue),
168    /// Create api key which is placed in cookie value.
169    Cookie(ApiKeyValue),
170}
171
172/// Value object for [`ApiKey`].
173#[non_exhaustive]
174#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
175pub struct ApiKeyValue {
176    /// Name of the [`ApiKey`] parameter.
177    pub name: String,
178
179    /// Description of the [`ApiKey`] [`SecurityScheme`]. Supports markdown syntax.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub description: Option<String>,
182}
183
184impl ApiKeyValue {
185    /// Constructs new api key value.
186    ///
187    /// # Examples
188    ///
189    /// Create new api key security schema with name `api_key`.
190    /// ```
191    /// # use salvo_oapi::security::ApiKeyValue;
192    /// let api_key = ApiKeyValue::new("api_key");
193    /// ```
194    pub fn new<S: Into<String>>(name: S) -> Self {
195        Self {
196            name: name.into(),
197            description: None,
198        }
199    }
200
201    /// Construct a new api key with optional description supporting markdown syntax.
202    ///
203    /// # Examples
204    ///
205    /// Create new api key security schema with name `api_key` with description.
206    /// ```
207    /// # use salvo_oapi::security::ApiKeyValue;
208    /// let api_key = ApiKeyValue::with_description("api_key", "my api_key token");
209    /// ```
210    pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
211        Self {
212            name: name.into(),
213            description: Some(description.into()),
214        }
215    }
216}
217
218/// Http authentication [`SecurityScheme`] builder.
219///
220/// Methods can be chained to configure _bearer_format_ or to add _description_.
221#[non_exhaustive]
222#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
223#[serde(rename_all = "camelCase")]
224pub struct Http {
225    /// Http authorization scheme in HTTP `Authorization` header value.
226    pub scheme: HttpAuthScheme,
227
228    /// Optional hint to client how the bearer token is formatted. Valid only with [`HttpAuthScheme::Bearer`].
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub bearer_format: Option<String>,
231
232    /// Optional description of [`Http`] [`SecurityScheme`] supporting markdown syntax.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub description: Option<String>,
235}
236
237impl Http {
238    /// Create new http authentication security schema.
239    ///
240    /// Accepts one argument which defines the scheme of the http authentication.
241    ///
242    /// # Examples
243    ///
244    /// Create http security schema with basic authentication.
245    /// ```
246    /// # use salvo_oapi::security::{SecurityScheme, Http, HttpAuthScheme};
247    /// SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
248    /// ```
249    pub fn new(scheme: HttpAuthScheme) -> Self {
250        Self {
251            scheme,
252            bearer_format: None,
253            description: None,
254        }
255    }
256    /// Add or change http authentication scheme used.
257    pub fn scheme(mut self, scheme: HttpAuthScheme) -> Self {
258        self.scheme = scheme;
259
260        self
261    }
262    /// Add or change informative bearer format for http security schema.
263    ///
264    /// This is only applicable to [`HttpAuthScheme::Bearer`].
265    ///
266    /// # Examples
267    ///
268    /// Add JTW bearer format for security schema.
269    /// ```
270    /// # use salvo_oapi::security::{Http, HttpAuthScheme};
271    /// Http::new(HttpAuthScheme::Bearer).bearer_format("JWT");
272    /// ```
273    pub fn bearer_format<S: Into<String>>(mut self, bearer_format: S) -> Self {
274        if self.scheme == HttpAuthScheme::Bearer {
275            self.bearer_format = Some(bearer_format.into());
276        }
277
278        self
279    }
280
281    /// Add or change optional description supporting markdown syntax.
282    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
283        self.description = Some(description.into());
284
285        self
286    }
287}
288
289/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1).
290///
291/// Types are maintained at <https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml>.
292#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
293#[serde(rename_all = "lowercase")]
294pub enum HttpAuthScheme {
295    /// Basic authentication scheme.
296    Basic,
297    /// Bearer authentication scheme.
298    Bearer,
299    /// Digest authentication scheme.
300    Digest,
301    /// HOBA authentication scheme.
302    Hoba,
303    /// Mutual authentication scheme.
304    Mutual,
305    /// Negotiate authentication scheme.
306    Negotiate,
307    /// OAuth authentication scheme.
308    OAuth,
309    /// ScramSha1 authentication scheme.
310    #[serde(rename = "scram-sha-1")]
311    ScramSha1,
312    /// ScramSha256 authentication scheme.
313    #[serde(rename = "scram-sha-256")]
314    ScramSha256,
315    /// Vapid authentication scheme.
316    Vapid,
317}
318
319impl Default for HttpAuthScheme {
320    fn default() -> Self {
321        Self::Basic
322    }
323}
324
325/// Open id connect [`SecurityScheme`]
326#[non_exhaustive]
327#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
328#[serde(rename_all = "camelCase")]
329pub struct OpenIdConnect {
330    /// Url of the [`OpenIdConnect`] to discover OAuth2 connect values.
331    pub open_id_connect_url: String,
332
333    /// Description of [`OpenIdConnect`] [`SecurityScheme`] supporting markdown syntax.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub description: Option<String>,
336}
337
338impl OpenIdConnect {
339    /// Construct a new open id connect security schema.
340    ///
341    /// # Examples
342    ///
343    /// ```
344    /// # use salvo_oapi::security::OpenIdConnect;
345    /// OpenIdConnect::new("https://localhost/openid");
346    /// ```
347    pub fn new<S: Into<String>>(open_id_connect_url: S) -> Self {
348        Self {
349            open_id_connect_url: open_id_connect_url.into(),
350            description: None,
351        }
352    }
353
354    /// Construct a new [`OpenIdConnect`] [`SecurityScheme`] with optional description
355    /// supporting markdown syntax.
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// # use salvo_oapi::security::OpenIdConnect;
361    /// OpenIdConnect::with_description("https://localhost/openid", "my pet api open id connect");
362    /// ```
363    pub fn with_description<S: Into<String>>(open_id_connect_url: S, description: S) -> Self {
364        Self {
365            open_id_connect_url: open_id_connect_url.into(),
366            description: Some(description.into()),
367        }
368    }
369}
370
371/// OAuth2 [`Flow`] configuration for [`SecurityScheme`].
372#[non_exhaustive]
373#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
374pub struct OAuth2 {
375    /// Map of supported OAuth2 flows.
376    pub flows: PropMap<String, Flow>,
377
378    /// Optional description for the [`OAuth2`] [`Flow`] [`SecurityScheme`].
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub description: Option<String>,
381
382    /// Optional extensions "x-something"
383    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
384    pub extensions: PropMap<String, serde_json::Value>,
385}
386
387impl OAuth2 {
388    /// Construct a new OAuth2 security schema configuration object.
389    ///
390    /// Oauth flow accepts slice of [`Flow`] configuration objects and can be optionally provided with description.
391    ///
392    /// # Examples
393    ///
394    /// Create new OAuth2 flow with multiple authentication flows.
395    /// ```
396    /// # use salvo_oapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
397    /// OAuth2::new([Flow::Password(
398    ///     Password::with_refresh_url(
399    ///         "https://localhost/oauth/token",
400    ///         Scopes::from_iter([
401    ///             ("edit:items", "edit my items"),
402    ///             ("read:items", "read my items")
403    ///         ]),
404    ///         "https://localhost/refresh/token"
405    ///     )),
406    ///     Flow::AuthorizationCode(
407    ///         AuthorizationCode::new(
408    ///         "https://localhost/authorization/token",
409    ///         "https://localhost/token/url",
410    ///         Scopes::from_iter([
411    ///             ("edit:items", "edit my items"),
412    ///             ("read:items", "read my items")
413    ///         ])),
414    ///    ),
415    /// ]);
416    /// ```
417    pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
418        Self {
419            flows: PropMap::from_iter(
420                flows
421                    .into_iter()
422                    .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
423            ),
424            description: None,
425            extensions: Default::default(),
426        }
427    }
428
429    /// Construct a new OAuth2 flow with optional description supporting markdown syntax.
430    ///
431    /// # Examples
432    ///
433    /// Create new OAuth2 flow with multiple authentication flows with description.
434    /// ```
435    /// # use salvo_oapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
436    /// OAuth2::with_description([Flow::Password(
437    ///     Password::with_refresh_url(
438    ///         "https://localhost/oauth/token",
439    ///         Scopes::from_iter([
440    ///             ("edit:items", "edit my items"),
441    ///             ("read:items", "read my items")
442    ///         ]),
443    ///         "https://localhost/refresh/token"
444    ///     )),
445    ///     Flow::AuthorizationCode(
446    ///         AuthorizationCode::new(
447    ///         "https://localhost/authorization/token",
448    ///         "https://localhost/token/url",
449    ///         Scopes::from_iter([
450    ///             ("edit:items", "edit my items"),
451    ///             ("read:items", "read my items")
452    ///         ])
453    ///      ),
454    ///    ),
455    /// ], "my oauth2 flow");
456    /// ```
457    pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(
458        flows: I,
459        description: S,
460    ) -> Self {
461        Self {
462            flows: PropMap::from_iter(
463                flows
464                    .into_iter()
465                    .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
466            ),
467            description: Some(description.into()),
468            extensions: Default::default(),
469        }
470    }
471}
472
473/// [`OAuth2`] flow configuration object.
474///
475///
476/// See more details at <https://spec.openapis.org/oas/latest.html#oauth-flows-object>.
477#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
478#[serde(untagged)]
479pub enum Flow {
480    /// Define implicit [`Flow`] type. See [`Implicit::new`] for usage details.
481    ///
482    /// Soon to be deprecated by <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics>.
483    Implicit(Implicit),
484    /// Define password [`Flow`] type. See [`Password::new`] for usage details.
485    Password(Password),
486    /// Define client credentials [`Flow`] type. See [`ClientCredentials::new`] for usage details.
487    ClientCredentials(ClientCredentials),
488    /// Define authorization code [`Flow`] type. See [`AuthorizationCode::new`] for usage details.
489    AuthorizationCode(AuthorizationCode),
490}
491
492impl Flow {
493    fn get_type_as_str(&self) -> &str {
494        match self {
495            Self::Implicit(_) => "implicit",
496            Self::Password(_) => "password",
497            Self::ClientCredentials(_) => "clientCredentials",
498            Self::AuthorizationCode(_) => "authorizationCode",
499        }
500    }
501}
502
503/// Implicit [`Flow`] configuration for [`OAuth2`].
504#[non_exhaustive]
505#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
506#[serde(rename_all = "camelCase")]
507pub struct Implicit {
508    /// Authorization token url for the flow.
509    pub authorization_url: String,
510
511    /// Optional refresh token url for the flow.
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub refresh_url: Option<String>,
514
515    /// Scopes required by the flow.
516    #[serde(flatten)]
517    pub scopes: Scopes,
518}
519
520impl Implicit {
521    /// Construct a new implicit oauth2 flow.
522    ///
523    /// Accepts two arguments: one which is authorization url and second map of scopes. Scopes can
524    /// also be an empty map.
525    ///
526    /// # Examples
527    ///
528    /// Create new implicit flow with scopes.
529    /// ```
530    /// # use salvo_oapi::security::{Implicit, Scopes};
531    /// Implicit::new(
532    ///     "https://localhost/auth/dialog",
533    ///     Scopes::from_iter([
534    ///         ("edit:items", "edit my items"),
535    ///         ("read:items", "read my items")
536    ///     ]),
537    /// );
538    /// ```
539    ///
540    /// Create new implicit flow without any scopes.
541    /// ```
542    /// # use salvo_oapi::security::{Implicit, Scopes};
543    /// Implicit::new(
544    ///     "https://localhost/auth/dialog",
545    ///     Scopes::new(),
546    /// );
547    /// ```
548    pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
549        Self {
550            authorization_url: authorization_url.into(),
551            refresh_url: None,
552            scopes,
553        }
554    }
555
556    /// Construct a new implicit oauth2 flow with refresh url for getting refresh tokens.
557    ///
558    /// This is essentially same as [`Implicit::new`] but allows defining `refresh_url` for the [`Implicit`]
559    /// oauth2 flow.
560    ///
561    /// # Examples
562    ///
563    /// Create a new implicit oauth2 flow with refresh token.
564    /// ```
565    /// # use salvo_oapi::security::{Implicit, Scopes};
566    /// Implicit::with_refresh_url(
567    ///     "https://localhost/auth/dialog",
568    ///     Scopes::new(),
569    ///     "https://localhost/refresh-token"
570    /// );
571    /// ```
572    pub fn with_refresh_url<S: Into<String>>(
573        authorization_url: S,
574        scopes: Scopes,
575        refresh_url: S,
576    ) -> Self {
577        Self {
578            authorization_url: authorization_url.into(),
579            refresh_url: Some(refresh_url.into()),
580            scopes,
581        }
582    }
583}
584
585/// Authorization code [`Flow`] configuration for [`OAuth2`].
586#[non_exhaustive]
587#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
588#[serde(rename_all = "camelCase")]
589pub struct AuthorizationCode {
590    /// Url for authorization token.
591    pub authorization_url: String,
592    /// Token url for the flow.
593    pub token_url: String,
594
595    /// Optional refresh token url for the flow.
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub refresh_url: Option<String>,
598
599    /// Scopes required by the flow.
600    #[serde(flatten)]
601    pub scopes: Scopes,
602}
603
604impl AuthorizationCode {
605    /// Construct a new authorization code oauth flow.
606    ///
607    /// Accepts three arguments: one which is authorization url, two a token url and
608    /// three a map of scopes for oauth flow.
609    ///
610    /// # Examples
611    ///
612    /// Create new authorization code flow with scopes.
613    /// ```
614    /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
615    /// AuthorizationCode::new(
616    ///     "https://localhost/auth/dialog",
617    ///     "https://localhost/token",
618    ///     Scopes::from_iter([
619    ///         ("edit:items", "edit my items"),
620    ///         ("read:items", "read my items")
621    ///     ]),
622    /// );
623    /// ```
624    ///
625    /// Create new authorization code flow without any scopes.
626    /// ```
627    /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
628    /// AuthorizationCode::new(
629    ///     "https://localhost/auth/dialog",
630    ///     "https://localhost/token",
631    ///     Scopes::new(),
632    /// );
633    /// ```
634    pub fn new<A: Into<String>, T: Into<String>>(
635        authorization_url: A,
636        token_url: T,
637        scopes: Scopes,
638    ) -> Self {
639        Self {
640            authorization_url: authorization_url.into(),
641            token_url: token_url.into(),
642            refresh_url: None,
643            scopes,
644        }
645    }
646
647    /// Construct a new  [`AuthorizationCode`] OAuth2 flow with additional refresh token url.
648    ///
649    /// This is essentially same as [`AuthorizationCode::new`] but allows defining extra parameter `refresh_url`
650    /// for fetching refresh token.
651    ///
652    /// # Examples
653    ///
654    /// Create [`AuthorizationCode`] OAuth2 flow with refresh url.
655    /// ```
656    /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
657    /// AuthorizationCode::with_refresh_url(
658    ///     "https://localhost/auth/dialog",
659    ///     "https://localhost/token",
660    ///     Scopes::new(),
661    ///     "https://localhost/refresh-token"
662    /// );
663    /// ```
664    pub fn with_refresh_url<S: Into<String>>(
665        authorization_url: S,
666        token_url: S,
667        scopes: Scopes,
668        refresh_url: S,
669    ) -> Self {
670        Self {
671            authorization_url: authorization_url.into(),
672            token_url: token_url.into(),
673            refresh_url: Some(refresh_url.into()),
674            scopes,
675        }
676    }
677}
678
679/// Password [`Flow`] configuration for [`OAuth2`].
680#[non_exhaustive]
681#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
682#[serde(rename_all = "camelCase")]
683pub struct Password {
684    /// Token url for this OAuth2 flow. OAuth2 standard requires TLS.
685    pub token_url: String,
686
687    /// Optional refresh token url.
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub refresh_url: Option<String>,
690
691    /// Scopes required by the flow.
692    #[serde(flatten)]
693    pub scopes: Scopes,
694}
695
696impl Password {
697    /// Construct a new password oauth flow.
698    ///
699    /// Accepts two arguments: one which is a token url and
700    /// two a map of scopes for oauth flow.
701    ///
702    /// # Examples
703    ///
704    /// Create new password flow with scopes.
705    /// ```
706    /// # use salvo_oapi::security::{Password, Scopes};
707    /// Password::new(
708    ///     "https://localhost/token",
709    ///     Scopes::from_iter([
710    ///         ("edit:items", "edit my items"),
711    ///         ("read:items", "read my items")
712    ///     ]),
713    /// );
714    /// ```
715    ///
716    /// Create new password flow without any scopes.
717    /// ```
718    /// # use salvo_oapi::security::{Password, Scopes};
719    /// Password::new(
720    ///     "https://localhost/token",
721    ///     Scopes::new(),
722    /// );
723    /// ```
724    pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
725        Self {
726            token_url: token_url.into(),
727            refresh_url: None,
728            scopes,
729        }
730    }
731
732    /// Construct a new password oauth flow with additional refresh url.
733    ///
734    /// This is essentially same as [`Password::new`] but allows defining third parameter for `refresh_url`
735    /// for fetching refresh tokens.
736    ///
737    /// # Examples
738    ///
739    /// Create new password flow with refresh url.
740    /// ```
741    /// # use salvo_oapi::security::{Password, Scopes};
742    /// Password::with_refresh_url(
743    ///     "https://localhost/token",
744    ///     Scopes::from_iter([
745    ///         ("edit:items", "edit my items"),
746    ///         ("read:items", "read my items")
747    ///     ]),
748    ///     "https://localhost/refres-token"
749    /// );
750    /// ```
751    pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
752        Self {
753            token_url: token_url.into(),
754            refresh_url: Some(refresh_url.into()),
755            scopes,
756        }
757    }
758}
759
760/// Client credentials [`Flow`] configuration for [`OAuth2`].
761#[non_exhaustive]
762#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
763#[serde(rename_all = "camelCase")]
764pub struct ClientCredentials {
765    /// Token url used for [`ClientCredentials`] flow. OAuth2 standard requires TLS.
766    pub token_url: String,
767
768    /// Optional refresh token url.
769    #[serde(skip_serializing_if = "Option::is_none")]
770    pub refresh_url: Option<String>,
771
772    /// Scopes required by the flow.
773    #[serde(flatten)]
774    pub scopes: Scopes,
775}
776
777impl ClientCredentials {
778    /// Construct a new client credentials oauth flow.
779    ///
780    /// Accepts two arguments: one which is a token url and
781    /// two a map of scopes for oauth flow.
782    ///
783    /// # Examples
784    ///
785    /// Create new client credentials flow with scopes.
786    /// ```
787    /// # use salvo_oapi::security::{ClientCredentials, Scopes};
788    /// ClientCredentials::new(
789    ///     "https://localhost/token",
790    ///     Scopes::from_iter([
791    ///         ("edit:items", "edit my items"),
792    ///         ("read:items", "read my items")
793    ///     ]),
794    /// );
795    /// ```
796    ///
797    /// Create new client credentials flow without any scopes.
798    /// ```
799    /// # use salvo_oapi::security::{ClientCredentials, Scopes};
800    /// ClientCredentials::new(
801    ///     "https://localhost/token",
802    ///     Scopes::new(),
803    /// );
804    /// ```
805    pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
806        Self {
807            token_url: token_url.into(),
808            refresh_url: None,
809            scopes,
810        }
811    }
812
813    /// Construct a new client credentials oauth flow with additional refresh url.
814    ///
815    /// This is essentially same as [`ClientCredentials::new`] but allows defining third parameter for
816    /// `refresh_url`.
817    ///
818    /// # Examples
819    ///
820    /// Create new client credentials for with refresh url.
821    /// ```
822    /// # use salvo_oapi::security::{ClientCredentials, Scopes};
823    /// ClientCredentials::with_refresh_url(
824    ///     "https://localhost/token",
825    ///     Scopes::from_iter([
826    ///         ("edit:items", "edit my items"),
827    ///         ("read:items", "read my items")
828    ///     ]),
829    ///     "https://localhost/refresh-url"
830    /// );
831    /// ```
832    pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
833        Self {
834            token_url: token_url.into(),
835            refresh_url: Some(refresh_url.into()),
836            scopes,
837        }
838    }
839}
840
841/// [`OAuth2`] flow scopes object defines required permissions for oauth flow.
842///
843/// Scopes must be given to oauth2 flow but depending on need one of few initialization methods
844/// could be used.
845///
846/// * Create empty map of scopes you can use [`Scopes::new`].
847/// * Create map with only one scope you can use [`Scopes::one`].
848/// * Create multiple scopes from iterator with [`Scopes::from_iter`].
849///
850/// # Examples
851///
852/// Create empty map of scopes.
853/// ```
854/// # use salvo_oapi::security::Scopes;
855/// let scopes = Scopes::new();
856/// ```
857///
858/// Create [`Scopes`] holding one scope.
859/// ```
860/// # use salvo_oapi::security::Scopes;
861/// let scopes = Scopes::one("edit:item", "edit pets");
862/// ```
863///
864/// Create map of scopes from iterator.
865/// ```
866/// # use salvo_oapi::security::Scopes;
867/// let scopes = Scopes::from_iter([
868///     ("edit:items", "edit my items"),
869///     ("read:items", "read my items")
870/// ]);
871/// ```
872#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
873pub struct Scopes {
874    scopes: PropMap<String, String>,
875}
876
877impl Scopes {
878    /// Construct new [`Scopes`] with empty map of scopes. This is useful if oauth flow does not need
879    /// any permission scopes.
880    ///
881    /// # Examples
882    ///
883    /// Create empty map of scopes.
884    /// ```
885    /// # use salvo_oapi::security::Scopes;
886    /// let scopes = Scopes::new();
887    /// ```
888    pub fn new() -> Self {
889        Default::default()
890    }
891
892    /// Construct new [`Scopes`] with holding one scope.
893    ///
894    /// * `scope` Is be the permission required.
895    /// * `description` Short description about the permission.
896    ///
897    /// # Examples
898    ///
899    /// Create map of scopes with one scope item.
900    /// ```
901    /// # use salvo_oapi::security::Scopes;
902    /// let scopes = Scopes::one("edit:item", "edit items");
903    /// ```
904    pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
905        Self {
906            scopes: PropMap::from_iter(iter::once_with(|| (scope.into(), description.into()))),
907        }
908    }
909}
910
911impl<I> FromIterator<(I, I)> for Scopes
912where
913    I: Into<String>,
914{
915    fn from_iter<T: IntoIterator<Item = (I, I)>>(iter: T) -> Self {
916        Self {
917            scopes: iter
918                .into_iter()
919                .map(|(key, value)| (key.into(), value.into()))
920                .collect(),
921        }
922    }
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928
929    macro_rules! test_fn {
930        ($name:ident: $schema:expr; $expected:literal) => {
931            #[test]
932            fn $name() {
933                let value = serde_json::to_value($schema).unwrap();
934                let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap();
935
936                assert_eq!(
937                    value,
938                    expected_value,
939                    "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}",
940                    stringify!($name),
941                    value,
942                    expected_value
943                );
944
945                println!("{}", &serde_json::to_string_pretty(&$schema).unwrap());
946            }
947        };
948    }
949
950    test_fn! {
951        security_scheme_correct_default_http_auth:
952        SecurityScheme::Http(Http::new(HttpAuthScheme::default()));
953        r###"{
954  "type": "http",
955  "scheme": "basic"
956}"###
957    }
958
959    test_fn! {
960        security_scheme_correct_http_bearer_json:
961        SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT"));
962        r###"{
963  "type": "http",
964  "scheme": "bearer",
965  "bearerFormat": "JWT"
966}"###
967    }
968
969    test_fn! {
970        security_scheme_correct_basic_auth:
971        SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
972        r###"{
973  "type": "http",
974  "scheme": "basic"
975}"###
976    }
977
978    test_fn! {
979        security_scheme_correct_basic_auth_change_to_digest_auth_with_description:
980        SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).scheme(HttpAuthScheme::Digest).description(String::from("digest auth")));
981        r###"{
982  "type": "http",
983  "scheme": "digest",
984  "description": "digest auth"
985}"###
986    }
987
988    test_fn! {
989        security_scheme_correct_digest_auth:
990        SecurityScheme::Http(Http::new(HttpAuthScheme::Digest));
991        r###"{
992  "type": "http",
993  "scheme": "digest"
994}"###
995    }
996
997    test_fn! {
998        security_scheme_correct_hoba_auth:
999        SecurityScheme::Http(Http::new(HttpAuthScheme::Hoba));
1000        r###"{
1001  "type": "http",
1002  "scheme": "hoba"
1003}"###
1004    }
1005
1006    test_fn! {
1007        security_scheme_correct_mutual_auth:
1008        SecurityScheme::Http(Http::new(HttpAuthScheme::Mutual));
1009        r###"{
1010  "type": "http",
1011  "scheme": "mutual"
1012}"###
1013    }
1014
1015    test_fn! {
1016        security_scheme_correct_negotiate_auth:
1017        SecurityScheme::Http(Http::new(HttpAuthScheme::Negotiate));
1018        r###"{
1019  "type": "http",
1020  "scheme": "negotiate"
1021}"###
1022    }
1023
1024    test_fn! {
1025        security_scheme_correct_oauth_auth:
1026        SecurityScheme::Http(Http::new(HttpAuthScheme::OAuth));
1027        r###"{
1028  "type": "http",
1029  "scheme": "oauth"
1030}"###
1031    }
1032
1033    test_fn! {
1034        security_scheme_correct_scram_sha1_auth:
1035        SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha1));
1036        r###"{
1037  "type": "http",
1038  "scheme": "scram-sha-1"
1039}"###
1040    }
1041
1042    test_fn! {
1043        security_scheme_correct_scram_sha256_auth:
1044        SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha256));
1045        r###"{
1046  "type": "http",
1047  "scheme": "scram-sha-256"
1048}"###
1049    }
1050
1051    test_fn! {
1052        security_scheme_correct_api_key_cookie_auth:
1053        SecurityScheme::from(ApiKey::Cookie(ApiKeyValue::new(String::from("api_key"))));
1054        r###"{
1055  "type": "apiKey",
1056  "name": "api_key",
1057  "in": "cookie"
1058}"###
1059    }
1060
1061    test_fn! {
1062        security_scheme_correct_api_key_header_auth:
1063        SecurityScheme::from(ApiKey::Header(ApiKeyValue::new("api_key")));
1064        r###"{
1065  "type": "apiKey",
1066  "name": "api_key",
1067  "in": "header"
1068}"###
1069    }
1070
1071    test_fn! {
1072        security_scheme_correct_api_key_query_auth:
1073        SecurityScheme::from(ApiKey::Query(ApiKeyValue::new(String::from("api_key"))));
1074        r###"{
1075  "type": "apiKey",
1076  "name": "api_key",
1077  "in": "query"
1078}"###
1079    }
1080
1081    test_fn! {
1082        security_scheme_correct_api_key_query_auth_with_description:
1083        SecurityScheme::from(ApiKey::Query(ApiKeyValue::with_description(String::from("api_key"), String::from("my api_key"))));
1084        r###"{
1085  "type": "apiKey",
1086  "name": "api_key",
1087  "description": "my api_key",
1088  "in": "query"
1089}"###
1090    }
1091
1092    test_fn! {
1093        security_scheme_correct_open_id_connect_auth:
1094        SecurityScheme::from(OpenIdConnect::new("https://localhost/openid"));
1095        r###"{
1096  "type": "openIdConnect",
1097  "openIdConnectUrl": "https://localhost/openid"
1098}"###
1099    }
1100
1101    test_fn! {
1102        security_scheme_correct_open_id_connect_auth_with_description:
1103        SecurityScheme::from(OpenIdConnect::with_description("https://localhost/openid", "OpenIdConnect auth"));
1104        r###"{
1105  "type": "openIdConnect",
1106  "openIdConnectUrl": "https://localhost/openid",
1107  "description": "OpenIdConnect auth"
1108}"###
1109    }
1110
1111    test_fn! {
1112        security_scheme_correct_oauth2_implicit:
1113        SecurityScheme::from(
1114            OAuth2::with_description([Flow::Implicit(
1115                Implicit::new(
1116                    "https://localhost/auth/dialog",
1117                    Scopes::from_iter([
1118                        ("edit:items", "edit my items"),
1119                        ("read:items", "read my items")
1120                    ]),
1121                ),
1122            )], "my oauth2 flow")
1123        );
1124        r###"{
1125  "type": "oauth2",
1126  "flows": {
1127    "implicit": {
1128      "authorizationUrl": "https://localhost/auth/dialog",
1129      "scopes": {
1130        "edit:items": "edit my items",
1131        "read:items": "read my items"
1132      }
1133    }
1134  },
1135  "description": "my oauth2 flow"
1136}"###
1137    }
1138
1139    test_fn! {
1140        security_scheme_correct_oauth2_implicit_with_refresh_url:
1141        SecurityScheme::from(
1142            OAuth2::with_description([Flow::Implicit(
1143                Implicit::with_refresh_url(
1144                    "https://localhost/auth/dialog",
1145                    Scopes::from_iter([
1146                        ("edit:items", "edit my items"),
1147                        ("read:items", "read my items")
1148                    ]),
1149                    "https://localhost/refresh-token"
1150                ),
1151            )], "my oauth2 flow")
1152        );
1153        r###"{
1154  "type": "oauth2",
1155  "flows": {
1156    "implicit": {
1157      "authorizationUrl": "https://localhost/auth/dialog",
1158      "refreshUrl": "https://localhost/refresh-token",
1159      "scopes": {
1160        "edit:items": "edit my items",
1161        "read:items": "read my items"
1162      }
1163    }
1164  },
1165  "description": "my oauth2 flow"
1166}"###
1167    }
1168
1169    test_fn! {
1170        security_scheme_correct_oauth2_password:
1171        SecurityScheme::OAuth2(
1172            OAuth2::with_description([Flow::Password(
1173                Password::new(
1174                    "https://localhost/oauth/token",
1175                    Scopes::from_iter([
1176                        ("edit:items", "edit my items"),
1177                        ("read:items", "read my items")
1178                    ])
1179                ),
1180            )], "my oauth2 flow")
1181        );
1182        r###"{
1183  "type": "oauth2",
1184  "flows": {
1185    "password": {
1186      "tokenUrl": "https://localhost/oauth/token",
1187      "scopes": {
1188        "edit:items": "edit my items",
1189        "read:items": "read my items"
1190      }
1191    }
1192  },
1193  "description": "my oauth2 flow"
1194}"###
1195    }
1196
1197    test_fn! {
1198        security_scheme_correct_oauth2_password_with_refresh_url:
1199        SecurityScheme::OAuth2(
1200            OAuth2::with_description([Flow::Password(
1201                Password::with_refresh_url(
1202                    "https://localhost/oauth/token",
1203                    Scopes::from_iter([
1204                        ("edit:items", "edit my items"),
1205                        ("read:items", "read my items")
1206                    ]),
1207                    "https://localhost/refresh/token"
1208                ),
1209            )], "my oauth2 flow")
1210        );
1211        r###"{
1212  "type": "oauth2",
1213  "flows": {
1214    "password": {
1215      "tokenUrl": "https://localhost/oauth/token",
1216      "refreshUrl": "https://localhost/refresh/token",
1217      "scopes": {
1218        "edit:items": "edit my items",
1219        "read:items": "read my items"
1220      }
1221    }
1222  },
1223  "description": "my oauth2 flow"
1224}"###
1225    }
1226
1227    test_fn! {
1228        security_scheme_correct_oauth2_client_credentials:
1229        SecurityScheme::OAuth2(
1230            OAuth2::new([Flow::ClientCredentials(
1231                ClientCredentials::new(
1232                    "https://localhost/oauth/token",
1233                    Scopes::from_iter([
1234                        ("edit:items", "edit my items"),
1235                        ("read:items", "read my items")
1236                    ])
1237                ),
1238            )])
1239        );
1240        r###"{
1241  "type": "oauth2",
1242  "flows": {
1243    "clientCredentials": {
1244      "tokenUrl": "https://localhost/oauth/token",
1245      "scopes": {
1246        "edit:items": "edit my items",
1247        "read:items": "read my items"
1248      }
1249    }
1250  }
1251}"###
1252    }
1253
1254    test_fn! {
1255        security_scheme_correct_oauth2_client_credentials_with_refresh_url:
1256        SecurityScheme::OAuth2(
1257            OAuth2::new([Flow::ClientCredentials(
1258                ClientCredentials::with_refresh_url(
1259                    "https://localhost/oauth/token",
1260                    Scopes::from_iter([
1261                        ("edit:items", "edit my items"),
1262                        ("read:items", "read my items")
1263                    ]),
1264                    "https://localhost/refresh/token"
1265                ),
1266            )])
1267        );
1268        r###"{
1269  "type": "oauth2",
1270  "flows": {
1271    "clientCredentials": {
1272      "tokenUrl": "https://localhost/oauth/token",
1273      "refreshUrl": "https://localhost/refresh/token",
1274      "scopes": {
1275        "edit:items": "edit my items",
1276        "read:items": "read my items"
1277      }
1278    }
1279  }
1280}"###
1281    }
1282
1283    test_fn! {
1284        security_scheme_correct_oauth2_authorization_code:
1285        SecurityScheme::OAuth2(
1286            OAuth2::new([Flow::AuthorizationCode(
1287                AuthorizationCode::with_refresh_url(
1288                    "https://localhost/authorization/token",
1289                    "https://localhost/token/url",
1290                    Scopes::from_iter([
1291                        ("edit:items", "edit my items"),
1292                        ("read:items", "read my items")
1293                    ]),
1294                    "https://localhost/refresh/token"
1295                ),
1296            )])
1297        );
1298        r###"{
1299  "type": "oauth2",
1300  "flows": {
1301    "authorizationCode": {
1302      "authorizationUrl": "https://localhost/authorization/token",
1303      "tokenUrl": "https://localhost/token/url",
1304      "refreshUrl": "https://localhost/refresh/token",
1305      "scopes": {
1306        "edit:items": "edit my items",
1307        "read:items": "read my items"
1308      }
1309    }
1310  }
1311}"###
1312    }
1313
1314    test_fn! {
1315        security_scheme_correct_oauth2_authorization_code_no_scopes:
1316        SecurityScheme::OAuth2(
1317            OAuth2::new([Flow::AuthorizationCode(
1318                AuthorizationCode::new(
1319                    "https://localhost/authorization/token",
1320                    "https://localhost/token/url",
1321                    Scopes::new()
1322                ),
1323            )])
1324        );
1325        r###"{
1326  "type": "oauth2",
1327  "flows": {
1328    "authorizationCode": {
1329      "authorizationUrl": "https://localhost/authorization/token",
1330      "tokenUrl": "https://localhost/token/url",
1331      "scopes": {}
1332    }
1333  }
1334}"###
1335    }
1336
1337    test_fn! {
1338        security_scheme_correct_oauth2_authorization_code_one_scopes:
1339        SecurityScheme::OAuth2(
1340            OAuth2::new([Flow::AuthorizationCode(
1341                AuthorizationCode::new(
1342                    "https://localhost/authorization/token",
1343                    "https://localhost/token/url",
1344                    Scopes::one("edit:items", "edit my items")
1345                ),
1346            )])
1347        );
1348        r###"{
1349  "type": "oauth2",
1350  "flows": {
1351    "authorizationCode": {
1352      "authorizationUrl": "https://localhost/authorization/token",
1353      "tokenUrl": "https://localhost/token/url",
1354      "scopes": {
1355        "edit:items": "edit my items"
1356      }
1357    }
1358  }
1359}"###
1360    }
1361
1362    test_fn! {
1363        security_scheme_correct_mutual_tls:
1364        SecurityScheme::MutualTls {
1365            description: Some(String::from("authorization is performed with client side certificate"))
1366        };
1367        r###"{
1368  "type": "mutualTLS",
1369  "description": "authorization is performed with client side certificate"
1370}"###
1371    }
1372}