diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java index 989a7386fc..3a1b9d144c 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java @@ -11,9 +11,11 @@ import static java.lang.annotation.ElementType.METHOD; /** - * The annotation may be applied at class or method level, or in {@link io.swagger.v3.oas.annotations.Operation#security()} ()} to define security requirements for the + * The annotation may be applied at class or method level, or in {@link io.swagger.v3.oas.annotations.Operation#security()} to define security requirements for the * single operation (when applied at method level) or for all operations of a class (when applied at class level). *

It can also be used in {@link io.swagger.v3.oas.annotations.OpenAPIDefinition#security()} to define spec level security.

+ *

{@link SecurityRequirement#combine()} can be used to define multiple security requirements at the same time, requiring each one of them. + * If only one of multiple security schemes is required, use multiple {@link SecurityRequirement} annotations.

* * @see Security Requirement (OpenAPI specification) * @see io.swagger.v3.oas.annotations.OpenAPIDefinition @@ -26,10 +28,11 @@ public @interface SecurityRequirement { /** * This name must correspond to a declared SecurityRequirement. + *

Exactly one of this and {@link #combine()} must be set.

* * @return String name */ - String name(); + String name() default ""; /** * If the security scheme is of type "oauth2" or "openIdConnect", then the value is a list of scope names required for the execution. @@ -38,4 +41,13 @@ * @return String array of scopes */ String[] scopes() default {}; + + /** + * If multiple requirements apply at the same time, use this value instead of {@link #name()} and {@link #scopes()}. + * If any one of multiple security schemes is required, use multiple {@link SecurityRequirement} annotations instead. + *

Exactly one of this and {@link #name()} must be set.

+ * + * @return SecurityRequirementEntry array of requirements + */ + SecurityRequirementEntry[] combine() default {}; } diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java new file mode 100644 index 0000000000..4070c06c96 --- /dev/null +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java @@ -0,0 +1,33 @@ +package io.swagger.v3.oas.annotations.security; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation may be applied in {@link SecurityRequirement#combine()} to define combined security requirements for the + * single operation. + * + * @see Security Requirement (OpenAPI specification) + * @see io.swagger.v3.oas.annotations.security.SecurityRequirement + **/ +@Target({ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SecurityRequirementEntry { + /** + * This name must correspond to a declared SecurityRequirement. + * + * @return String name + */ + String name(); + + /** + * If the security scheme is of type "oauth2" or "openIdConnect", then the value is a list of scope names required for the execution. + * For other security scheme types, the array must be empty. + * + * @return String array of scopes + */ + String[] scopes() default {}; +} diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java index c0a6010b31..0d3eba846e 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java @@ -28,14 +28,28 @@ public static Optional> getSecurityRequirements(io.swa } List securityRequirements = new ArrayList<>(); for (io.swagger.v3.oas.annotations.security.SecurityRequirement securityRequirementApi : securityRequirementsApi) { - if (StringUtils.isBlank(securityRequirementApi.name())) { - continue; - } SecurityRequirement securityRequirement = new SecurityRequirement(); - if (securityRequirementApi.scopes().length > 0) { - securityRequirement.addList(securityRequirementApi.name(), Arrays.asList(securityRequirementApi.scopes())); - } else { - securityRequirement.addList(securityRequirementApi.name()); + if (securityRequirementApi.combine().length > 0) { + for (io.swagger.v3.oas.annotations.security.SecurityRequirementEntry entry : securityRequirementApi.combine()) { + if (StringUtils.isBlank(entry.name())) { + continue; + } + if (entry.scopes().length > 0) { + securityRequirement.addList(entry.name(), Arrays.asList(entry.scopes())); + } else { + securityRequirement.addList(entry.name()); + } + } + } + else { + if (StringUtils.isBlank(securityRequirementApi.name())) { + continue; + } + if (securityRequirementApi.scopes().length > 0) { + securityRequirement.addList(securityRequirementApi.name(), Arrays.asList(securityRequirementApi.scopes())); + } else { + securityRequirement.addList(securityRequirementApi.name()); + } } securityRequirements.add(securityRequirement); } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java index bdaba471fa..a1cecd845b 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.security.OAuthFlows; import io.swagger.v3.oas.annotations.security.OAuthScope; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirementEntry; import io.swagger.v3.oas.annotations.security.SecurityScheme; import org.testng.annotations.Test; @@ -19,7 +20,7 @@ public class SecurityTest extends AbstractAnnotationTest { @Test - public void testSecuritySheme() { + public void testSecurityScheme() { String openApiYAML = readIntoYaml(SecurityTest.OAuth2SchemeOnClass.class); int start = openApiYAML.indexOf("components:"); String extractedYAML = openApiYAML.substring(start, openApiYAML.length() - 1); @@ -90,7 +91,7 @@ public void testSecurityRequirement() throws IOException { } @Test - public void testMultipleSecurityShemes() { + public void testMultipleSecuritySchemes() { String openApiYAML = readIntoYaml(SecurityTest.MultipleSchemesOnClass.class); int start = openApiYAML.indexOf("components:"); String extractedYAML = openApiYAML.substring(start, openApiYAML.length() - 1); @@ -112,6 +113,30 @@ public void testMultipleSecurityShemes() { } + @Test + public void testCombinedSecurityRequirements() { + String openApiYAML = readIntoYaml(SecurityTest.CombinedSecurityRequirementsOnClass.class); + int start = openApiYAML.indexOf("security:"); + int end = openApiYAML.indexOf("components:"); + String extractedYAML = openApiYAML.substring(start, end); + String expectedYAML = "security:\n" + + "- api_key: []\n" + + " myOauth2Security: []\n"; + assertEquals(extractedYAML, expectedYAML); + } + + @Test + public void testSecurityRequirementAlternatives() { + String openApiYAML = readIntoYaml(SecurityRequirementAlternativesOnClass.class); + int start = openApiYAML.indexOf("security:"); + int end = openApiYAML.indexOf("components:"); + String extractedYAML = openApiYAML.substring(start, end); + String expectedYAML = "security:\n" + + "- api_key: []\n" + + "- myOauth2Security: []\n"; + assertEquals(extractedYAML, expectedYAML); + } + @Test public void testTicket2767() { String openApiYAML = readIntoYaml(SecurityTest.Ticket2767.class); @@ -169,6 +194,34 @@ static class MultipleSchemesOnClass { } + @OpenAPIDefinition( + security = {@SecurityRequirement(combine = { @SecurityRequirementEntry(name = "api_key"), @SecurityRequirementEntry(name = "myOauth2Security") })} + ) + @SecurityScheme(name = "api_key", type = SecuritySchemeType.APIKEY, paramName = "API_KEY") + @SecurityScheme(name = "myOauth2Security", + type = SecuritySchemeType.OAUTH2, + in = SecuritySchemeIn.HEADER, + flows = @OAuthFlows( + implicit = @OAuthFlow(authorizationUrl = "http://url.com/auth", + scopes = @OAuthScope(name = "write:pets", description = "modify pets in your account")))) + static class CombinedSecurityRequirementsOnClass { + + } + + @OpenAPIDefinition( + security = {@SecurityRequirement(name = "api_key"), @SecurityRequirement(name = "myOauth2Security")} + ) + @SecurityScheme(name = "api_key", type = SecuritySchemeType.APIKEY, paramName = "API_KEY") + @SecurityScheme(name = "myOauth2Security", + type = SecuritySchemeType.OAUTH2, + in = SecuritySchemeIn.HEADER, + flows = @OAuthFlows( + implicit = @OAuthFlow(authorizationUrl = "http://url.com/auth", + scopes = @OAuthScope(name = "write:pets", description = "modify pets in your account")))) + static class SecurityRequirementAlternativesOnClass { + + } + @OpenAPIDefinition( security = {@SecurityRequirement(name = "basicAuth")},