Skip to content

Commit

Permalink
Merge pull request #44344 from sberyozkin/add_permission
Browse files Browse the repository at this point in the history
Add permission checker shortcuts to QuarkusSecurityIdentity.Builder
  • Loading branch information
sberyozkin authored Nov 12, 2024
2 parents 0037f52 + 94f7050 commit 6775bf1
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Permission, Uni<Boolean>>() { <2>
@Override
public Uni<Boolean> 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`.

Expand All @@ -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>
Expand Down Expand Up @@ -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<Boolean>`. 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]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@
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;
import java.util.SortedMap;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -393,38 +391,8 @@ static void setSecurityIdentityPermissions(QuarkusSecurityIdentity.Builder build

static void addTokenScopesAsPermissions(Builder builder, Collection<String> scopes) {
if (!scopes.isEmpty()) {
builder.addPermissionChecker(new Function<Permission, Uni<Boolean>>() {

private final Permission[] permissions = transformScopesToPermissions(scopes);

@Override
public Uni<Boolean> 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<String> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -144,6 +146,7 @@ public static class Builder {
Principal principal;
Set<String> roles = new HashSet<>();
Set<Credential> credentials = new HashSet<>();
Set<Permission> permissions = new HashSet<>();
Map<String, Object> attributes = new HashMap<>();
List<Function<Permission, Uni<Boolean>>> permissionCheckers = new ArrayList<>();
private boolean anonymous;
Expand Down Expand Up @@ -205,6 +208,48 @@ public Builder addAttributes(Map<String, Object> 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<String> 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<Permission> permissions) {
this.permissions.addAll(permissions);
return this;
}

/**
* Adds a permission checker function. This permission checker has the following semantics:
*
Expand Down Expand Up @@ -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<Permission, Uni<Boolean>>() {

@Override
public Uni<Boolean> 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);
}

}
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,13 +15,94 @@

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;
import io.smallrye.mutiny.Uni;

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()
Expand Down

0 comments on commit 6775bf1

Please sign in to comment.