Skip to content

Commit

Permalink
Support @PermissionsAllowed with @BeanParam parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Sep 17, 2024
1 parent c1531d0 commit 48e28a4
Show file tree
Hide file tree
Showing 22 changed files with 1,082 additions and 21 deletions.
106 changes: 106 additions & 0 deletions docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,112 @@ public @interface CanWrite {
----
<1> Any method or class annotated with the `@CanWrite` annotation is secured with this `@PermissionsAllowed` annotation instance.

[[permission-bean-params]]
=== Pass `@BeanParam` parameters into a custom permission

The `@PermissionsAllowed` annotation attribute `params` can refer to methods or fields of the secured method parameter.
You can use this feature to pass `jakarta.ws.rs.BeanParam` parameters into your custom permission.
Consider following `jakarta.ws.rs.BeanParam` class:

[source,java]
----
package org.acme.crud;
import java.util.List;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.UriInfo;
public class SimpleBeanParam {
@QueryParam("query")
private String privateQuery;
@HeaderParam("Authorization")
String authorizationHeader;
@Context
SecurityContext securityContext;
@Context
UriInfo uriInfo;
String getPrivateQuery() {
return privateQuery;
}
}
----

And let's say we have `SimplePermission` where we need to check `Authorization` header and principal name.

Check warning on line 1120 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.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 1120, "column": 51}}}, "severity": "INFO"}

Check warning on line 1120 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.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 1120, "column": 51}}}, "severity": "INFO"}
That can be done like this:

[source,java]
----
package org.acme.crud;
import java.security.Permission;
import java.util.Objects;
public class SimplePermission extends Permission {
private final String authorizationHeader;
private final String principalName;
public SimpleBeanParamPermission(String permissionName, String authorizationHeader, String name, String privateQuery) { <1> <2>
super(permissionName);
this.authorizationHeader = authorizationHeader;
this.principalName = name;
if ("private-query".equals(privateQuery)) {
// your business logic
}
}
@Override
public boolean implies(Permission requiredPermission) {
if (requiredPermission instanceof SimpleBeanParamPermission that) {
return "MyAuthorization".equals(that.authorizationHeader)
&& "admin".equals(that.principalName);
}
return false;
}
...
}
----
<1> Constructor parameters `authorizationHeader` and `principalName` corresponds to `SimpleBeanParam` fields with same names.
<2> Constructor parameter `privateQuery` is accessed with a getter method.
Quarkus always prefer method access, therefore, first we look for the `privateQuery` method, then for the `getPrivateQuery` method.
If neither method can be found on the `SimpleBeanParam` class, Quarkus tries to find the `privateQuery` field.

The `SimplePermission` can be used on your Jakarta REST resource like in the example below:

[source,java]
----
package org.acme.crud;
import io.quarkus.security.PermissionsAllowed;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/hello")
public class SimpleResource {
@PermissionsAllowed(value = "read", permission = SimplePermission.class,
params = { "param.authorizationHeader", "param.privateQuery", "param.securityContext.getUserPrincipal.name" }) <1>
@GET
public String sayHello(@BeanParam SimpleBeanParam param) {
return "Hello path is " + param.uriInfo.getPath();
}
}
----
<1> `SecurityIdentity` principal is accessed from the `jakarta.ws.rs.core.SecurityContext`.
Please note that the `name` part of the `param.securityContext.getUserPrincipal.name` expression matches the `SimplePermission` parameter `name`.

Check warning on line 1185 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.Fluff] Depending on the context, consider using 'Be concise: rewrite the sentence to not use' rather than 'note that'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Be concise: rewrite the sentence to not use' rather than 'note that'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 1185, "column": 8}}}, "severity": "INFO"}

Check warning on line 1185 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.Fluff] Depending on the context, consider using 'Be concise: rewrite the sentence to not use' rather than 'note that'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Be concise: rewrite the sentence to not use' rather than 'note that'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 1185, "column": 8}}}, "severity": "INFO"}

== References

* xref:security-overview.adoc[Quarkus Security overview]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.quarkus.resteasy.reactive.server.test.security;

import jakarta.ws.rs.BeanParam;

import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestQuery;

public record MyBeanParam(@RestQuery String queryParam, @BeanParam Headers headers) {
record Headers(@RestHeader String authorization) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.quarkus.resteasy.reactive.server.test.security;

import java.security.Permission;
import java.util.Objects;

public class MyPermission extends Permission {

static final MyPermission EMPTY = new MyPermission("my-perm", null, null);

private final String authorization;
private final String queryParam;

public MyPermission(String permissionName, String authorization, String queryParam) {
super(permissionName);
this.authorization = authorization;
this.queryParam = queryParam;
}

@Override
public boolean implies(Permission permission) {
if (permission instanceof MyPermission myPermission) {
return myPermission.authorization != null && "query1".equals(myPermission.queryParam);
}
return false;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MyPermission that = (MyPermission) o;
return Objects.equals(authorization, that.authorization)
&& Objects.equals(queryParam, that.queryParam);
}

@Override
public int hashCode() {
return Objects.hash(authorization, queryParam);
}

@Override
public String getActions() {
return "";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.quarkus.resteasy.reactive.server.test.security;

import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

import org.hamcrest.Matchers;
import org.jboss.resteasy.reactive.RestCookie;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;

public class PermissionsAllowedBeanParamTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(TestIdentityProvider.class, TestIdentityController.class, SimpleBeanParam.class,
SimpleResource.class, SimpleBeanParamPermission.class, MyPermission.class, MyBeanParam.class));

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

@Test
public void testSimpleBeanParam() {
getSimpleBeanParamReq()
.post("/simple/param")
.then().statusCode(401);
getSimpleBeanParamReq()
.auth().preemptive().basic("user", "user")
.post("/simple/param")
.then().statusCode(403);
getSimpleBeanParamReq()
.auth().preemptive().basic("admin", "admin")
.post("/simple/param")
.then().statusCode(200).body(Matchers.equalTo("OK"));
}

@Test
public void testRecordBeanParam() {
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.queryParam("queryParam", "query1")
.get("/simple/record-param")
.then().statusCode(403);
RestAssured
.given()
.auth().preemptive().basic("admin", "admin")
.queryParam("queryParam", "query1")
.get("/simple/record-param")
.then().statusCode(200)
.body(Matchers.equalTo("OK"));
RestAssured
.given()
.auth().preemptive().basic("admin", "admin")
.queryParam("queryParam", "wrong-query-param")
.get("/simple/record-param")
.then().statusCode(403);
}

private static RequestSpecification getSimpleBeanParamReq() {
return RestAssured
.with()
.header("header", "one-header")
.queryParam("query", "one-query")
.queryParam("queryList", "one")
.queryParam("queryList", "two")
.queryParam("int", "666")
.cookie("cookie", "cookie")
.body("OK");
}

@Path("/simple")
public static class SimpleResource {

@PermissionsAllowed(value = "perm1", permission = SimpleBeanParamPermission.class, params = { "cookie",
"beanParam.query",
"beanParam.protectedQuery", "beanParam.publicQuery", "beanParam.header", "beanParam.queryList",
"beanParam.securityContext", "beanParam.uriInfo", "beanParam.privateQuery" })
@Path("/param")
@POST
public String simpleBeanParam(@BeanParam SimpleBeanParam beanParam, String payload, @RestCookie String cookie) {
return payload;
}

@PermissionsAllowed(value = "perm2", permission = MyPermission.class, params = { "beanParam.queryParam",
"beanParam.headers.authorization" })
@Path("/record-param")
@GET
public String recordBeanParam(@BeanParam MyBeanParam beanParam) {
return "OK";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.resteasy.reactive.server.test.security;

import java.util.List;

import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.UriInfo;

public class SimpleBeanParam {
@QueryParam("query")
String query;

@QueryParam("query")
private String privateQuery;

@QueryParam("query")
protected String protectedQuery;

@QueryParam("query")
public String publicQuery;

@HeaderParam("header")
String header;

@QueryParam("queryList")
List<String> queryList;

@Context
SecurityContext securityContext;

@Context
UriInfo uriInfo;

String getPrivateQuery() {
return privateQuery;
}
}
Loading

0 comments on commit 48e28a4

Please sign in to comment.