Skip to content

Commit

Permalink
Add ability to specify default JAX-RS roles allowed
Browse files Browse the repository at this point in the history
Also improve docs around this

Fixes quarkusio#10362
  • Loading branch information
stuartwdouglas committed Jul 24, 2021
1 parent 5cf94dc commit 5a8bc59
Show file tree
Hide file tree
Showing 13 changed files with 492 additions and 14 deletions.
21 changes: 16 additions & 5 deletions docs/src/main/asciidoc/security-authorization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
}

additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes));
} else if (config.defaultRolesAllowed.isPresent() && resteasyDeployment != null) {
final List<ClassInfo> classes = new ArrayList<>();

List<String> 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));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}

}
Original file line number Diff line number Diff line change
@@ -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);

}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<List<String>> defaultRolesAllowed;

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
}

additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes));
} else if (config.defaultRolesAllowed.isPresent() && resteasyDeployment.isPresent()) {

final List<ClassInfo> classes = new ArrayList<>();
Set<DotName> 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));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<List<String>> defaultRolesAllowed;
}
Original file line number Diff line number Diff line change
@@ -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);

}

}
Loading

0 comments on commit 5a8bc59

Please sign in to comment.