Skip to content

Commit

Permalink
Add support for specifying multiple security requirement keys (#813)
Browse files Browse the repository at this point in the history
This PR add support for specifying multiple security requirements with AND
fashion that allows greater control over the requirements.
```rust
security(
     // literally means (api_key and api_key2) or api_key3
     ("api_key" = [], "api_key2" = []),
     ("api_key3" = []),
 )
```
  • Loading branch information
abs0luty authored Dec 14, 2023
1 parent f965165 commit 7493c33
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 32 deletions.
12 changes: 12 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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"]),
("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 @@ -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<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
name: N,
scopes: S,
Expand All @@ -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<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.
Expand Down

0 comments on commit 7493c33

Please sign in to comment.