diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index aede1e90..9f40583e 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -1010,11 +1010,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. diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs index e24c3f94..ceb5928d 100644 --- a/utoipa-gen/src/openapi.rs +++ b/utoipa-gen/src/openapi.rs @@ -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, }; @@ -27,7 +27,7 @@ pub struct OpenApiAttr<'o> { paths: Punctuated, components: Components, modifiers: Punctuated, - security: Option>, + security: Option>, tags: Option>, external_docs: Option, servers: Punctuated, diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index abc9894f..e68a4c48 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -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}; @@ -37,7 +37,7 @@ pub struct PathAttr<'p> { operation_id: Option, tag: Option, params: Vec>, - security: Option>, + security: Option>, context_path: Option, } @@ -421,7 +421,7 @@ struct Operation<'a> { parameters: &'a Vec>, request_body: Option<&'a RequestBody<'a>>, responses: &'a Vec>, - security: Option<&'a Array<'a, SecurityRequirementAttr>>, + security: Option<&'a Array<'a, SecurityRequirementsAttr>>, } impl ToTokens for Operation<'_> { diff --git a/utoipa-gen/src/security_requirement.rs b/utoipa-gen/src/security_requirement.rs index a5934725..62bfa51d 100644 --- a/utoipa-gen/src/security_requirement.rs +++ b/utoipa-gen/src/security_requirement.rs @@ -12,23 +12,31 @@ use crate::Array; #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] -pub struct SecurityRequirementAttr { - name: Option, - scopes: Option>, +pub struct SecurityRequirementsAttrItem { + pub name: Option, + pub scopes: Option>, } -impl Parse for SecurityRequirementAttr { +#[derive(Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct SecurityRequirementsAttr(Punctuated); + +impl Parse for SecurityRequirementsAttr { + fn parse(input: ParseStream) -> syn::Result { + Punctuated::::parse_terminated(input) + .map(|o| Self(o.into_iter().collect())) + } +} + +impl Parse for SecurityRequirementsAttrItem { fn parse(input: ParseStream) -> syn::Result { - if input.is_empty() { - return Ok(Self { - ..Default::default() - }); - } let name = input.parse::()?.value(); + input.parse::()?; let scopes_stream; bracketed!(scopes_stream in input); + let scopes = Punctuated::::parse_terminated(&scopes_stream)? .iter() .map(LitStr::value) @@ -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::>(); - 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::>(); + let scopes_len = scopes.len(); + + tokens.extend(quote! { + .add::<&str, [&str; #scopes_len], &str>(#name, #scopes) + }); + } } } } diff --git a/utoipa-gen/tests/openapi_derive.rs b/utoipa-gen/tests/openapi_derive.rs index a13c035f..5c6b3004 100644 --- a/utoipa-gen/tests/openapi_derive.rs +++ b/utoipa-gen/tests/openapi_derive.rs @@ -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; @@ -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" } } diff --git a/utoipa-gen/tests/openapi_derive_test.rs b/utoipa-gen/tests/openapi_derive_test.rs index 162e0857..7d82eb69 100644 --- a/utoipa-gen/tests/openapi_derive_test.rs +++ b/utoipa-gen/tests/openapi_derive_test.rs @@ -37,7 +37,7 @@ mod pet_api { ), security( (), - ("my_auth" = ["read:items", "edit:items"]), + ("my_auth1" = ["read:items", "edit:items"], "my_auth2" = ["read:items"]), ("token_jwt" = []) ) )] @@ -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" = []) ) )] diff --git a/utoipa/src/openapi/path.rs b/utoipa/src/openapi/path.rs index 17b6f6d4..5230cdd2 100644 --- a/utoipa/src/openapi/path.rs +++ b/utoipa/src/openapi/path.rs @@ -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"))] @@ -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()); diff --git a/utoipa/src/openapi/security.rs b/utoipa/src/openapi/security.rs index dabf74b8..aa5fffb7 100644 --- a/utoipa/src/openapi/security.rs +++ b/utoipa/src/openapi/security.rs @@ -54,6 +54,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, S: IntoIterator, I: Into>( name: N, scopes: S, @@ -70,6 +73,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`] 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, S: IntoIterator, I: Into>( + mut self, + name: N, + scopes: S, + ) -> Self { + self.value.insert( + Into::::into(name), + scopes.into_iter().map(Into::::into).collect(), + ); + + self + } } /// OpenAPI [security scheme][security] for path operations.