Skip to content

Commit

Permalink
Allow permission checks via @PermissionsAllowed security annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Feb 22, 2023
1 parent 181dae5 commit 3a525d5
Show file tree
Hide file tree
Showing 49 changed files with 4,405 additions and 20 deletions.
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
<mockito.version>5.1.1</mockito.version>
<jna.version>5.8.0</jna.version><!-- should satisfy both testcontainers and mongodb -->
<antlr.version>4.10.1</antlr.version><!-- needs to align with same property in build-parent/pom.xml -->
<quarkus-security.version>2.0.1.Final</quarkus-security.version>
<quarkus-security.version>2.0.2.Final-SNAPSHOT</quarkus-security.version>
<keycloak.version>20.0.3</keycloak.version>
<logstash-gelf.version>1.15.0</logstash-gelf.version>
<checker-qual.version>3.31.0</checker-qual.version>
Expand Down
180 changes: 180 additions & 0 deletions docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,186 @@ It is possible to use multiple expressions in the role definition.
<3> This `/subject/user` endpoint requires an authenticated user that has been granted the role "User" through the use of the `@RolesAllowed("${customer:User}")` annotation, as we did not set the configuration property `customer`.
<4> This `/subject/secured` endpoint requires an authenticated user that has been granted the role `User` in production but allows any authenticated user in development mode.

=== Permission annotation

Quarkus also provides the `io.quarkus.security.PermissionsAllowed` annotation that will permit any authenticated user with given permission to access the resource.
The annotation is extension of the common security annotations and there is no relation to configuration permissions defined with the configuration property `quarkus.http.auth.permission`.

.Example of endpoints secured with a `@PermissionsAllowed` annotation

[source,java]
----
import io.quarkus.arc.Arc;
import io.vertx.ext.web.RoutingContext;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import io.quarkus.security.PermissionsAllowed;
import java.security.Permission;
import java.util.Collection;
import java.util.Collections;
@Path("/crud")
public class CRUDResource {
@PermissionsAllowed("create") <1>
@PermissionsAllowed("update")
@POST
public String createOrUpdate() {
return "modified";
}
@PermissionsAllowed({"see:detail", "see:all", "admin"}) <2>
@GET
@Path("/id/{id}")
public String getItem(String id) {
return "item-detail-" + id;
}
@PermissionsAllowed(value = "permission-1", permission = CustomPermission.class) <3>
@GET
public Collection<String> list(@QueryParam("query-options") String queryOptions) {
// your business logic comes here
return Collections.emptySet();
}
public static class CustomPermission extends Permission {
public CustomPermission(String name) {
super(name);
}
@Override
public boolean implies(Permission permission) {
var event = Arc.container().instance(RoutingContext.class).get(); <4>
return "show-first-page".equals(event.request().params().get("query-options"));
}
...
}
}
----
<1> Resource method `createOrUpdate` is only accessible by user with both `create` and `update` permissions.
The `@PermissionsAllowed` is repeatable and all annotation instances are conjuncts.
<2> User is granted access to `getItem` if he possess either `admin` permission or `see` permission and one of actions (`all`, `detail`).
Relation between permissions specified through `PermissionsAllowed#value` is disjunctive.
<3> You can use any `java.security.Permission` implementation of your choice.
By default, string-based permission is performed by the `io.quarkus.security.StringPermission`.
<4> Permissions are not beans, therefore only way to obtain bean instances is programmatically via the `Arc.container()`.

CAUTION: If you plan to use the `@PermissionsAllowed` on the IO thread, review the information in xref:security-proactive-authentication-concept.adoc[Proactive Authentication].
NOTE: The `@PermissionsAllowed` is not repeatable on class-level due to limitations of Quarkus interceptors.
Please find well-argued explanation in the xref:cdi-reference.adoc#repeatable-interceptor-bindings[Repeatable interceptor bindings] section of the CDI reference.

On CDI beans, it is also possible to take custom `java.security.Permission` much further.
Let's image that in your application, you have following service:

[source,java]
----
import io.quarkus.security.PermissionsAllowed;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.security.Permission;
import java.util.Objects;
@ApplicationScoped
public class GreetingsService {
@PermissionsAllowed(value = "ignored", permission = HelloPermission.class) <1>
public Uni<String> greetings(int index, Object obj, String hello) {
return Uni.createFrom().item("Welcome!");
}
@PermissionsAllowed(value = "ignored", permission = HelloPermission.class, params = "greetings") <2>
public Uni<String> greetings(String farewell, String greetings) {
return Uni.createFrom().item("Welcome!");
}
public static class HelloPermission extends Permission {
private final boolean pass;
public HelloPermission(String name, String[] actions, String greetings) { <3>
super(name);
this.pass = "hello".equals(greetings);
}
@Override
public boolean implies(Permission permission) {
return pass;
}
...
}
}
----
<1> Formal parameter `hello` is identified as the first `String` parameter and passed to `HelloPermission`.
However this option comes with a price, as the `HelloPermission` must be instantiated every single time `greetings` method is invoked.
<2> Here, the first `String` parameter is `farewell`, therefore we marked `greetings` parameter explicitly via `PermissionsAllowed#params`.
Please note that both constructor and annotated method must have parameter `greetings`, otherwise validation will fail.
<3> There must be exactly one constructor of a custom `Permission` class, also first parameter is always considered a permission name (must be `String`).
Optionally, Quarkus may pass Permission actions to the constructor. Just declare the second parameter as `String[]`.

CAUTION: Passing method parameters to a custom `Permission` constructor is not supported on RESTEasy Reactive endpoints,
because there, security checks are done before serialization.

Currently, there is only one way to add permissions, and that is xref:security-customization.adoc#security-identity-customization[Security Identity Customization].

[source,java]
----
import java.security.Permission;
import java.util.function.Function;
import io.quarkus.security.StringPermission;
import jakarta.enterprise.context.ApplicationScoped;
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 PermissionsAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
if (identity.isAnonymous()) {
return Uni.createFrom().item(identity);
}
return Uni.createFrom().item(build(identity));
}
SecurityIdentity build(SecurityIdentity identity) {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
// grant permission 'read' to 'user'
if ("user".equals(identity.getPrincipal().getName())) {
builder.addPermissionChecker(createPermission("read")); <1>
}
return builder.build();
}
private Function<Permission, Uni<Boolean>> createPermission(String permissionName) {
return new Function<Permission, Uni<Boolean>>() {
@Override
public Uni<Boolean> apply(Permission requiredPermission) {
final var possessedPermission = new StringPermission(permissionName);
return Uni.createFrom().item(possessedPermission.implies(requiredPermission)); <2>
}
};
}
}
----
<1> You can add a permission checks via `io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder.addPermissionChecker`.
<2> The permission check will succeed only if incoming request posses permission `read`.

CAUTION: Annotation permissions does not work with the custom xref:security-customization.adoc#jaxrs-security-context[JAX-RS SecurityContext], for there are no permissions in `jakarta.ws.rs.core.SecurityContext`.

== References

* xref:security-overview-concept.adoc[Quarkus Security overview]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
import io.quarkus.runtime.TemplateHtmlBuilder;
import io.quarkus.runtime.util.HashUtil;
import io.quarkus.security.Authenticated;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem;
Expand Down Expand Up @@ -141,6 +142,7 @@ class ReactiveRoutesProcessor {
private static final DotName ROLES_ALLOWED = DotName.createSimple(RolesAllowed.class.getName());
private static final DotName AUTHENTICATED = DotName.createSimple(Authenticated.class.getName());
private static final DotName DENY_ALL = DotName.createSimple(DenyAll.class.getName());
private static final DotName PERMISSIONS_ALLOWED = DotName.createSimple(PermissionsAllowed.class.getName());

private static final List<ParameterInjector> PARAM_INJECTORS = initParamInjectors();

Expand Down Expand Up @@ -232,6 +234,7 @@ void validateBeanDeployment(
&& !returnTypeName.equals(DotNames.MULTI) && !returnTypeName.equals(DotNames.COMPLETION_STAGE);
final boolean hasRbacAnnotationThatRequiresAuth = annotationStore.hasAnnotation(method, ROLES_ALLOWED)
|| annotationStore.hasAnnotation(method, AUTHENTICATED)
|| annotationStore.hasAnnotation(method, PERMISSIONS_ALLOWED)
|| annotationStore.hasAnnotation(method, DENY_ALL);
alwaysAuthenticateRoute = possiblySynchronousResponse && hasRbacAnnotationThatRequiresAuth;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import java.io.IOException;
import java.security.Permission;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
Expand Down Expand Up @@ -90,6 +92,12 @@ public Map<String, Object> getAttributes() {
return oldAttributes;
}

@Override
public List<Function<Permission, Uni<Boolean>>> getPermissionCheckers() {
throw new UnsupportedOperationException(
"retrieving all permission checkers not supported when JAX-RS security context has been replaced");
}

@Override
public Uni<Boolean> checkPermission(Permission permission) {
return Uni.createFrom().nullItem();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1509,7 +1509,8 @@ void registerSecurityInterceptors(Capabilities capabilities,
// Register interceptors for standard security annotations to prevent repeated security checks
beans.produce(new AdditionalBeanBuildItem(StandardSecurityCheckInterceptor.RolesAllowedInterceptor.class,
StandardSecurityCheckInterceptor.AuthenticatedInterceptor.class,
StandardSecurityCheckInterceptor.PermitAllInterceptor.class));
StandardSecurityCheckInterceptor.PermitAllInterceptor.class,
StandardSecurityCheckInterceptor.PermissionsAllowedInterceptor.class));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
import org.jboss.jandex.MethodInfo;

import io.quarkus.security.Authenticated;
import io.quarkus.security.PermissionsAllowed;

public class SecurityTransformerUtils {
public static final Set<DotName> SECURITY_BINDINGS = new HashSet<>();

static {
// keep the contents the same as in io.quarkus.resteasy.deployment.SecurityTransformerUtils
SECURITY_BINDINGS.add(DotName.createSimple(RolesAllowed.class.getName()));
SECURITY_BINDINGS.add(DotName.createSimple(PermissionsAllowed.class.getName()));
SECURITY_BINDINGS.add(DotName.createSimple(Authenticated.class.getName()));
SECURITY_BINDINGS.add(DotName.createSimple(DenyAll.class.getName()));
SECURITY_BINDINGS.add(DotName.createSimple(PermitAll.class.getName()));
Expand All @@ -32,7 +34,7 @@ public static boolean hasStandardSecurityAnnotation(MethodInfo methodInfo) {
}

public static boolean hasStandardSecurityAnnotation(ClassInfo classInfo) {
return hasStandardSecurityAnnotation(classInfo.classAnnotations());
return hasStandardSecurityAnnotation(classInfo.declaredAnnotations());
}

private static boolean hasStandardSecurityAnnotation(Collection<AnnotationInstance> instances) {
Expand All @@ -49,7 +51,7 @@ public static Optional<AnnotationInstance> findFirstStandardSecurityAnnotation(M
}

public static Optional<AnnotationInstance> findFirstStandardSecurityAnnotation(ClassInfo classInfo) {
return findFirstStandardSecurityAnnotation(classInfo.classAnnotations());
return findFirstStandardSecurityAnnotation(classInfo.declaredAnnotations());
}

private static Optional<AnnotationInstance> findFirstStandardSecurityAnnotation(Collection<AnnotationInstance> instances) {
Expand Down
Loading

0 comments on commit 3a525d5

Please sign in to comment.