From 5a8bc59e628cd29c96a99c51a5faf40375b32e13 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Sat, 24 Jul 2021 11:24:23 +1000 Subject: [PATCH] Add ability to specify default JAX-RS roles allowed Also improve docs around this Fixes #10362 --- .../main/asciidoc/security-authorization.adoc | 21 +++-- .../deployment/ResteasyBuiltinsProcessor.java | 11 +++ .../DefaultRolesAllowedJaxRsTest.java | 85 +++++++++++++++++++ .../DefaultRolesAllowedStarJaxRsTest.java | 85 +++++++++++++++++++ .../resteasy/runtime/JaxRsSecurityConfig.java | 15 ++++ .../ResteasyReactiveCommonProcessor.java | 11 +++ .../runtime/ResteasyReactiveConfig.java | 14 +++ .../DefaultRolesAllowedJaxRsTest.java | 85 +++++++++++++++++++ .../DefaultRolesAllowedStarJaxRsTest.java | 85 +++++++++++++++++++ .../AdditionalRolesAllowedTransformer.java | 38 +++++++++ .../deployment/SecurityProcessor.java | 41 +++++++-- .../deployment/SecurityTransformerUtils.java | 2 + .../AdditionalSecuredClassesBuildItem.java | 13 +++ 13 files changed, 492 insertions(+), 14 deletions(-) create mode 100644 extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java create mode 100644 extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java create mode 100644 extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java diff --git a/docs/src/main/asciidoc/security-authorization.adoc b/docs/src/main/asciidoc/security-authorization.adoc index fde71e04b0bd4f..cba66a7b404896 100644 --- a/docs/src/main/asciidoc/security-authorization.adoc +++ b/docs/src/main/asciidoc/security-authorization.adoc @@ -11,7 +11,11 @@ Quarkus has an integrated pluggable web security layer. If security is enabled a check performed to make sure they are allowed to continue. NOTE: Configuration authorization checks are executed before any annotation-based authorization check is done, so both -checks have to pass for a request to be allowed. +checks have to pass for a request to be allowed. This means you cannot use `@PermitAll` to open up a path if the path has +been blocked using `quarkus.http.auth.` configuration. If you are using JAX-RS you may want to consider using the +`quarkus.security.jaxrs.deny-unannotated-endpoints` or `quarkus.security.jaxrs.default-roles-allowed` to set default security +requirements instead of HTTP path level matching, as these properties can be overridden by annotations on an individual +endpoint. == Authorization using Configuration @@ -144,11 +148,18 @@ so would require both the `user` and `admin` roles. === Configuration Properties to Deny access -There are two configuration settings that alter the RBAC Deny behavior: +There are three configuration settings that alter the RBAC Deny behavior: -- `quarkus.security.jaxrs.deny-unannotated-endpoints=true|false` - if set to true, the access will be denied for all JAX-RS endpoints by default. -That is if the security annotations do not define the access control. Defaults to `false`. -- `quarkus.security.deny-unannotated-members=true|false` - if set to true, the access will be denied to all CDI methods +`quarkus.security.jaxrs.deny-unannotated-endpoints=true|false`:: +If set to true, the access will be denied for all JAX-RS endpoints by default, so if a JAX-RS endpoint does not have any security annotations +then it will default to `@DenyAll` behaviour. This is useful to ensure you cannot accidently expose an endpoint that is supposed to be secured. Defaults to `false`. + +`quarkus.security.jaxrs.default-roles-allowed=role1,role2`:: +Defines the default role requirements for unannotated endpoints. The role '**' is a special role that means any authenticated user. This cannot be combined with +`deny-unannotated-endpoints`, as the deny will take effect instead. + +`quarkus.security.deny-unannotated-members=true|false`:: +- if set to true, the access will be denied to all CDI methods and JAX-RS endpoints that do not have security annotations but are defined in classes that contain methods with security annotations. Defaults to `false`. diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java index 31fb586e3d58ae..27d6e8d174b8a1 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java @@ -63,6 +63,17 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index, } additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes)); + } else if (config.defaultRolesAllowed.isPresent() && resteasyDeployment != null) { + final List classes = new ArrayList<>(); + + List resourceClasses = resteasyDeployment.getDeployment().getScannedResourceClasses(); + for (String className : resourceClasses) { + ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className)); + if (!hasSecurityAnnotation(classInfo)) { + classes.add(classInfo); + } + } + additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes, config.defaultRolesAllowed)); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java new file mode 100644 index 00000000000000..90f70eabacfc1f --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java @@ -0,0 +1,85 @@ +package io.quarkus.resteasy.test.security; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class DefaultRolesAllowedJaxRsTest { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(PermitAllResource.class, UnsecuredResource.class, + TestIdentityProvider.class, + TestIdentityController.class, + UnsecuredSubResource.class) + .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void shouldDenyUnannotated() { + String path = "/unsecured/defaultSecurity"; + assertStatus(path, 200, 403, 401); + } + + @Test + public void shouldDenyDenyAllMethod() { + String path = "/unsecured/denyAll"; + assertStatus(path, 403, 403, 401); + } + + @Test + public void shouldPermitPermitAllMethod() { + assertStatus("/unsecured/permitAll", 200, 200, 200); + } + + @Test + public void shouldDenySubResource() { + String path = "/unsecured/sub/subMethod"; + assertStatus(path, 200, 403, 401); + } + + @Test + public void shouldAllowPermitAllSubResource() { + String path = "/unsecured/permitAllSub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + @Test + public void shouldAllowPermitAllClass() { + String path = "/permitAll/sub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + private void assertStatus(String path, int adminStatus, int userStatus, int anonStatus) { + given().auth().preemptive() + .basic("admin", "admin").get(path) + .then() + .statusCode(adminStatus); + given().auth().preemptive() + .basic("user", "user").get(path) + .then() + .statusCode(userStatus); + when().get(path) + .then() + .statusCode(anonStatus); + + } + +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java new file mode 100644 index 00000000000000..e49aa523599bf9 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java @@ -0,0 +1,85 @@ +package io.quarkus.resteasy.test.security; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class DefaultRolesAllowedStarJaxRsTest { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(PermitAllResource.class, UnsecuredResource.class, + TestIdentityProvider.class, + TestIdentityController.class, + UnsecuredSubResource.class) + .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void shouldDenyUnannotated() { + String path = "/unsecured/defaultSecurity"; + assertStatus(path, 200, 200, 401); + } + + @Test + public void shouldDenyDenyAllMethod() { + String path = "/unsecured/denyAll"; + assertStatus(path, 403, 403, 401); + } + + @Test + public void shouldPermitPermitAllMethod() { + assertStatus("/unsecured/permitAll", 200, 200, 200); + } + + @Test + public void shouldDenySubResource() { + String path = "/unsecured/sub/subMethod"; + assertStatus(path, 200, 200, 401); + } + + @Test + public void shouldAllowPermitAllSubResource() { + String path = "/unsecured/permitAllSub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + @Test + public void shouldAllowPermitAllClass() { + String path = "/permitAll/sub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + private void assertStatus(String path, int adminStatus, int userStatus, int anonStatus) { + given().auth().preemptive() + .basic("admin", "admin").get(path) + .then() + .statusCode(adminStatus); + given().auth().preemptive() + .basic("user", "user").get(path) + .then() + .statusCode(userStatus); + when().get(path) + .then() + .statusCode(anonStatus); + + } + +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/JaxRsSecurityConfig.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/JaxRsSecurityConfig.java index dc1a8c730393d1..4264a3c7d4dbf6 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/JaxRsSecurityConfig.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/JaxRsSecurityConfig.java @@ -1,5 +1,8 @@ package io.quarkus.resteasy.runtime; +import java.util.List; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -14,4 +17,16 @@ public class JaxRsSecurityConfig { */ @ConfigItem(name = "deny-unannotated-endpoints") public boolean denyJaxRs; + + /** + * If no security annotations are affecting a method then they will default to requiring these roles, + * (equivalent to adding an @RolesAllowed annotation with the roles to every endpoint class). + * + * The role of '**' means any authenticated user, which is equivalent to the {@link io.quarkus.security.Authenticated} + * annotation. + * + */ + @ConfigItem + public Optional> defaultRolesAllowed; + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index c8a01d873dbd95..7f45087e4f53b5 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -78,6 +78,17 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index, } additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes)); + } else if (config.defaultRolesAllowed.isPresent() && resteasyDeployment.isPresent()) { + + final List classes = new ArrayList<>(); + Set resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet(); + for (DotName className : resourceClasses) { + ClassInfo classInfo = index.getIndex().getClassByName(className); + if (!SecurityTransformerUtils.hasSecurityAnnotation(classInfo)) { + classes.add(classInfo); + } + } + additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes, config.defaultRolesAllowed)); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java index 5d2d26ae866574..dbf5b93c4c4da2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java @@ -1,5 +1,8 @@ package io.quarkus.resteasy.reactive.common.runtime; +import java.util.List; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -47,4 +50,15 @@ public class ResteasyReactiveConfig { */ @ConfigItem(name = "deny-unannotated-endpoints") public boolean denyJaxRs; + + /** + * If no security annotations are affecting a method then they will default to requiring these roles, + * (equivalent to adding an @RolesAllowed annotation with the roles to every endpoint class). + * + * The role of '**' means any authenticated user, which is equivalent to the {@link io.quarkus.security.Authenticated} + * annotation. + * + */ + @ConfigItem + public Optional> defaultRolesAllowed; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java new file mode 100644 index 00000000000000..5db9bf5b097fa6 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java @@ -0,0 +1,85 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class DefaultRolesAllowedJaxRsTest { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(PermitAllResource.class, UnsecuredResource.class, + TestIdentityProvider.class, + TestIdentityController.class, + UnsecuredSubResource.class) + .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void shouldDenyUnannotated() { + String path = "/unsecured/defaultSecurity"; + assertStatus(path, 200, 403, 401); + } + + @Test + public void shouldDenyDenyAllMethod() { + String path = "/unsecured/denyAll"; + assertStatus(path, 403, 403, 401); + } + + @Test + public void shouldPermitPermitAllMethod() { + assertStatus("/unsecured/permitAll", 200, 200, 200); + } + + @Test + public void shouldDenySubResource() { + String path = "/unsecured/sub/subMethod"; + assertStatus(path, 200, 403, 401); + } + + @Test + public void shouldAllowPermitAllSubResource() { + String path = "/unsecured/permitAllSub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + @Test + public void shouldAllowPermitAllClass() { + String path = "/permitAll/sub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + private void assertStatus(String path, int adminStatus, int userStatus, int anonStatus) { + given().auth().preemptive() + .basic("admin", "admin").get(path) + .then() + .statusCode(adminStatus); + given().auth().preemptive() + .basic("user", "user").get(path) + .then() + .statusCode(userStatus); + when().get(path) + .then() + .statusCode(anonStatus); + + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java new file mode 100644 index 00000000000000..a0761c6a9ec1ea --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java @@ -0,0 +1,85 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class DefaultRolesAllowedStarJaxRsTest { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(PermitAllResource.class, UnsecuredResource.class, + TestIdentityProvider.class, + TestIdentityController.class, + UnsecuredSubResource.class) + .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void shouldDenyUnannotated() { + String path = "/unsecured/defaultSecurity"; + assertStatus(path, 200, 200, 401); + } + + @Test + public void shouldDenyDenyAllMethod() { + String path = "/unsecured/denyAll"; + assertStatus(path, 403, 403, 401); + } + + @Test + public void shouldPermitPermitAllMethod() { + assertStatus("/unsecured/permitAll", 200, 200, 200); + } + + @Test + public void shouldDenySubResource() { + String path = "/unsecured/sub/subMethod"; + assertStatus(path, 200, 200, 401); + } + + @Test + public void shouldAllowPermitAllSubResource() { + String path = "/unsecured/permitAllSub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + @Test + public void shouldAllowPermitAllClass() { + String path = "/permitAll/sub/subMethod"; + assertStatus(path, 200, 200, 200); + } + + private void assertStatus(String path, int adminStatus, int userStatus, int anonStatus) { + given().auth().preemptive() + .basic("admin", "admin").get(path) + .then() + .statusCode(adminStatus); + given().auth().preemptive() + .basic("user", "user").get(path) + .then() + .statusCode(userStatus); + when().get(path) + .then() + .statusCode(anonStatus); + + } + +} diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java new file mode 100644 index 00000000000000..8362572a780d27 --- /dev/null +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java @@ -0,0 +1,38 @@ +package io.quarkus.security.deployment; + +import static io.quarkus.security.deployment.SecurityTransformerUtils.ROLES_ALLOWED; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; + +import io.quarkus.arc.processor.AnnotationsTransformer; + +public class AdditionalRolesAllowedTransformer implements AnnotationsTransformer { + + private final Set classNames; + private final AnnotationValue[] rolesAllowed; + + public AdditionalRolesAllowedTransformer(Collection classNames, List rolesAllowed) { + this.classNames = new HashSet<>(classNames); + this.rolesAllowed = rolesAllowed.stream().map(s -> AnnotationValue.createStringValue("", s)) + .toArray(AnnotationValue[]::new); + } + + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void transform(TransformationContext context) { + String className = context.getTarget().asClass().name().toString(); + if (classNames.contains(className)) { + context.transform().add(ROLES_ALLOWED, AnnotationValue.createArrayValue("value", rolesAllowed)).done(); + } + } +} diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 6667feae08ed92..726419f7d976ac 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -289,14 +289,21 @@ void transformSecurityAnnotations(BuildProducer transformers.produce(new AnnotationsTransformerBuildItem(new DenyingUnannotatedTransformer())); } if (!additionalSecuredClasses.isEmpty()) { - Set additionalSecured = new HashSet<>(); for (AdditionalSecuredClassesBuildItem securedClasses : additionalSecuredClasses) { + Set additionalSecured = new HashSet<>(); for (ClassInfo additionalSecuredClass : securedClasses.additionalSecuredClasses) { additionalSecured.add(additionalSecuredClass.name().toString()); } + if (securedClasses.rolesAllowed.isPresent()) { + transformers.produce( + new AnnotationsTransformerBuildItem(new AdditionalRolesAllowedTransformer(additionalSecured, + securedClasses.rolesAllowed.get()))); + } else { + transformers.produce( + new AnnotationsTransformerBuildItem( + new AdditionalDenyingUnannotatedTransformer(additionalSecured))); + } } - transformers.produce( - new AnnotationsTransformerBuildItem(new AdditionalDenyingUnannotatedTransformer(additionalSecured))); } } @@ -310,11 +317,11 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, List additionalSecurityChecks, SecurityBuildTimeConfig config) { classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorage.AppPredicate())); - final Map additionalSecured = new HashMap<>(); + final Map additionalSecured = new HashMap<>(); for (AdditionalSecuredClassesBuildItem securedClasses : additionalSecuredClasses) { securedClasses.additionalSecuredClasses.forEach(c -> { if (!additionalSecured.containsKey(c.name())) { - additionalSecured.put(c.name(), c); + additionalSecured.put(c.name(), new AdditionalSecured(c, securedClasses.rolesAllowed)); } }); } @@ -352,7 +359,7 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, private Map gatherSecurityAnnotations( IndexView index, - Map additionalSecuredClasses, boolean denyUnannotated, SecurityCheckRecorder recorder) { + Map additionalSecuredClasses, boolean denyUnannotated, SecurityCheckRecorder recorder) { Map methodToInstanceCollector = new HashMap<>(); Map classAnnotations = new HashMap<>(); @@ -371,14 +378,19 @@ private Map gatherSecurityAnnotations( * Handle additional secured classes by adding the denyAll check to all public non-static methods * that don't have security annotations */ - for (Map.Entry additionalSecureClassInfo : additionalSecuredClasses.entrySet()) { - for (MethodInfo methodInfo : additionalSecureClassInfo.getValue().methods()) { + for (Map.Entry additionalSecureClassInfo : additionalSecuredClasses.entrySet()) { + for (MethodInfo methodInfo : additionalSecureClassInfo.getValue().classInfo.methods()) { if (!isPublicNonStaticNonConstructor(methodInfo)) { continue; } AnnotationInstance alreadyExistingInstance = methodToInstanceCollector.get(methodInfo); if ((alreadyExistingInstance == null)) { - result.put(methodInfo, recorder.denyAll()); + if (additionalSecureClassInfo.getValue().rolesAllowed.isPresent()) { + result.put(methodInfo, recorder + .rolesAllowed(additionalSecureClassInfo.getValue().rolesAllowed.get().toArray(String[]::new))); + } else { + result.put(methodInfo, recorder.denyAll()); + } } else if (alreadyExistingInstance.target().kind() == AnnotationTarget.Kind.CLASS) { throw new IllegalStateException("Class " + methodInfo.declaringClass() + " should not have been added as an additional secured class"); @@ -481,4 +493,15 @@ void registerAdditionalBeans(BuildProducer beans) { AdditionalBeanBuildItem authorizationController() { return AdditionalBeanBuildItem.builder().addBeanClass(AuthorizationController.class).build(); } + + static class AdditionalSecured { + + final ClassInfo classInfo; + final Optional> rolesAllowed; + + AdditionalSecured(ClassInfo classInfo, Optional> rolesAllowed) { + this.classInfo = classInfo; + this.rolesAllowed = rolesAllowed; + } + } } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityTransformerUtils.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityTransformerUtils.java index 1d073ec11b4968..9633ae5cb62d0b 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityTransformerUtils.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityTransformerUtils.java @@ -5,6 +5,7 @@ import java.util.Set; import javax.annotation.security.DenyAll; +import javax.annotation.security.RolesAllowed; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; @@ -16,6 +17,7 @@ */ public class SecurityTransformerUtils { public static final DotName DENY_ALL = DotName.createSimple(DenyAll.class.getName()); + public static final DotName ROLES_ALLOWED = DotName.createSimple(RolesAllowed.class.getName()); private static final Set SECURITY_ANNOTATIONS = SecurityAnnotationsRegistrar.SECURITY_BINDINGS.keySet(); public static boolean hasStandardSecurityAnnotation(MethodInfo methodInfo) { diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java index 0c308f8a6e2be9..5a1e138548253f 100644 --- a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java @@ -2,6 +2,8 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Optional; import org.jboss.jandex.ClassInfo; @@ -12,8 +14,19 @@ */ public final class AdditionalSecuredClassesBuildItem extends MultiBuildItem { public final Collection additionalSecuredClasses; + /** + * The roles alloe + */ + public final Optional> rolesAllowed; public AdditionalSecuredClassesBuildItem(Collection additionalSecuredClasses) { this.additionalSecuredClasses = Collections.unmodifiableCollection(additionalSecuredClasses); + rolesAllowed = Optional.empty(); + } + + public AdditionalSecuredClassesBuildItem(Collection additionalSecuredClasses, + Optional> rolesAllowed) { + this.additionalSecuredClasses = Collections.unmodifiableCollection(additionalSecuredClasses); + this.rolesAllowed = rolesAllowed; } }