Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for specifying multiple security requirement keys #813

Merged
merged 7 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,11 +1009,23 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// (),
/// ("name" = []),
/// ("name" = ["scope1", "scope2"]),
/// ("name" = ["scope1", "scope2"], "name2" = []),
/// ```
///
/// Leaving empty _`()`_ creates an empty [`SecurityRequirement`][security] this is useful when
/// security requirement is optional for operation.
///
/// You can define multiple security requirements within same parenthesis seperated by comma. This
/// allows you to define keys that must be simultaneously provided for the endpoint / API.
///
/// _**Following could be explained as: Security is optional and if provided it must either contain
/// `api_key` or `key AND key2`.**_
/// ```text
/// (),
/// ("api_key" = []),
/// ("key" = [], "key2" = []),
/// ```
///
/// # actix_extras feature support for actix-web
///
/// **actix_extras** feature gives **utoipa** ability to parse path operation information from **actix-web** types and macros.
Expand Down
4 changes: 2 additions & 2 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned, ToTokens};

use crate::{
parse_utils, path::PATH_STRUCT_PREFIX, security_requirement::SecurityRequirementAttr, Array,
parse_utils, path::PATH_STRUCT_PREFIX, security_requirement::SecurityRequirementsAttr, Array,
ExternalDocs, ResultExt,
};

Expand All @@ -27,7 +27,7 @@ pub struct OpenApiAttr<'o> {
paths: Punctuated<ExprPath, Comma>,
components: Components,
modifiers: Punctuated<Modifier, Comma>,
security: Option<Array<'static, SecurityRequirementAttr>>,
security: Option<Array<'static, SecurityRequirementsAttr>>,
tags: Option<Array<'static, Tag>>,
external_docs: Option<ExternalDocs>,
servers: Punctuated<Server, Comma>,
Expand Down
6 changes: 3 additions & 3 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use syn::{Expr, ExprLit, Lit, LitStr, Type};
use crate::component::{GenericType, TypeTree};
use crate::path::request_body::RequestBody;
use crate::{parse_utils, Deprecated};
use crate::{schema_type::SchemaType, security_requirement::SecurityRequirementAttr, Array};
use crate::{schema_type::SchemaType, security_requirement::SecurityRequirementsAttr, Array};

use self::response::Response;
use self::{parameter::Parameter, request_body::RequestBodyAttr, response::Responses};
Expand All @@ -37,7 +37,7 @@ pub struct PathAttr<'p> {
operation_id: Option<Expr>,
tag: Option<parse_utils::Value>,
params: Vec<Parameter<'p>>,
security: Option<Array<'p, SecurityRequirementAttr>>,
security: Option<Array<'p, SecurityRequirementsAttr>>,
context_path: Option<parse_utils::Value>,
}

Expand Down Expand Up @@ -421,7 +421,7 @@ struct Operation<'a> {
parameters: &'a Vec<Parameter<'a>>,
request_body: Option<&'a RequestBody<'a>>,
responses: &'a Vec<Response<'a>>,
security: Option<&'a Array<'a, SecurityRequirementAttr>>,
security: Option<&'a Array<'a, SecurityRequirementsAttr>>,
}

impl ToTokens for Operation<'_> {
Expand Down
52 changes: 31 additions & 21 deletions utoipa-gen/src/security_requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,31 @@ use crate::Array;

#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct SecurityRequirementAttr {
name: Option<String>,
scopes: Option<Vec<String>>,
pub struct SecurityRequirementsAttrItem {
pub name: Option<String>,
pub scopes: Option<Vec<String>>,
}

impl Parse for SecurityRequirementAttr {
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct SecurityRequirementsAttr(Punctuated<SecurityRequirementsAttrItem, Comma>);

impl Parse for SecurityRequirementsAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
Punctuated::<SecurityRequirementsAttrItem, Comma>::parse_terminated(input)
.map(|o| Self(o.into_iter().collect()))
}
}

impl Parse for SecurityRequirementsAttrItem {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(Self {
..Default::default()
});
}
let name = input.parse::<LitStr>()?.value();

input.parse::<Token![=]>()?;

let scopes_stream;
bracketed!(scopes_stream in input);

let scopes = Punctuated::<LitStr, Comma>::parse_terminated(&scopes_stream)?
.iter()
.map(LitStr::value)
Expand All @@ -41,19 +49,21 @@ impl Parse for SecurityRequirementAttr {
}
}

impl ToTokens for SecurityRequirementAttr {
impl ToTokens for SecurityRequirementsAttr {
fn to_tokens(&self, tokens: &mut TokenStream) {
if let (Some(name), Some(scopes)) = (&self.name, &self.scopes) {
let scopes_array = scopes.iter().collect::<Array<&String>>();
let scopes_len = scopes.len();

tokens.extend(quote! {
utoipa::openapi::security::SecurityRequirement::new::<&str, [&str; #scopes_len], &str>(#name, #scopes_array)
})
} else {
tokens.extend(quote! {
utoipa::openapi::security::SecurityRequirement::default()
})
tokens.extend(quote! {
utoipa::openapi::security::SecurityRequirement::default()
});

for requirement in &self.0 {
if let (Some(name), Some(scopes)) = (&requirement.name, &requirement.scopes) {
let scopes = scopes.iter().collect::<Array<&String>>();
let scopes_len = scopes.len();

tokens.extend(quote! {
.add::<&str, [&str; #scopes_len], &str>(#name, #scopes)
});
}
}
}
}
5 changes: 4 additions & 1 deletion utoipa-gen/tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ fn derive_openapi_with_security_requirement() {
#[openapi(security(
(),
("my_auth" = ["read:items", "edit:items"]),
("token_jwt" = [])
("token_jwt" = []),
("api_key1" = [], "api_key2" = []),
))]
struct ApiDoc;

Expand All @@ -28,6 +29,8 @@ fn derive_openapi_with_security_requirement() {
"security.[1].my_auth.[0]" = r###""read:items""###, "api_oauth first scope"
"security.[1].my_auth.[1]" = r###""edit:items""###, "api_oauth second scope"
"security.[2].token_jwt" = "[]", "jwt_token auth scopes"
"security.[3].api_key1" = "[]", "api_key1 auth scopes"
"security.[3].api_key2" = "[]", "api_key2 auth scopes"
}
}

Expand Down
4 changes: 2 additions & 2 deletions utoipa-gen/tests/openapi_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ mod pet_api {
),
security(
(),
("my_auth" = ["read:items", "edit:items"]),
("my_auth1" = ["read:items", "edit:items"], "my_auth2" = ["read:items"]),
abs0luty marked this conversation as resolved.
Show resolved Hide resolved
("token_jwt" = [])
)
)]
Expand All @@ -58,7 +58,7 @@ mod pet_api {
modifiers(&Foo),
security(
(),
("my_auth" = ["read:items", "edit:items"]),
("my_auth1" = ["read:items", "edit:items"], "my_auth2" = ["read:items"]),
("token_jwt" = [])
)
)]
Expand Down
7 changes: 4 additions & 3 deletions utoipa/src/openapi/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use super::{
builder,
request_body::RequestBody,
response::{Response, Responses},
set_value, Deprecated, ExternalDocs, RefOr, Required, Schema, SecurityRequirement, Server,
security::SecurityRequirement,
set_value, Deprecated, ExternalDocs, RefOr, Required, Schema, Server,
};

#[cfg(not(feature = "preserve_path_order"))]
Expand Down Expand Up @@ -773,8 +774,8 @@ mod tests {
SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]);
let operation = OperationBuilder::new()
.security(security_requirement1)
.security(security_requirement2)
.security(security_requirement1.into())
.security(security_requirement2.into())
.build();

assert!(operation.security.is_some());
Expand Down
66 changes: 66 additions & 0 deletions utoipa/src/openapi/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ impl SecurityRequirement {
/// # use utoipa::openapi::security::SecurityRequirement;
/// SecurityRequirement::default();
/// ```
///
/// If you have more than one name in the security requirement you can use
/// [`SecurityRequirement::add`].
pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
name: N,
scopes: S,
Expand All @@ -69,6 +72,69 @@ impl SecurityRequirement {
})),
}
}

/// Allows to add multiple names to security requirement.
///
/// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
/// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
/// Scopes must match to the ones defined in [`SecurityScheme`].
///
/// # Examples
///
/// Make both API keys required:
/// ```rust
/// # use utoipa::openapi::security::{SecurityRequirement, HttpAuthScheme, HttpBuilder, SecurityScheme};
/// # use utoipa::{openapi, Modify, OpenApi};
/// # use serde::Serialize;
/// #[derive(Debug, Serialize)]
/// struct Foo;
///
/// impl Modify for Foo {
/// fn modify(&self, openapi: &mut openapi::OpenApi) {
/// if let Some(schema) = openapi.components.as_mut() {
/// schema.add_security_scheme(
/// "api_key1",
/// SecurityScheme::Http(
/// HttpBuilder::new()
/// .scheme(HttpAuthScheme::Bearer)
/// .bearer_format("JWT")
/// .build(),
/// ),
/// );
/// schema.add_security_scheme(
/// "api_key2",
/// SecurityScheme::Http(
/// HttpBuilder::new()
/// .scheme(HttpAuthScheme::Bearer)
/// .bearer_format("JWT")
/// .build(),
/// ),
/// );
/// }
/// }
/// }
///
/// #[derive(Default, OpenApi)]
/// #[openapi(
/// modifiers(&Foo),
/// security(
/// ("api_key1" = ["edit:items", "read:items"], "api_key2" = ["edit:items", "read:items"]),
/// )
/// )]
/// struct ApiDoc;
/// ```
pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
abs0luty marked this conversation as resolved.
Show resolved Hide resolved
mut self,
name: N,
scopes: S,
) -> Self {
self.value.insert(
Into::<String>::into(name),
scopes.into_iter().map(Into::<String>::into).collect(),
);

self
}
}

/// OpenAPI [security scheme][security] for path operations.
Expand Down