use std::collections::BTreeMap;
use std::iter;
use serde::{Deserialize, Deserializer, Serialize};
use crate::PropMap;
#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, Default, Clone, PartialEq, Eq)]
pub struct SecurityRequirement {
#[serde(flatten)]
pub(crate) value: BTreeMap<String, Vec<String>>,
}
impl SecurityRequirement {
#[must_use]
pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
name: N,
scopes: S,
) -> Self {
Self {
value: BTreeMap::from_iter(iter::once_with(|| {
(
Into::<String>::into(name),
scopes
.into_iter()
.map(|scope| Into::<String>::into(scope))
.collect::<Vec<_>>(),
)
})),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
#[must_use]
pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
mut self,
name: N,
scopes: S,
) -> Self {
self.value.insert(
Into::<String>::into(name),
scopes.into_iter().map(Into::<String>::into).collect(),
);
self
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SecurityScheme {
#[serde(rename = "oauth2")]
OAuth2(OAuth2),
ApiKey(ApiKey),
Http(Http),
OpenIdConnect(OpenIdConnect),
#[serde(rename = "mutualTLS")]
MutualTls {
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
deprecated: Option<bool>,
},
}
impl From<OAuth2> for SecurityScheme {
fn from(oauth2: OAuth2) -> Self {
Self::OAuth2(oauth2)
}
}
impl From<ApiKey> for SecurityScheme {
fn from(api_key: ApiKey) -> Self {
Self::ApiKey(api_key)
}
}
impl From<OpenIdConnect> for SecurityScheme {
fn from(open_id_connect: OpenIdConnect) -> Self {
Self::OpenIdConnect(open_id_connect)
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(tag = "in", rename_all = "lowercase")]
pub enum ApiKey {
Header(ApiKeyValue),
Query(ApiKeyValue),
Cookie(ApiKeyValue),
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct ApiKeyValue {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl ApiKeyValue {
pub fn new<S: Into<String>>(name: S) -> Self {
Self {
name: name.into(),
description: None,
deprecated: None,
extensions: Default::default(),
}
}
pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
deprecated: None,
extensions: Default::default(),
}
}
#[must_use]
pub fn deprecated(mut self, deprecated: bool) -> Self {
self.deprecated = Some(deprecated);
self
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Http {
pub scheme: HttpAuthScheme,
#[serde(skip_serializing_if = "Option::is_none")]
pub bearer_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl Http {
#[must_use]
pub fn new(scheme: HttpAuthScheme) -> Self {
Self {
scheme,
bearer_format: None,
description: None,
deprecated: None,
extensions: Default::default(),
}
}
#[must_use]
pub fn scheme(mut self, scheme: HttpAuthScheme) -> Self {
self.scheme = scheme;
self
}
#[must_use]
pub fn bearer_format<S: Into<String>>(mut self, bearer_format: S) -> Self {
if self.scheme == HttpAuthScheme::Bearer {
self.bearer_format = Some(bearer_format.into());
}
self
}
#[must_use]
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn deprecated(mut self, deprecated: bool) -> Self {
self.deprecated = Some(deprecated);
self
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum HttpAuthScheme {
#[default]
Basic,
Bearer,
Digest,
Hoba,
Mutual,
Negotiate,
OAuth,
#[serde(rename = "scram-sha-1")]
ScramSha1,
#[serde(rename = "scram-sha-256")]
ScramSha256,
Vapid,
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OpenIdConnect {
pub open_id_connect_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl OpenIdConnect {
pub fn new<S: Into<String>>(open_id_connect_url: S) -> Self {
Self {
open_id_connect_url: open_id_connect_url.into(),
description: None,
deprecated: None,
extensions: Default::default(),
}
}
pub fn with_description<S: Into<String>>(open_id_connect_url: S, description: S) -> Self {
Self {
open_id_connect_url: open_id_connect_url.into(),
description: Some(description.into()),
deprecated: None,
extensions: Default::default(),
}
}
#[must_use]
pub fn deprecated(mut self, deprecated: bool) -> Self {
self.deprecated = Some(deprecated);
self
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OAuth2 {
#[serde(deserialize_with = "deserialize_flows")]
pub flows: PropMap<String, Flow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "oauth2MetadataUrl", skip_serializing_if = "Option::is_none")]
pub oauth2_metadata_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl OAuth2 {
pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
Self {
flows: PropMap::from_iter(
flows
.into_iter()
.map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
),
description: None,
oauth2_metadata_url: None,
deprecated: None,
extensions: Default::default(),
}
}
pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(
flows: I,
description: S,
) -> Self {
Self {
flows: PropMap::from_iter(
flows
.into_iter()
.map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
),
description: Some(description.into()),
oauth2_metadata_url: None,
deprecated: None,
extensions: Default::default(),
}
}
#[must_use]
pub fn oauth2_metadata_url<S: Into<String>>(mut self, oauth2_metadata_url: S) -> Self {
self.oauth2_metadata_url = Some(oauth2_metadata_url.into());
self
}
#[must_use]
pub fn deprecated(mut self, deprecated: bool) -> Self {
self.deprecated = Some(deprecated);
self
}
}
fn deserialize_flows<'de, D>(deserializer: D) -> Result<PropMap<String, Flow>, D::Error>
where
D: Deserializer<'de>,
{
fn flow<T, E>(name: &str, value: serde_json::Value) -> Result<T, E>
where
T: serde::de::DeserializeOwned,
E: serde::de::Error,
{
serde_json::from_value(value)
.map_err(|e| E::custom(format!("invalid `{name}` oauth2 flow: {e}")))
}
let raw = PropMap::<String, serde_json::Value>::deserialize(deserializer)?;
let mut flows = PropMap::new();
for (key, value) in raw {
let parsed = match &*key {
"implicit" => Flow::Implicit(flow(&key, value)?),
"password" => Flow::Password(flow(&key, value)?),
"clientCredentials" => Flow::ClientCredentials(flow(&key, value)?),
"authorizationCode" => Flow::AuthorizationCode(flow(&key, value)?),
"deviceAuthorization" => Flow::DeviceAuthorization(flow(&key, value)?),
_ => flow(&key, value)?,
};
flows.insert(key, parsed);
}
Ok(flows)
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(untagged)]
pub enum Flow {
DeviceAuthorization(DeviceAuthorization),
Implicit(Implicit),
Password(Password),
ClientCredentials(ClientCredentials),
AuthorizationCode(AuthorizationCode),
}
impl Flow {
fn get_type_as_str(&self) -> &str {
match self {
Self::DeviceAuthorization(_) => "deviceAuthorization",
Self::Implicit(_) => "implicit",
Self::Password(_) => "password",
Self::ClientCredentials(_) => "clientCredentials",
Self::AuthorizationCode(_) => "authorizationCode",
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Implicit {
pub authorization_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl Implicit {
pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
Self {
authorization_url: authorization_url.into(),
refresh_url: None,
scopes,
extensions: Default::default(),
}
}
pub fn with_refresh_url<S: Into<String>>(
authorization_url: S,
scopes: Scopes,
refresh_url: S,
) -> Self {
Self {
authorization_url: authorization_url.into(),
refresh_url: Some(refresh_url.into()),
scopes,
extensions: Default::default(),
}
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationCode {
pub authorization_url: String,
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl AuthorizationCode {
pub fn new<A: Into<String>, T: Into<String>>(
authorization_url: A,
token_url: T,
scopes: Scopes,
) -> Self {
Self {
authorization_url: authorization_url.into(),
token_url: token_url.into(),
refresh_url: None,
scopes,
extensions: Default::default(),
}
}
pub fn with_refresh_url<S: Into<String>>(
authorization_url: S,
token_url: S,
scopes: Scopes,
refresh_url: S,
) -> Self {
Self {
authorization_url: authorization_url.into(),
token_url: token_url.into(),
refresh_url: Some(refresh_url.into()),
scopes,
extensions: Default::default(),
}
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Password {
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl Password {
pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
Self {
token_url: token_url.into(),
refresh_url: None,
scopes,
extensions: Default::default(),
}
}
pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
Self {
token_url: token_url.into(),
refresh_url: Some(refresh_url.into()),
scopes,
extensions: Default::default(),
}
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ClientCredentials {
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl ClientCredentials {
pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
Self {
token_url: token_url.into(),
refresh_url: None,
scopes,
extensions: Default::default(),
}
}
pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
Self {
token_url: token_url.into(),
refresh_url: Some(refresh_url.into()),
scopes,
extensions: Default::default(),
}
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DeviceAuthorization {
pub device_authorization_url: String,
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
#[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
pub extensions: PropMap<String, serde_json::Value>,
}
impl DeviceAuthorization {
pub fn new<S: Into<String>>(device_authorization_url: S, token_url: S, scopes: Scopes) -> Self {
Self {
device_authorization_url: device_authorization_url.into(),
token_url: token_url.into(),
refresh_url: None,
scopes,
extensions: Default::default(),
}
}
pub fn with_refresh_url<S: Into<String>>(
device_authorization_url: S,
token_url: S,
scopes: Scopes,
refresh_url: S,
) -> Self {
Self {
device_authorization_url: device_authorization_url.into(),
token_url: token_url.into(),
refresh_url: Some(refresh_url.into()),
scopes,
extensions: Default::default(),
}
}
#[must_use]
pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
self.extensions = extensions;
self
}
}
#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct Scopes {
scopes: PropMap<String, String>,
}
impl Scopes {
#[must_use]
pub fn new() -> Self {
Default::default()
}
#[must_use]
pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
Self {
scopes: PropMap::from_iter(iter::once_with(|| (scope.into(), description.into()))),
}
}
}
impl<I> FromIterator<(I, I)> for Scopes
where
I: Into<String>,
{
fn from_iter<T: IntoIterator<Item = (I, I)>>(iter: T) -> Self {
Self {
scopes: iter
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_fn {
($name:ident : $schema:expr; $expected:literal) => {
#[test]
fn $name() {
let value = serde_json::to_value($schema).unwrap();
let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap();
assert_eq!(
value,
expected_value,
"testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}",
stringify!($name),
value,
expected_value
);
println!("{}", &serde_json::to_string_pretty(&$schema).unwrap());
}
};
}
test_fn! {
security_scheme_correct_default_http_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::default()));
r###"{
"type": "http",
"scheme": "basic"
}"###
}
test_fn! {
security_scheme_correct_http_bearer_json:
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT"));
r###"{
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}"###
}
test_fn! {
security_scheme_correct_basic_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
r###"{
"type": "http",
"scheme": "basic"
}"###
}
test_fn! {
security_scheme_correct_basic_auth_change_to_digest_auth_with_description:
SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).scheme(HttpAuthScheme::Digest).description(String::from("digest auth")));
r###"{
"type": "http",
"scheme": "digest",
"description": "digest auth"
}"###
}
test_fn! {
security_scheme_correct_digest_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::Digest));
r###"{
"type": "http",
"scheme": "digest"
}"###
}
test_fn! {
security_scheme_correct_hoba_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::Hoba));
r###"{
"type": "http",
"scheme": "hoba"
}"###
}
test_fn! {
security_scheme_correct_mutual_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::Mutual));
r###"{
"type": "http",
"scheme": "mutual"
}"###
}
test_fn! {
security_scheme_correct_negotiate_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::Negotiate));
r###"{
"type": "http",
"scheme": "negotiate"
}"###
}
test_fn! {
security_scheme_correct_oauth_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::OAuth));
r###"{
"type": "http",
"scheme": "oauth"
}"###
}
test_fn! {
security_scheme_correct_scram_sha1_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha1));
r###"{
"type": "http",
"scheme": "scram-sha-1"
}"###
}
test_fn! {
security_scheme_correct_scram_sha256_auth:
SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha256));
r###"{
"type": "http",
"scheme": "scram-sha-256"
}"###
}
test_fn! {
security_scheme_correct_api_key_cookie_auth:
SecurityScheme::from(ApiKey::Cookie(ApiKeyValue::new(String::from("api_key"))));
r###"{
"type": "apiKey",
"name": "api_key",
"in": "cookie"
}"###
}
test_fn! {
security_scheme_correct_api_key_header_auth:
SecurityScheme::from(ApiKey::Header(ApiKeyValue::new("api_key")));
r###"{
"type": "apiKey",
"name": "api_key",
"in": "header"
}"###
}
test_fn! {
security_scheme_correct_api_key_query_auth:
SecurityScheme::from(ApiKey::Query(ApiKeyValue::new(String::from("api_key"))));
r###"{
"type": "apiKey",
"name": "api_key",
"in": "query"
}"###
}
test_fn! {
security_scheme_correct_api_key_query_auth_with_description:
SecurityScheme::from(ApiKey::Query(ApiKeyValue::with_description(String::from("api_key"), String::from("my api_key"))));
r###"{
"type": "apiKey",
"name": "api_key",
"description": "my api_key",
"in": "query"
}"###
}
test_fn! {
security_scheme_correct_open_id_connect_auth:
SecurityScheme::from(OpenIdConnect::new("https://localhost/openid"));
r###"{
"type": "openIdConnect",
"openIdConnectUrl": "https://localhost/openid"
}"###
}
test_fn! {
security_scheme_correct_open_id_connect_auth_with_description:
SecurityScheme::from(OpenIdConnect::with_description("https://localhost/openid", "OpenIdConnect auth"));
r###"{
"type": "openIdConnect",
"openIdConnectUrl": "https://localhost/openid",
"description": "OpenIdConnect auth"
}"###
}
test_fn! {
security_scheme_correct_oauth2_implicit:
SecurityScheme::from(
OAuth2::with_description([Flow::Implicit(
Implicit::new(
"https://localhost/auth/dialog",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
]),
),
)], "my oauth2 flow")
);
r###"{
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "https://localhost/auth/dialog",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
},
"description": "my oauth2 flow"
}"###
}
test_fn! {
security_scheme_correct_oauth2_implicit_with_refresh_url:
SecurityScheme::from(
OAuth2::with_description([Flow::Implicit(
Implicit::with_refresh_url(
"https://localhost/auth/dialog",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
]),
"https://localhost/refresh-token"
),
)], "my oauth2 flow")
);
r###"{
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "https://localhost/auth/dialog",
"refreshUrl": "https://localhost/refresh-token",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
},
"description": "my oauth2 flow"
}"###
}
test_fn! {
security_scheme_correct_oauth2_password:
SecurityScheme::OAuth2(
OAuth2::with_description([Flow::Password(
Password::new(
"https://localhost/oauth/token",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
])
),
)], "my oauth2 flow")
);
r###"{
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "https://localhost/oauth/token",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
},
"description": "my oauth2 flow"
}"###
}
test_fn! {
security_scheme_correct_oauth2_password_with_refresh_url:
SecurityScheme::OAuth2(
OAuth2::with_description([Flow::Password(
Password::with_refresh_url(
"https://localhost/oauth/token",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
]),
"https://localhost/refresh/token"
),
)], "my oauth2 flow")
);
r###"{
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "https://localhost/oauth/token",
"refreshUrl": "https://localhost/refresh/token",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
},
"description": "my oauth2 flow"
}"###
}
test_fn! {
security_scheme_correct_oauth2_client_credentials:
SecurityScheme::OAuth2(
OAuth2::new([Flow::ClientCredentials(
ClientCredentials::new(
"https://localhost/oauth/token",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
])
),
)])
);
r###"{
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "https://localhost/oauth/token",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
}
}"###
}
test_fn! {
security_scheme_correct_oauth2_client_credentials_with_refresh_url:
SecurityScheme::OAuth2(
OAuth2::new([Flow::ClientCredentials(
ClientCredentials::with_refresh_url(
"https://localhost/oauth/token",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
]),
"https://localhost/refresh/token"
),
)])
);
r###"{
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "https://localhost/oauth/token",
"refreshUrl": "https://localhost/refresh/token",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
}
}"###
}
test_fn! {
security_scheme_correct_oauth2_authorization_code:
SecurityScheme::OAuth2(
OAuth2::new([Flow::AuthorizationCode(
AuthorizationCode::with_refresh_url(
"https://localhost/authorization/token",
"https://localhost/token/url",
Scopes::from_iter([
("edit:items", "edit my items"),
("read:items", "read my items")
]),
"https://localhost/refresh/token"
),
)])
);
r###"{
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://localhost/authorization/token",
"tokenUrl": "https://localhost/token/url",
"refreshUrl": "https://localhost/refresh/token",
"scopes": {
"edit:items": "edit my items",
"read:items": "read my items"
}
}
}
}"###
}
test_fn! {
security_scheme_correct_oauth2_authorization_code_no_scopes:
SecurityScheme::OAuth2(
OAuth2::new([Flow::AuthorizationCode(
AuthorizationCode::new(
"https://localhost/authorization/token",
"https://localhost/token/url",
Scopes::new()
),
)])
);
r###"{
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://localhost/authorization/token",
"tokenUrl": "https://localhost/token/url",
"scopes": {}
}
}
}"###
}
test_fn! {
security_scheme_correct_oauth2_authorization_code_one_scopes:
SecurityScheme::OAuth2(
OAuth2::new([Flow::AuthorizationCode(
AuthorizationCode::new(
"https://localhost/authorization/token",
"https://localhost/token/url",
Scopes::one("edit:items", "edit my items")
),
)])
);
r###"{
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://localhost/authorization/token",
"tokenUrl": "https://localhost/token/url",
"scopes": {
"edit:items": "edit my items"
}
}
}
}"###
}
test_fn! {
security_scheme_correct_mutual_tls:
SecurityScheme::MutualTls {
description: Some(String::from("authorization is performed with client side certificate")),
deprecated: None
};
r###"{
"type": "mutualTLS",
"description": "authorization is performed with client side certificate"
}"###
}
#[test]
fn security_requirement_accepts_uri_references() {
let requirement = SecurityRequirement::new("api_key", Vec::<String>::new())
.add("./foo", ["read:items"])
.add("https://example.com/schemes.json#/oauth", ["write:items"]);
let value = serde_json::to_value(&requirement).expect("serialize");
assert_eq!(
value,
serde_json::json!({
"api_key": [],
"./foo": ["read:items"],
"https://example.com/schemes.json#/oauth": ["write:items"]
})
);
let parsed: SecurityRequirement = serde_json::from_value(value).expect("deserialize");
assert_eq!(parsed, requirement);
}
#[test]
fn device_authorization_flow_round_trips_under_its_own_key() {
let scheme = SecurityScheme::OAuth2(OAuth2::new([Flow::DeviceAuthorization(
DeviceAuthorization::with_refresh_url(
"https://localhost/device_authorization",
"https://localhost/token",
Scopes::one("edit:items", "edit my items"),
"https://localhost/refresh",
),
)]));
let value = serde_json::to_value(&scheme).expect("serialize");
assert_eq!(
value,
serde_json::json!({
"type": "oauth2",
"flows": {
"deviceAuthorization": {
"deviceAuthorizationUrl": "https://localhost/device_authorization",
"tokenUrl": "https://localhost/token",
"refreshUrl": "https://localhost/refresh",
"scopes": { "edit:items": "edit my items" }
}
}
})
);
let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
assert_eq!(parsed, scheme);
}
#[test]
fn oauth2_metadata_url_and_deprecated_serialize() {
let scheme = SecurityScheme::OAuth2(
OAuth2::new([Flow::ClientCredentials(ClientCredentials::new(
"https://localhost/token",
Scopes::new(),
))])
.oauth2_metadata_url("https://localhost/.well-known/oauth-authorization-server")
.deprecated(true),
);
let value = serde_json::to_value(&scheme).expect("serialize");
assert_eq!(
value["oauth2MetadataUrl"],
serde_json::json!("https://localhost/.well-known/oauth-authorization-server")
);
assert_eq!(value["deprecated"], serde_json::json!(true));
let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
assert_eq!(parsed, scheme);
}
#[test]
fn deprecated_is_available_on_every_scheme_kind() {
for scheme in [
SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).deprecated(true)),
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("api_key").deprecated(true))),
SecurityScheme::OpenIdConnect(
OpenIdConnect::new("https://localhost/openid").deprecated(true),
),
SecurityScheme::MutualTls {
description: None,
deprecated: Some(true),
},
] {
let value = serde_json::to_value(&scheme).expect("serialize");
assert_eq!(
value["deprecated"],
serde_json::json!(true),
"deprecated missing from {value}"
);
let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
assert_eq!(parsed, scheme);
}
}
}