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 30, 2024
1 parent 6c99b6c commit 03afb70
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -911,8 +911,8 @@ public class LibraryService {
@PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class) <1>
public Library updateLibrary(String newDesc, Library library) {
update.description = newDesc;
return update;
library.description = newDesc;
return library;
}
@PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class) <2>
Expand All @@ -925,9 +925,9 @@ public class LibraryService {
}
----
<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.
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 second `Library` parameter has matching name `library`,
<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 @@ -1107,11 +1107,11 @@ public class SimpleResource {
}
----
<1> The `params` annotation attribute specifies that user principal should be passed to the `BeanParamPermission` constructor.
<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 explicitly.
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:

Expand Down Expand Up @@ -1179,22 +1179,63 @@ public class BeanParamPermission extends Permission {
}
@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;
public boolean implies(Permission possessedPermission) {
boolean permissionNameMatches = permissionName.equals(possessedPermission.getName());
boolean queryParamAllowedForPermissionName = checkQueryParams(queryParam);
boolean usernameWhitelisted = isUserNameWhitelisted(userName);
boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorization);
return permissionNameMatches && queryParamAllowedForPermissionName && usernameWhitelisted
&& customAuthorizationMatches;
}
...
}
----

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
Expand Up @@ -21,16 +21,13 @@ public OtherBeanParamPermission(String permissionName, String customAuthorizatio
}

@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;
public boolean implies(Permission possessedPermission) {
boolean permissionNameMatches = permissionName.equals(possessedPermission.getName());
boolean queryParamAllowedForPermissionName = checkQueryParams(queryParam);
boolean usernameWhitelisted = isUserNameWhitelisted(userName);
boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorization);
return permissionNameMatches && queryParamAllowedForPermissionName && usernameWhitelisted
&& customAuthorizationMatches;
}

private static boolean checkCustomAuthorization(String customAuthorization) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ public class PermissionsAllowedBeanParamTest {
.withApplicationRoot((jar) -> jar
.addClasses(TestIdentityProvider.class, TestIdentityController.class, SimpleBeanParam.class,
SimpleResource.class, SimpleBeanParamPermission.class, MyPermission.class, MyBeanParam.class,
OtherBeanParamPermission.class, OtherBeanParam.class));
OtherBeanParamPermission.class, OtherBeanParam.class, BeanParamPermissionIdentityAugmentor.class));

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

@Test
Expand Down

0 comments on commit 03afb70

Please sign in to comment.