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 29, 2024
1 parent 2811eec commit 05fa0bb
Show file tree
Hide file tree
Showing 26 changed files with 1,403 additions and 110 deletions.
129 changes: 123 additions & 6 deletions docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@ Your custom class must define exactly one constructor that accepts the permissio
In this scenario, the permission `list` is added to the `SecurityIdentity` instance as `new CustomPermission("list")`.

You can also create a custom `java.security.Permission` class with additional constructor parameters.
These additional parameters get matched with arguments of the method annotated with the `@PermissionsAllowed` annotation.
These additional parameters names get matched with arguments names of the method annotated with the `@PermissionsAllowed` annotation.
Later, Quarkus instantiates your custom permission with actual arguments, with which the method annotated with the `@PermissionsAllowed` has been invoked.

Check warning on line 834 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 ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 834, "column": 79}}}, "severity": "INFO"}

.Example of a custom `java.security.Permission` class that accepts additional arguments
Expand Down Expand Up @@ -910,12 +910,12 @@ import org.acme.library.LibraryPermission.Library;
public class LibraryService {
@PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class) <1>
public Library updateLibrary(String newDesc, Library update) {
public Library updateLibrary(String newDesc, Library library) {
update.description = newDesc;
return update;
}
@PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class, params = "library") <2>
@PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class) <2>
@PermissionsAllowed(value = {"tv:read", "tv:list"}, permission = LibraryPermission.class)
public Library migrateLibrary(Library migrate, Library library) {
// migrate libraries
Expand All @@ -924,10 +924,11 @@ public class LibraryService {
}
----
<1> The formal parameter `update` is identified as the first `Library` parameter and gets passed to the `LibraryPermission` class.
<1> The formal parameter `library` is identified as the parameter matching same-named `LibraryPermission` constructor parameter.

Check warning on line 927 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": 927, "column": 50}}}, "severity": "INFO"}
Therefore, Quarkus pass the `library` parameter to the `LibraryPermission` class constructor.
However, the `LibraryPermission` must be instantiated each time the `updateLibrary` method is invoked.
<2> Here, the first `Library` parameter is `migrate`; therefore, the `library` parameter gets marked explicitly through `PermissionsAllowed#params`.
The permission constructor and the annotated method must have the parameter `library` set; otherwise, validation fails.
<2> Here, the second `Library` parameter has matching name `library`,
while the `migrate` parameter is ignored during the `LibraryPermission` permission instantiation.

.Example of a resource secured with the `LibraryPermission`

Expand Down Expand Up @@ -1078,6 +1079,122 @@ 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

Quarkus can map fields of a secured method parameters to a custom permission constructor parameters.
You can use this feature to pass `jakarta.ws.rs.BeanParam` parameters into your custom permission.
Let's consider following Jakarta REST resource:

[source,java]
----
package org.acme.security.rest.resource;
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 = BeanParamPermission.class,
params = "beanParam.securityContext.userPrincipal.name") <1>
@GET
public String sayHello(@BeanParam SimpleBeanParam beanParam) {
return "Hello from " + beanParam.uriInfo.getPath();
}
}
----
<1> The `params` annotation attribute specifies that user principal should be passed to the `BeanParamPermission` constructor.
Other `BeanParamPermission` constructor parameters like `customAuthorizationHeader` and `query` are matched automatically.
Quarkus identifies the `BeanParamPermission` constructor parameters among `beanParam` fields and their public accessors.
To avoid ambiguous resolution, automatic detection only works for the `beanParam` fields.
For that reason, we had to specify path to the user principal explicitly.

Where the `SimpleBeanParam` class is declared like in the example below:

[source,java]
----
package org.acme.security.rest.dto;
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 {
@HeaderParam("CustomAuthorization")
private String customAuthorizationHeader;
@Context
SecurityContext securityContext;
@Context
public UriInfo uriInfo;
@QueryParam("query")
public String query; <1>
public SecurityContext getSecurityContext() { <2>
return securityContext;
}
public String customAuthorizationHeader() { <3>
return customAuthorizationHeader;
}
}
----
<1> Quarkus Security can only pass public fields to a custom permission constructor.
<2> Quarkus Security automatically uses public getter methods if they are available.
<3> The `customAuthorizationHeader` field is not public, therefore Quarkus access this field with the `customAuthorizationHeader` accessor.
That is particularly useful with Java records, where generated accessors are not prefixed with `get`.

Here is an example of the `BeanParamPermission` permission that checks user principal, custom header and query parameter:

[source,java]
----
package org.acme.security.permission;
import java.security.Permission;
public class BeanParamPermission extends Permission {
private final String permissionName;
private final String customAuthorization;
private final String userName;
private final String queryParam;
public BeanParamPermission(String permissionName, String customAuthorizationHeader, String name, String query) {
super(permissionName);
this.permissionName = permissionName;
this.customAuthorization = customAuthorizationHeader;
this.userName = name;
this.queryParam = query;
}
@Override
public boolean implies(Permission permission) {
if (permission instanceof BeanParamPermission that) {
boolean permissionNameMatches = permissionName.equals(that.permissionName);
boolean queryParamAllowedForPermissionName = checkQueryParams(that.queryParam);
boolean usernameWhitelisted = isUserNameWhitelisted(that.userName);
boolean customAuthorizationMatches = checkCustomAuthorization(that.customAuthorization);
return permissionNameMatches && queryParamAllowedForPermissionName
&& usernameWhitelisted && customAuthorizationMatches;
}
return false;
}
...
}
----

== 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) {
public 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,30 @@
package io.quarkus.resteasy.reactive.server.test.security;

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 OtherBeanParam {

@HeaderParam("CustomAuthorization")
private String customAuthorizationHeader;

@Context
SecurityContext securityContext;

@Context
public UriInfo uriInfo;

@QueryParam("query")
public String query;

public SecurityContext getSecurityContext() {
return securityContext;
}

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

import java.security.BasicPermission;
import java.security.Permission;

public class OtherBeanParamPermission extends BasicPermission {

static final OtherBeanParamPermission READ = new OtherBeanParamPermission("read", null, null, null);

private final String permissionName;
private final String customAuthorization;
private final String userName;
private final String queryParam;

public OtherBeanParamPermission(String permissionName, String customAuthorizationHeader, String name, String query) {
super(permissionName);
this.permissionName = permissionName;
this.customAuthorization = customAuthorizationHeader;
this.userName = name;
this.queryParam = query;
}

@Override
public boolean implies(Permission permission) {
if (permission instanceof OtherBeanParamPermission that) {
boolean permissionNameMatches = permissionName.equals(that.permissionName);
boolean queryParamAllowedForPermissionName = checkQueryParams(that.queryParam);
boolean usernameWhitelisted = isUserNameWhitelisted(that.userName);
boolean customAuthorizationMatches = checkCustomAuthorization(that.customAuthorization);
return permissionNameMatches && queryParamAllowedForPermissionName && usernameWhitelisted
&& customAuthorizationMatches;
}
return false;
}

private static boolean checkCustomAuthorization(String customAuthorization) {
return "customAuthorization".equals(customAuthorization);
}

private static boolean isUserNameWhitelisted(String userName) {
return "admin".equals(userName);
}

private static boolean checkQueryParams(String queryParam) {
return "myQueryParam".equals(queryParam);
}

}
Loading

0 comments on commit 05fa0bb

Please sign in to comment.