Skip to content

Commit

Permalink
Merge pull request quarkusio#42749 from michalvavrik/feature/add-auth…
Browse files Browse the repository at this point in the history
…orization-policy-annotation

Add new AuthorizationPolicy annotation to bind named HttpSecurityPolicy to a Jakarta REST endpoints
  • Loading branch information
sberyozkin authored Aug 28, 2024
2 parents 7e86ad8 + fcb8abb commit d6e82b7
Show file tree
Hide file tree
Showing 60 changed files with 2,114 additions and 323 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,33 @@ quarkus.http.auth.permission.custom1.policy=custom
----
<1> Custom policy name must match the value returned by the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method.

Alternatively, you can bind custom named HttpSecurityPolicy to Jakarta REST endpoints with the `@AuthorizationPolicy` security annotation.

[[authorization-policy-example]]
.Example of custom named HttpSecurityPolicy bound to a Jakarta REST endpoint
[source,java]
----
import io.quarkus.vertx.http.security.AuthorizationPolicy;
import jakarta.annotation.security.DenyAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@DenyAll <1>
@Path("hello")
public class HelloResource {
@AuthorizationPolicy(name = "custom") <2>
@GET
public String hello() {
return "hello";
}
}
----
<1> The `@AuthorizationPolicy` annotation can be used together with other standard security annotations.
As usual, the method-level annotation has priority over class-level annotation.
<2> Apply custom named HttpSecurityPolicy to the Jakarta REST `hello` endpoint.

[TIP]
====
You can also create global `HttpSecurityPolicy` invoked on every request.
Expand Down Expand Up @@ -466,6 +493,9 @@ s| `@PermitAll` | Specifies that all security roles are allowed to invoke the sp
`@PermitAll` lets everybody in, even without authentication.
s| `@RolesAllowed` | Specifies the list of security roles allowed to access methods in an application.
s| `@Authenticated` | {project-name} provides the `io.quarkus.security.Authenticated` annotation that permits any authenticated user to access the resource. It's equivalent to `@RolesAllowed("**")`.
s| `@PermissionsAllowed` | Specifies the list of permissions that are allowed to invoke the specified methods.
s| `@AuthorizationPolicy` | Specifies named `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy` that should authorize access to the specified endpoints.HttpSecurityPolicy.
Named HttpSecurityPolicy can be used for general authorization checks as demonstrated by <<authorization-policy-example>>.
|===

The following <<subject-example>> demonstrates an endpoint that uses both Jakarta REST and Common Security annotations to describe and secure its endpoints.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public CheckResult apply(RoutingContext routingContext, SecurityIdentity securit
}
});
} else {
return Uni.createFrom().item(CheckResult.PERMIT);
return CheckResult.permit();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public PathConfig get() {
public Uni<CheckResult> apply(PathConfig pathConfig) {
if (pathConfig != null
&& pathConfig.getEnforcementMode() == EnforcementMode.ENFORCING) {
return Uni.createFrom().item(CheckResult.DENY);
return CheckResult.deny();
}
return checkPermissionInternal(routingContext, identity);
}
Expand Down Expand Up @@ -121,7 +121,7 @@ private Uni<CheckResult> checkPermissionInternal(RoutingContext routingContext,

if (credential == null) {
// SecurityIdentity has been created by the authentication mechanism other than quarkus-oidc
return Uni.createFrom().item(CheckResult.PERMIT);
return CheckResult.permit();
}

VertxHttpFacade httpFacade = new VertxHttpFacade(routingContext, credential.getToken(), resolver.getReadTimeout());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.quarkus.resteasy.runtime.vertx.JsonObjectReader;
import io.quarkus.resteasy.runtime.vertx.JsonObjectWriter;
import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
import io.quarkus.vertx.http.runtime.security.JaxRsPathMatchingHttpSecurityPolicy;

public class ResteasyBuiltinsProcessor {

Expand Down Expand Up @@ -67,6 +68,7 @@ void setUpSecurity(BuildProducer<ResteasyJaxrsProviderBuildItem> providers,
providers.produce(new ResteasyJaxrsProviderBuildItem(EagerSecurityFilter.class.getName()));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityFilter.class));
transformEagerSecurityNativeMethod(bytecodeTransformerProducer);
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(JaxRsPathMatchingHttpSecurityPolicy.class));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(JaxRsPermissionChecker.class));
additionalBeanBuildItem.produce(
AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.RolesAllowedInterceptor.class));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package io.quarkus.resteasy.test.security.authzpolicy;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.restassured.RestAssured;

public abstract class AbstractAuthorizationPolicyTest {

protected static final Class<?>[] TEST_CLASSES = { TestIdentityProvider.class, TestIdentityController.class,
ForbidAllButViewerAuthorizationPolicy.class, ForbidViewerClassLevelPolicyResource.class,
ForbidViewerMethodLevelPolicyResource.class, NoAuthorizationPolicyResource.class,
PermitUserAuthorizationPolicy.class, ClassRolesAllowedMethodAuthZPolicyResource.class,
ClassAuthZPolicyMethodRolesAllowedResource.class, ViewerAugmentingPolicy.class,
AuthorizationPolicyAndPathMatchingPoliciesResource.class };

protected static final String APPLICATION_PROPERTIES = """
quarkus.http.auth.policy.admin-role.roles-allowed=admin
quarkus.http.auth.policy.viewer-role.roles-allowed=viewer
quarkus.http.auth.permission.jax-rs1.paths=/no-authorization-policy/jax-rs-path-matching-http-perm
quarkus.http.auth.permission.jax-rs1.policy=admin-role
quarkus.http.auth.permission.jax-rs1.applies-to=JAXRS
quarkus.http.auth.permission.standard1.paths=/no-authorization-policy/path-matching-http-perm
quarkus.http.auth.permission.standard1.policy=admin-role
quarkus.http.auth.permission.jax-rs2.paths=/authz-policy-and-path-matching-policies/jax-rs-path-matching-http-perm
quarkus.http.auth.permission.jax-rs2.policy=viewer-role
quarkus.http.auth.permission.jax-rs2.applies-to=JAXRS
quarkus.http.auth.permission.standard2.paths=/authz-policy-and-path-matching-policies/path-matching-http-perm
quarkus.http.auth.permission.standard2.policy=viewer-role
""";

@BeforeAll
public static void setupUsers() {
TestIdentityController.resetRoles()
.add("admin", "admin", "admin", "viewer")
.add("user", "user")
.add("viewer", "viewer", "viewer");
}

@Test
public void testNoAuthorizationPolicy() {
// unsecured endpoint
RestAssured.given().auth().preemptive().basic("viewer", "viewer").get("/no-authorization-policy/unsecured")
.then().statusCode(200).body(Matchers.equalTo("viewer"));

// secured with JAX-RS path-matching roles allowed HTTP permission requiring 'admin' role
RestAssured.given().auth().preemptive().basic("user", "user")
.get("/no-authorization-policy/jax-rs-path-matching-http-perm")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("admin", "admin")
.get("/no-authorization-policy/jax-rs-path-matching-http-perm")
.then().statusCode(200).body(Matchers.equalTo("admin"));

// secured with path-matching roles allowed HTTP permission requiring 'admin' role
RestAssured.given().auth().preemptive().basic("user", "user").get("/no-authorization-policy/path-matching-http-perm")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/no-authorization-policy/path-matching-http-perm")
.then().statusCode(200).body(Matchers.equalTo("admin"));

// secured with @RolesAllowed("admin")
RestAssured.given().auth().preemptive().basic("user", "user").get("/no-authorization-policy/roles-allowed-annotation")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/no-authorization-policy/roles-allowed-annotation")
.then().statusCode(200).body(Matchers.equalTo("admin"));
}

@Test
public void testMethodLevelAuthorizationPolicy() {
// policy placed on the endpoint directly, requires 'viewer' principal and must not pass anyone else
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/forbid-viewer-method-level-policy")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("viewer", "viewer").get("/forbid-viewer-method-level-policy")
.then().statusCode(200).body(Matchers.equalTo("viewer"));

// which means the other endpoint inside same resource class must not be affected by the policy
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/forbid-viewer-method-level-policy/unsecured")
.then().statusCode(200).body(Matchers.equalTo("admin"));
}

@Test
public void testClassLevelAuthorizationPolicy() {
// policy placed on the resource, requires 'viewer' principal and must not pass anyone else
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/forbid-viewer-class-level-policy")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("viewer", "viewer").get("/forbid-viewer-class-level-policy")
.then().statusCode(200).body(Matchers.equalTo("viewer"));
}

@Test
public void testAuthorizationPolicyOnMethodAndRolesAllowedOnClass() {
// class with @RolesAllowed("admin")
// method with @AuthorizationPolicy(policy = "permit-user")
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-allowed-class-authorization-policy-method")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("user", "user").get("/roles-allowed-class-authorization-policy-method")
.then().statusCode(200).body(Matchers.equalTo("user"));

// no @AuthorizationPolicy on method, therefore require admin
RestAssured.given().auth().preemptive().basic("user", "user")
.get("/roles-allowed-class-authorization-policy-method/no-authz-policy")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("admin", "admin")
.get("/roles-allowed-class-authorization-policy-method/no-authz-policy")
.then().statusCode(200).body(Matchers.equalTo("admin"));
}

@Test
public void testAuthorizationPolicyOnClassRolesAllowedOnMethod() {
// class with @AuthorizationPolicy(policy = "permit-user")
// method with @RolesAllowed("admin")
RestAssured.given().auth().preemptive().basic("user", "user").get("/authorization-policy-class-roles-allowed-method")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("admin", "admin").get("/authorization-policy-class-roles-allowed-method")
.then().statusCode(200).body(Matchers.equalTo("admin"));

// class with @AuthorizationPolicy(policy = "permit-user")
// method has no annotation, therefore expect to permit only the user
RestAssured.given().auth().preemptive().basic("admin", "admin")
.get("/authorization-policy-class-roles-allowed-method/no-roles-allowed")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("user", "user")
.get("/authorization-policy-class-roles-allowed-method/no-roles-allowed")
.then().statusCode(200).body(Matchers.equalTo("user"));
}

@Test
public void testCombinationOfAuthzPolicyAndPathConfigPolicies() {
// ViewerAugmentingPolicy adds 'admin' role to the viewer

// here we test that both @AuthorizationPolicy and path-matching policies work together
// viewer role is required by (JAX-RS) path-matching HTTP policies,
RestAssured.given().auth().preemptive().basic("admin", "admin")
.get("/authz-policy-and-path-matching-policies/jax-rs-path-matching-http-perm")
.then().statusCode(200).body(Matchers.equalTo("true"));
RestAssured.given().auth().preemptive().basic("viewer", "viewer")
.get("/authz-policy-and-path-matching-policies/jax-rs-path-matching-http-perm")
.then().statusCode(200).body(Matchers.equalTo("true"));
RestAssured.given().auth().preemptive().basic("user", "user")
.get("/authz-policy-and-path-matching-policies/jax-rs-path-matching-http-perm")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("admin", "admin")
.get("/authz-policy-and-path-matching-policies/path-matching-http-perm")
.then().statusCode(200).body(Matchers.equalTo("true"));
RestAssured.given().auth().preemptive().basic("viewer", "viewer")
.get("/authz-policy-and-path-matching-policies/path-matching-http-perm")
.then().statusCode(200).body(Matchers.equalTo("true"));
RestAssured.given().auth().preemptive().basic("user", "user")
.get("/authz-policy-and-path-matching-policies/path-matching-http-perm")
.then().statusCode(403);

// endpoint is annotated with @RolesAllowed("admin"), therefore class-level @AuthorizationPolicy is not applied
RestAssured.given().auth().preemptive().basic("admin", "admin")
.get("/authz-policy-and-path-matching-policies/roles-allowed-annotation")
.then().statusCode(200).body(Matchers.equalTo("admin"));
RestAssured.given().auth().preemptive().basic("viewer", "viewer")
.get("/authz-policy-and-path-matching-policies/roles-allowed-annotation")
.then().statusCode(403);
RestAssured.given().auth().preemptive().basic("user", "user")
.get("/authz-policy-and-path-matching-policies/roles-allowed-annotation")
.then().statusCode(403);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.resteasy.test.security.authzpolicy;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

import io.quarkus.vertx.http.security.AuthorizationPolicy;

@AuthorizationPolicy(name = "viewer-augmenting-policy")
@Path("authz-policy-and-path-matching-policies")
public class AuthorizationPolicyAndPathMatchingPoliciesResource {

@GET
@Path("jax-rs-path-matching-http-perm")
public boolean jaxRsPathMatchingHttpPerm(@Context SecurityContext securityContext) {
return securityContext.isUserInRole("admin");
}

@GET
@Path("path-matching-http-perm")
public boolean pathMatchingHttpPerm(@Context SecurityContext securityContext) {
return securityContext.isUserInRole("admin");
}

@RolesAllowed("admin")
@GET
@Path("roles-allowed-annotation")
public String rolesAllowed(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.resteasy.test.security.authzpolicy;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication;
import io.quarkus.vertx.http.security.AuthorizationPolicy;

@Path("basic-auth-ann")
@BasicAuthentication
public class BasicAuthenticationResource {

@GET
public String noAuthorizationPolicy(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

@Path("authorization-policy")
@AuthorizationPolicy(name = "forbid-all-but-viewer")
@GET
public String authorizationPolicy(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.resteasy.test.security.authzpolicy;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

import io.quarkus.vertx.http.security.AuthorizationPolicy;

@AuthorizationPolicy(name = "permit-user")
@Path("authorization-policy-class-roles-allowed-method")
public class ClassAuthZPolicyMethodRolesAllowedResource {

@RolesAllowed("admin")
@GET
public String principal(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

@Path("no-roles-allowed")
@GET
public String noAuthorizationPolicy(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.resteasy.test.security.authzpolicy;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

import io.quarkus.vertx.http.security.AuthorizationPolicy;

@RolesAllowed("admin")
@Path("roles-allowed-class-authorization-policy-method")
public class ClassRolesAllowedMethodAuthZPolicyResource {

@AuthorizationPolicy(name = "permit-user")
@GET
public String principal(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

@Path("no-authz-policy")
@GET
public String noAuthorizationPolicy(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}

}
Loading

0 comments on commit d6e82b7

Please sign in to comment.