From 03afb70a3fd6bb9f12dd6c36c4924efcfc62ab1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Mon, 30 Sep 2024 15:22:21 +0200 Subject: [PATCH] Support @PermissionsAllowed with @BeanParam parameters --- ...ity-authorize-web-endpoints-reference.adoc | 73 +++++++++++++++---- .../BeanParamPermissionIdentityAugmentor.java | 34 +++++++++ .../security/OtherBeanParamPermission.java | 17 ++--- .../PermissionsAllowedBeanParamTest.java | 6 +- 4 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/BeanParamPermissionIdentityAugmentor.java diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index b14b2ed0c059cb..3acc19a918bc2e 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -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> @@ -925,9 +925,9 @@ public class LibraryService { } ---- <1> The formal parameter `library` is identified as the parameter matching same-named `LibraryPermission` constructor parameter. -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` @@ -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: @@ -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 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] diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/BeanParamPermissionIdentityAugmentor.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/BeanParamPermissionIdentityAugmentor.java new file mode 100644 index 00000000000000..f1bab3d31aafe3 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/BeanParamPermissionIdentityAugmentor.java @@ -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 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"); + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/OtherBeanParamPermission.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/OtherBeanParamPermission.java index 1185b08503d603..80a00b6cb0e190 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/OtherBeanParamPermission.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/OtherBeanParamPermission.java @@ -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) { diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedBeanParamTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedBeanParamTest.java index ec346ab2310e58..a436e6ab89cd4f 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedBeanParamTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedBeanParamTest.java @@ -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