From 32a8059fb581ef9065e39bf0e8e61ac88eb23717 Mon Sep 17 00:00:00 2001 From: Adi Salimgereev Date: Sat, 2 Dec 2023 11:18:24 +0600 Subject: [PATCH 1/7] Implement code generation part --- utoipa-gen/src/openapi.rs | 4 +- utoipa-gen/src/path.rs | 6 +- utoipa-gen/src/security_requirement.rs | 83 +++++++++++++++++--------- utoipa/src/openapi/path.rs | 12 ++-- utoipa/src/openapi/security.rs | 21 ++++++- 5 files changed, 85 insertions(+), 41 deletions(-) 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..2b6d33e9 100644 --- a/utoipa-gen/src/security_requirement.rs +++ b/utoipa-gen/src/security_requirement.rs @@ -12,48 +12,73 @@ use crate::Array; #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] -pub struct SecurityRequirementAttr { - name: Option, - scopes: Option>, +pub struct SecurityRequirementsAttrItem { + pub name: String, + pub scopes: Vec, } -impl Parse for SecurityRequirementAttr { +#[derive(Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct SecurityRequirementsAttr(Vec); + +impl Parse for SecurityRequirementsAttr { fn parse(input: ParseStream) -> syn::Result { + let mut items = Vec::new(); + if input.is_empty() { - return Ok(Self { - ..Default::default() - }); + return Ok(Self(items)); } + + items.push(input.parse::()?); + + while input.lookahead1().peek(Token![,]) { + input.parse::()?; + items.push(input.parse::()?); + } + + Ok(Self(items)) + } +} + +impl Parse for SecurityRequirementsAttrItem { + fn parse(input: ParseStream) -> syn::Result { 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) - .collect::>(); - - Ok(Self { - name: Some(name), - scopes: Some(scopes), - }) + + if input.lookahead1().peek(Token![=]) { + input.parse::()?; + + let scopes_stream; + bracketed!(scopes_stream in input); + + let scopes = Punctuated::::parse_terminated(&scopes_stream)? + .iter() + .map(LitStr::value) + .collect::>(); + + Ok(Self { name, scopes }) + } else { + Ok(Self { + name, + scopes: vec![], + }) + } } } -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::>(); + tokens.extend(quote! { + utoipa::openapi::security::SecurityRequirement::new() + }); + + for requirement in &self.0 { + let name = &requirement.name; + let scopes = requirement.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() - }) + .add::<&str, [&str; #scopes_len], &str>(#name, #scopes) + }); } } } diff --git a/utoipa/src/openapi/path.rs b/utoipa/src/openapi/path.rs index c81acc4f..5960050e 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"))] @@ -770,11 +771,12 @@ mod tests { #[test] fn operation_builder_security() { let security_requirement1 = - SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]); - let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]); + SecurityRequirement::single("api_oauth2_flow", ["edit:items", "read:items"]); + let security_requirement2 = + SecurityRequirement::single("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..5778047b 100644 --- a/utoipa/src/openapi/security.rs +++ b/utoipa/src/openapi/security.rs @@ -45,7 +45,7 @@ impl SecurityRequirement { /// Create new security requirement with scopes. /// ```rust /// # use utoipa::openapi::security::SecurityRequirement; - /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]); + /// SecurityRequirement::single("api_oauth2_flow", ["edit:items", "read:items"]); /// ``` /// /// You can also create an empty security requirement with `Default::default()`. @@ -53,7 +53,7 @@ impl SecurityRequirement { /// # use utoipa::openapi::security::SecurityRequirement; /// SecurityRequirement::default(); /// ``` - pub fn new, S: IntoIterator, I: Into>( + pub fn single, S: IntoIterator, I: Into>( name: N, scopes: S, ) -> Self { @@ -69,6 +69,23 @@ impl SecurityRequirement { })), } } + + pub fn new() -> Self { + Self::default() + } + + 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. From 6bc1922a534c98efd8a429a30391cc98e19c83eb Mon Sep 17 00:00:00 2001 From: Adi Salimgereev Date: Sat, 2 Dec 2023 11:33:41 +0600 Subject: [PATCH 2/7] Add recovering --- utoipa-gen/src/security_requirement.rs | 47 ++++++++++++-------------- utoipa/src/openapi/path.rs | 5 ++- utoipa/src/openapi/security.rs | 23 +++++++++---- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/utoipa-gen/src/security_requirement.rs b/utoipa-gen/src/security_requirement.rs index 2b6d33e9..aaf95b51 100644 --- a/utoipa-gen/src/security_requirement.rs +++ b/utoipa-gen/src/security_requirement.rs @@ -13,8 +13,8 @@ use crate::Array; #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct SecurityRequirementsAttrItem { - pub name: String, - pub scopes: Vec, + pub name: Option, + pub scopes: Option>, } #[derive(Default)] @@ -44,41 +44,38 @@ impl Parse for SecurityRequirementsAttrItem { fn parse(input: ParseStream) -> syn::Result { let name = input.parse::()?.value(); - if input.lookahead1().peek(Token![=]) { - input.parse::()?; + input.parse::()?; - let scopes_stream; - bracketed!(scopes_stream in input); + let scopes_stream; + bracketed!(scopes_stream in input); - let scopes = Punctuated::::parse_terminated(&scopes_stream)? - .iter() - .map(LitStr::value) - .collect::>(); + let scopes = Punctuated::::parse_terminated(&scopes_stream)? + .iter() + .map(LitStr::value) + .collect::>(); - Ok(Self { name, scopes }) - } else { - Ok(Self { - name, - scopes: vec![], - }) - } + Ok(Self { + name: Some(name), + scopes: Some(scopes), + }) } } impl ToTokens for SecurityRequirementsAttr { fn to_tokens(&self, tokens: &mut TokenStream) { tokens.extend(quote! { - utoipa::openapi::security::SecurityRequirement::new() + utoipa::openapi::security::SecurityRequirement::default() }); for requirement in &self.0 { - let name = &requirement.name; - let scopes = requirement.scopes.iter().collect::>(); - let scopes_len = scopes.len(); - - tokens.extend(quote! { - .add::<&str, [&str; #scopes_len], &str>(#name, #scopes) - }); + 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/src/openapi/path.rs b/utoipa/src/openapi/path.rs index 5960050e..2a302f22 100644 --- a/utoipa/src/openapi/path.rs +++ b/utoipa/src/openapi/path.rs @@ -771,9 +771,8 @@ mod tests { #[test] fn operation_builder_security() { let security_requirement1 = - SecurityRequirement::single("api_oauth2_flow", ["edit:items", "read:items"]); - let security_requirement2 = - SecurityRequirement::single("api_oauth2_flow", ["remove:items"]); + 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.into()) .security(security_requirement2.into()) diff --git a/utoipa/src/openapi/security.rs b/utoipa/src/openapi/security.rs index 5778047b..76cb2221 100644 --- a/utoipa/src/openapi/security.rs +++ b/utoipa/src/openapi/security.rs @@ -45,7 +45,7 @@ impl SecurityRequirement { /// Create new security requirement with scopes. /// ```rust /// # use utoipa::openapi::security::SecurityRequirement; - /// SecurityRequirement::single("api_oauth2_flow", ["edit:items", "read:items"]); + /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]); /// ``` /// /// You can also create an empty security requirement with `Default::default()`. @@ -53,7 +53,7 @@ impl SecurityRequirement { /// # use utoipa::openapi::security::SecurityRequirement; /// SecurityRequirement::default(); /// ``` - pub fn single, S: IntoIterator, I: Into>( + pub fn new, S: IntoIterator, I: Into>( name: N, scopes: S, ) -> Self { @@ -70,10 +70,21 @@ impl SecurityRequirement { } } - pub fn new() -> Self { - Self::default() - } - + /// Allows to add multiple security requirements. + /// + /// 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; + /// SecurityRequirement::default() + /// .add("refresh_token", ["edit:accounts"]) + /// .add("access_token", ["edit:accounts"]); + /// ``` pub fn add, S: IntoIterator, I: Into>( mut self, name: N, From 3007783b65434a1e3adaa509277d378f04550bf6 Mon Sep 17 00:00:00 2001 From: Adi Salimgereev Date: Sat, 2 Dec 2023 12:27:33 +0600 Subject: [PATCH 3/7] Add test --- utoipa-gen/tests/openapi_derive_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utoipa-gen/tests/openapi_derive_test.rs b/utoipa-gen/tests/openapi_derive_test.rs index 162e0857..e868245b 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" = []) ) )] From 63a09122404313b92c9180494226bdfea51904cb Mon Sep 17 00:00:00 2001 From: Adi Salimgereev Date: Mon, 4 Dec 2023 20:30:44 +0600 Subject: [PATCH 4/7] Add tests for multiple security requirements support --- utoipa-gen/tests/openapi_derive.rs | 5 ++++- utoipa-gen/tests/openapi_derive_test.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 e868245b..7d82eb69 100644 --- a/utoipa-gen/tests/openapi_derive_test.rs +++ b/utoipa-gen/tests/openapi_derive_test.rs @@ -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" = []) ) )] From c78fdfb683e210f887569c77aad34de4eb7c9452 Mon Sep 17 00:00:00 2001 From: Adi Salimgereev Date: Mon, 4 Dec 2023 20:32:22 +0600 Subject: [PATCH 5/7] Refactor security requirements parser --- utoipa-gen/src/security_requirement.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/utoipa-gen/src/security_requirement.rs b/utoipa-gen/src/security_requirement.rs index aaf95b51..62bfa51d 100644 --- a/utoipa-gen/src/security_requirement.rs +++ b/utoipa-gen/src/security_requirement.rs @@ -19,24 +19,12 @@ pub struct SecurityRequirementsAttrItem { #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] -pub struct SecurityRequirementsAttr(Vec); +pub struct SecurityRequirementsAttr(Punctuated); impl Parse for SecurityRequirementsAttr { fn parse(input: ParseStream) -> syn::Result { - let mut items = Vec::new(); - - if input.is_empty() { - return Ok(Self(items)); - } - - items.push(input.parse::()?); - - while input.lookahead1().peek(Token![,]) { - input.parse::()?; - items.push(input.parse::()?); - } - - Ok(Self(items)) + Punctuated::::parse_terminated(input) + .map(|o| Self(o.into_iter().collect())) } } From f21333770235007b5992c632872664f85b4a2c6d Mon Sep 17 00:00:00 2001 From: Adi Salimgereev Date: Mon, 4 Dec 2023 20:32:45 +0600 Subject: [PATCH 6/7] Add better doctest for add() method --- utoipa/src/openapi/security.rs | 48 ++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/utoipa/src/openapi/security.rs b/utoipa/src/openapi/security.rs index 76cb2221..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, S: IntoIterator, I: Into>( name: N, scopes: S, @@ -70,7 +73,7 @@ impl SecurityRequirement { } } - /// Allows to add multiple security requirements. + /// 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`]. @@ -80,10 +83,45 @@ impl SecurityRequirement { /// /// Make both API keys required: /// ```rust - /// # use utoipa::openapi::security::SecurityRequirement; - /// SecurityRequirement::default() - /// .add("refresh_token", ["edit:accounts"]) - /// .add("access_token", ["edit:accounts"]); + /// # 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, From a9f123c5658e714df46590798f51fac1f2f999e4 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Thu, 14 Dec 2023 02:06:35 +0200 Subject: [PATCH 7/7] Update docs for the security requirement --- utoipa-gen/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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.