Repository: juhaku/utoipa (Rust)

Issue:
Feature request: add support for adding multiple security requirements.
As described in https://swagger.io/docs/specification/authentication/:

Some REST APIs support several authentication types. The security section lets you combine the security requirements using logical OR and AND to achieve the desired result. security uses the following logic:
```
security:    # A OR B
  - A
  - B
security:    # A AND B
  - A
    B
security:    # (A AND B) OR (C AND D)
  - A
    B
  - C
    D
```

I think, the previous example can be implemented this way:

```rs
#[utoipa::path(
  ..,
  security(
    ("a" = ["test"], "b" = ["test"]), // (A AND B) OR (C AND D)
    ("c" = [], "d" = []),
  ),
  ..
)
async fn test() {}
```


Discussion:
Sure this could be added in future releases. The discussion earlier related to this https://github.com/juhaku/utoipa/discussions/625 seemingly indicates that there is a valid need for this support. 
I'll take it. Already implemented the codegen part.



Patch:
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index e6f0894c..a8ae754a 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -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.
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<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>,
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<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>,
 }
 
@@ -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<'_> {
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<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)
@@ -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)
+                });
+            }
         }
     }
 }
diff --git a/utoipa/src/openapi/path.rs b/utoipa/src/openapi/path.rs
index c81acc4f..2a302f22 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 f5011d99..ca920215 100644
--- a/utoipa/src/openapi/security.rs
+++ b/utoipa/src/openapi/security.rs
@@ -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,
@@ -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>>(
+        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.


Test patch:
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" = [])
     )
 )]