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 Oct 1, 2024
1 parent 4cab5df commit 0a03ff2
Show file tree
Hide file tree
Showing 27 changed files with 1,509 additions and 112 deletions.
177 changes: 169 additions & 8 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.

.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) {
update.description = newDesc;
return update;
public Library updateLibrary(String newDesc, Library library) {
library.description = newDesc;
return library;
}
@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.
Therefore, Quarkus will 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 matches the 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,166 @@ 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 = "say:hello", 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 name 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 name 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 actions;
public BeanParamPermission(String permissionName, String customAuthorizationHeader, String name, String query) {
super(permissionName);
this.actions = computeActions(customAuthorizationHeader, name, query);
}
@Override
public boolean implies(Permission p) {
boolean nameMatches = getName().equals(p.getName());
boolean actionMatches = actions.equals(p.getActions());
return nameMatches && actionMatches;
}
private static String computeActions(String customAuthorizationHeader, String name, String query) {
boolean queryParamAllowedForPermissionName = checkQueryParams(query);
boolean usernameWhitelisted = isUserNameWhitelisted(name);
boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorizationHeader);
var isAuthorized = queryParamAllowedForPermissionName && usernameWhitelisted && customAuthorizationMatches;
if (isAuthorized) {
return "hello";
} else {
return "goodbye";
}
}
...
}
----

Permission `BeanParamPermission` is required to access the `SimpleResource#sayHello` endpoint.
Access will only be granted if you add `possessedPermission` from the previous example to your `SecurityIdentity`:

[source,java]
----
package org.acme.security.permission;
import java.security.Permission;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.security.StringPermission;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
@ApplicationScoped
public class PermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity securityIdentity,
AuthenticationRequestContext authenticationRequestContext) {
var possessedPermission = createPossessedPermission(securityIdentity);
var augmentedIdentity = QuarkusSecurityIdentity
.builder(securityIdentity)
.addPermissionChecker(requiredPermission -> Uni
.createFrom()
.item(requiredPermission.implies(possessedPermission)))
.build();
return Uni.createFrom().item(augmentedIdentity);
}
private static Permission createPossessedPermission(SecurityIdentity securityIdentity) {
// replace next line with your business logic
return securityIdentity.isAnonymous() ? new StringPermission("ping") : new StringPermission("read");
}
}
----

NOTE: You can pass `@BeanParam` directly into a custom permission constructor and access its fields programmatically in the constructor instead.
Ability to reference `@BeanParam` fields with the `@PermissionsAllowed#params` attribute is useful when you have multiple differently structured `@BeanParam` classes.

== References

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

import java.security.Permission;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.security.StringPermission;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class BeanParamPermissionIdentityAugmentor implements SecurityIdentityAugmentor {

@Override
public Uni<SecurityIdentity> augment(SecurityIdentity securityIdentity,
AuthenticationRequestContext authenticationRequestContext) {
var possessedPermission = createPossessedPermission(securityIdentity);
var augmentedIdentity = QuarkusSecurityIdentity
.builder(securityIdentity)
.addPermissionChecker(requiredPermission -> Uni
.createFrom()
.item(requiredPermission.implies(possessedPermission)))
.build();
return Uni.createFrom().item(augmentedIdentity);
}

private Permission createPossessedPermission(SecurityIdentity securityIdentity) {
// here comes your business logic
return securityIdentity.isAnonymous() ? new StringPermission("list") : new StringPermission("read");
}
}
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,60 @@
package io.quarkus.resteasy.reactive.server.test.security;

import java.security.Permission;

public class OtherBeanParamPermission extends Permission {

private final String actions;

public OtherBeanParamPermission(String permissionName, String customAuthorizationHeader, String name, String query) {
super(permissionName);
this.actions = computeActions(customAuthorizationHeader, name, query);
}

@Override
public String getActions() {
return actions;
}

@Override
public boolean implies(Permission p) {
boolean nameMatches = getName().equals(p.getName());
boolean actionMatches = getActions().equals(p.getActions());
return nameMatches && actionMatches;
}

@Override
public boolean equals(Object obj) {
return false;
}

@Override
public int hashCode() {
return 0;
}

private static String computeActions(String customAuthorizationHeader, String name, String query) {
boolean queryParamAllowedForPermissionName = checkQueryParams(query);
boolean usernameWhitelisted = isUserNameWhitelisted(name);
boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorizationHeader);
var isAuthorized = queryParamAllowedForPermissionName && usernameWhitelisted && customAuthorizationMatches;
if (isAuthorized) {
return "hello";
} else {
return "goodbye";
}
}

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 0a03ff2

Please sign in to comment.