From 94f7050e11ae6dfb9483da9965bd05678000e3bf Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 6 Nov 2024 10:25:33 +0000 Subject: [PATCH] Add permission checker shortcuts to QuarkusSecurityIdentity.builder --- ...ity-authorize-web-endpoints-reference.adoc | 18 +--- .../io/quarkus/oidc/runtime/OidcUtils.java | 36 +------- .../quarkus/oidc/runtime/OidcUtilsTest.java | 17 ---- .../runtime/QuarkusSecurityIdentity.java | 79 ++++++++++++++++++ .../runtime/QuarkusSecurityIdentityTest.java | 82 +++++++++++++++++++ 5 files changed, 167 insertions(+), 65 deletions(-) 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 b2ce601cfcae0..151064e376e9a 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -1008,7 +1008,6 @@ In the following example, xref:security-customization.adoc#security-identity-cus [source,java] ---- import java.security.Permission; -import java.util.function.Function; import jakarta.enterprise.context.ApplicationScoped; @@ -1034,24 +1033,15 @@ public class PermissionsIdentityAugmentor implements SecurityIdentityAugmentor { } SecurityIdentity build(SecurityIdentity identity) { - Permission possessedPermission = new MediaLibraryPermission("media-library", - new String[] { "read", "write", "list"}); <1> return QuarkusSecurityIdentity.builder(identity) - .addPermissionChecker(new Function>() { <2> - @Override - public Uni apply(Permission requiredPermission) { - boolean accessGranted = possessedPermission.implies(requiredPermission); - return Uni.createFrom().item(accessGranted); - } - }) + .addPermission(new MediaLibraryPermission("media-library", new String[] { "read", "write", "list"}); <1> .build(); } } ---- -<1> The permission `media-library` that was created can perform `read`, `write`, and `list` actions. +<1> Add a `media-library` permission that was created can perform `read`, `write`, and `list` actions. Because `MediaLibrary` is the `TvLibrary` class parent, a user with the `admin` role is also permitted to modify `TvLibrary`. -<2> You can add a permission checker through `io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder#addPermissionChecker`. CAUTION: Annotation-based permissions do not work with custom xref:security-customization.adoc#jaxrs-security-context[Jakarta REST SecurityContexts] because there are no permissions in `jakarta.ws.rs.core.SecurityContext`. @@ -1076,7 +1066,7 @@ import jakarta.ws.rs.Path; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestPath; -@Path("/project") +@Path("/project/{projectName}") public class ProjectResource { @PermissionsAllowed("rename-project") <1> @@ -1129,7 +1119,7 @@ public class ProjectPermissionChecker { <1> A CDI bean with the permission checker must be either a normal scoped bean or a `@Singleton` bean. <2> The permission checker method must return either `boolean` or `Uni`. Private checker methods are not supported. -TIP: Permission checks run by default on event loops whenever possible. +TIP: Permission checks run by default on event loops. Annotate a permission checker method with the `io.smallrye.common.annotation.Blocking` annotation if you want to run the check on a worker thread. [[permission-meta-annotation]] diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 3444f128ac770..ff2689d96b373 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -7,13 +7,13 @@ import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.Permission; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -21,7 +21,6 @@ import java.util.StringTokenizer; import java.util.TreeMap; import java.util.function.Consumer; -import java.util.function.Function; import java.util.regex.Pattern; import javax.crypto.SecretKey; @@ -50,7 +49,6 @@ import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.runtime.providers.KnownOidcProviders; import io.quarkus.security.AuthenticationFailedException; -import io.quarkus.security.StringPermission; import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.SecurityIdentity; @@ -393,38 +391,8 @@ static void setSecurityIdentityPermissions(QuarkusSecurityIdentity.Builder build static void addTokenScopesAsPermissions(Builder builder, Collection scopes) { if (!scopes.isEmpty()) { - builder.addPermissionChecker(new Function>() { - - private final Permission[] permissions = transformScopesToPermissions(scopes); - - @Override - public Uni apply(Permission requiredPermission) { - for (Permission possessedPermission : permissions) { - if (possessedPermission.implies(requiredPermission)) { - // access granted - return Uni.createFrom().item(Boolean.TRUE); - } - } - // access denied - return Uni.createFrom().item(Boolean.FALSE); - } - }); - } - } - - static Permission[] transformScopesToPermissions(Collection scopes) { - final Permission[] permissions = new Permission[scopes.size()]; - int i = 0; - for (String scope : scopes) { - int semicolonIndex = scope.indexOf(':'); - if (semicolonIndex > 0 && semicolonIndex < scope.length() - 1) { - permissions[i++] = new StringPermission(scope.substring(0, semicolonIndex), - scope.substring(semicolonIndex + 1)); - } else { - permissions[i++] = new StringPermission(scope); - } + builder.addPermissionsAsString(new HashSet<>(scopes)); } - return permissions; } public static void setSecurityIdentityRoles(QuarkusSecurityIdentity.Builder builder, OidcTenantConfig config, diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index d4d85a4be26d2..15f03705d2936 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -11,7 +11,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.security.Permission; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -264,22 +263,6 @@ public void testDecodeJwt() throws Exception { assertTrue(json.containsKey("jti")); } - @Test - public void testTransformScopeToPermission() throws Exception { - Permission[] perms = OidcUtils.transformScopesToPermissions( - List.of("read", "read:d", "read:", ":read")); - assertEquals(4, perms.length); - - assertEquals("read", perms[0].getName()); - assertNull(perms[0].getActions()); - assertEquals("read", perms[1].getName()); - assertEquals("d", perms[1].getActions()); - assertEquals("read:", perms[2].getName()); - assertNull(perms[2].getActions()); - assertEquals(":read", perms[3].getName()); - assertNull(perms[3].getActions()); - } - @Test public void testEncodeScopesOpenidAdded() throws Exception { OidcTenantConfig config = new OidcTenantConfig(); diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityIdentity.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityIdentity.java index b99d800fcef6e..ad0ab17ae8515 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityIdentity.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityIdentity.java @@ -10,7 +10,9 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; +import io.quarkus.security.StringPermission; import io.quarkus.security.credential.Credential; import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.mutiny.Uni; @@ -144,6 +146,7 @@ public static class Builder { Principal principal; Set roles = new HashSet<>(); Set credentials = new HashSet<>(); + Set permissions = new HashSet<>(); Map attributes = new HashMap<>(); List>> permissionCheckers = new ArrayList<>(); private boolean anonymous; @@ -205,6 +208,48 @@ public Builder addAttributes(Map attributes) { return this; } + /** + * Adds a permission as String. + * + * @param permission The permission in a String format. + * @return This builder + */ + public Builder addPermissionAsString(String permission) { + return addPermissionsAsString(Set.of(permission)); + } + + /** + * Adds permissions as String + * + * @param permissions The permissions in a String format. + * @return This builder + */ + public Builder addPermissionsAsString(Set permissions) { + return addPermissions(permissions.stream().map(p -> toPermission(p)) + .collect(Collectors.toSet())); + } + + /** + * Adds a permission. + * + * @param permission The permission + * @return This builder + */ + public Builder addPermission(Permission permission) { + return addPermissions(Set.of(permission)); + } + + /** + * Adds permissions. + * + * @param permissions The permissions + * @return This builder + */ + public Builder addPermissions(Set permissions) { + this.permissions.addAll(permissions); + return this; + } + /** * Adds a permission checker function. This permission checker has the following semantics: * @@ -258,9 +303,43 @@ public QuarkusSecurityIdentity build() { if (principal == null && !anonymous) { throw new IllegalStateException("Principal is null but anonymous status is false"); } + addPossesedPermissionsChecker(); built = true; return new QuarkusSecurityIdentity(this); } + + private void addPossesedPermissionsChecker() { + if (!permissions.isEmpty()) { + addPermissionChecker( + new Function>() { + + @Override + public Uni apply(Permission requiredPermission) { + + for (Permission possessedPermission : permissions) { + if (possessedPermission.implies(requiredPermission)) { + return Uni.createFrom().item(true); + } + } + return Uni.createFrom().item(false); + + } + }); + } + + } + + static Permission toPermission(String permissionAsString) { + int semicolonIndex = permissionAsString.indexOf(':'); + if (semicolonIndex > 0 && semicolonIndex < permissionAsString.length() - 1) { + return new StringPermission(permissionAsString.substring(0, semicolonIndex), + permissionAsString.substring(semicolonIndex + 1)); + } else { + return new StringPermission(permissionAsString); + } + + } } + } diff --git a/extensions/security/runtime/src/test/java/io/quarkus/security/runtime/QuarkusSecurityIdentityTest.java b/extensions/security/runtime/src/test/java/io/quarkus/security/runtime/QuarkusSecurityIdentityTest.java index 7d9dc11e67be3..65cf83651e861 100644 --- a/extensions/security/runtime/src/test/java/io/quarkus/security/runtime/QuarkusSecurityIdentityTest.java +++ b/extensions/security/runtime/src/test/java/io/quarkus/security/runtime/QuarkusSecurityIdentityTest.java @@ -1,5 +1,6 @@ package io.quarkus.security.runtime; +import static io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder.toPermission; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test; +import io.quarkus.security.StringPermission; import io.quarkus.security.credential.Credential; import io.quarkus.security.credential.PasswordCredential; import io.quarkus.security.identity.SecurityIdentity; @@ -21,6 +23,86 @@ public class QuarkusSecurityIdentityTest { + @Test + public void testAddPermissionAsString() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("alice")) + .addPermissionAsString("read") + .build(); + + assertTrue(identity.checkPermissionBlocking(new StringPermission("read"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("write"))); + } + + @Test + public void testAddPermissionWithActionAsString() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("alice")) + .addPermissionAsString("read:singledoc") + .build(); + + assertTrue(identity.checkPermissionBlocking(new StringPermission("read", "singledoc"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("read", "all"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("write"))); + } + + @Test + public void testAddPermissionsAsString() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("alice")) + .addPermissionsAsString(Set.of("read", "write")) + .build(); + + assertTrue(identity.checkPermissionBlocking(new StringPermission("read"))); + assertTrue(identity.checkPermissionBlocking(new StringPermission("write"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("comment"))); + } + + @Test + public void testAddPermissionsWithActionAsString() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("alice")) + .addPermissionsAsString(Set.of("read:singledoc", "write:singledoc")) + .build(); + + assertTrue(identity.checkPermissionBlocking(new StringPermission("read", "singledoc"))); + assertTrue(identity.checkPermissionBlocking(new StringPermission("write", "singledoc"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("read:all"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("write:all"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("comment"))); + } + + @Test + public void testAddPermission() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("alice")) + .addPermission(new StringPermission("read")) + .build(); + + assertTrue(identity.checkPermissionBlocking(new StringPermission("read"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("write"))); + } + + @Test + public void testAddPermissions() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("alice")) + .addPermissions(Set.of(new StringPermission("read"), new StringPermission("write"))) + .build(); + + assertTrue(identity.checkPermissionBlocking(new StringPermission("read"))); + assertTrue(identity.checkPermissionBlocking(new StringPermission("write"))); + assertFalse(identity.checkPermissionBlocking(new StringPermission("comment"))); + } + + @Test + public void testConvertStringToPermission() throws Exception { + assertEquals(toPermission("read"), new StringPermission("read")); + assertEquals(toPermission("read:d"), new StringPermission("read", "d")); + assertEquals(toPermission("read:"), new StringPermission("read:")); + assertEquals(toPermission(":read"), new StringPermission(":read")); + } + @Test public void testCopyIdentity() throws Exception { SecurityIdentity identity1 = QuarkusSecurityIdentity.builder()