use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap},
iter,
};
#[non_exhaustive]
#[derive(Serialize, Deserialize, Default, Ord, PartialOrd, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct SecurityRequirement {
#[serde(flatten)]
value: BTreeMap<String, Vec<String>>,
}
impl SecurityRequirement {
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<_>>(),
)
})),
}
}
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
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, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
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>,
},
}
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)]
#[serde(tag = "in", rename_all = "lowercase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub enum ApiKey {
Header(ApiKeyValue),
Query(ApiKeyValue),
Cookie(ApiKeyValue),
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct ApiKeyValue {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl ApiKeyValue {
pub fn new<S: Into<String>>(name: S) -> Self {
Self {
name: name.into(),
description: None,
}
}
pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
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>,
}
impl Http {
pub fn new(scheme: HttpAuthScheme) -> Self {
Self {
scheme,
bearer_format: None,
description: None,
}
}
pub fn scheme(mut self, scheme: HttpAuthScheme) -> Self {
self.scheme = scheme;
self
}
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
}
pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
self.description = description.map(|description| description.into());
self
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "lowercase")]
pub enum HttpAuthScheme {
Basic,
Bearer,
Digest,
Hoba,
Mutual,
Negotiate,
OAuth,
#[serde(rename = "scram-sha-1")]
ScramSha1,
#[serde(rename = "scram-sha-256")]
ScramSha256,
Vapid,
}
impl Default for HttpAuthScheme {
fn default() -> Self {
Self::Basic
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct OpenIdConnect {
pub open_id_connect_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
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,
}
}
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()),
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct OAuth2 {
pub flows: BTreeMap<String, Flow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", flatten)]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
impl OAuth2 {
pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
Self {
flows: BTreeMap::from_iter(
flows
.into_iter()
.map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
),
extensions: None,
description: None,
}
}
pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(
flows: I,
description: S,
) -> Self {
Self {
flows: BTreeMap::from_iter(
flows
.into_iter()
.map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
),
extensions: None,
description: Some(description.into()),
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(untagged)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub enum Flow {
Implicit(Implicit),
Password(Password),
ClientCredentials(ClientCredentials),
AuthorizationCode(AuthorizationCode),
}
impl Flow {
fn get_type_as_str(&self) -> &str {
match self {
Self::Implicit(_) => "implicit",
Self::Password(_) => "password",
Self::ClientCredentials(_) => "clientCredentials",
Self::AuthorizationCode(_) => "authorizationCode",
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Implicit {
pub authorization_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
}
impl Implicit {
pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
Self {
authorization_url: authorization_url.into(),
refresh_url: None,
scopes,
}
}
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,
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
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,
}
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,
}
}
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,
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Password {
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
}
impl Password {
pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
Self {
token_url: token_url.into(),
refresh_url: None,
scopes,
}
}
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,
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct ClientCredentials {
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(flatten)]
pub scopes: Scopes,
}
impl ClientCredentials {
pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
Self {
token_url: token_url.into(),
refresh_url: None,
scopes,
}
}
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,
}
}
}
#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Scopes {
scopes: BTreeMap<String, String>,
}
impl Scopes {
pub fn new() -> Self {
Self {
..Default::default()
}
}
pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
Self {
scopes: BTreeMap::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(Some(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"))
};
r###"{
"type": "mutualTLS",
"description": "authorization is performed with client side certificate"
}"###
}
}