Skip to content

Commit

Permalink
Add new AuthorizationPolicy annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Aug 25, 2024
1 parent 2105d46 commit a2ef0ca
Show file tree
Hide file tree
Showing 60 changed files with 2,052 additions and 321 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.

Check failure on line 497 in docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spacing] Keep one space between words in 's.H'. Raw Output: {"message": "[Quarkus.Spacing] Keep one space between words in 's.H'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 497, "column": 159}}}, "severity": "ERROR"}
Named HttpSecurityPolicy can be used for general authorization checks as demonstrated by <<authorization-policy-example>>.

Check warning on line 498 in docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 498, "column": 71}}}, "severity": "INFO"}
|===

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 a2ef0ca

Please sign in to comment.