Skip to content

Commit

Permalink
Merge pull request quarkusio#26457 from michalvavrik/feature/fix-secu…
Browse files Browse the repository at this point in the history
…rityconstrainer-blocking-op-exception

Resolve Sec. Identity in RESTEasy Reactive when Proactive Auth disabled
  • Loading branch information
sberyozkin authored Jul 1, 2022
2 parents 3bba499 + 513e44c commit 4491639
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 7 deletions.
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/security-built-in-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ application this will fail (as you cannot do blocking operations on the IO threa
`Uni<SecurityIdentity> getDeferredIdentity();` method. You can then subscribe to the resulting `Uni` and will be notified
when authentication is complete and the identity is available.

NOTE: It's still possible to access the `SecurityIdentity` synchronously with `public SecurityIdentity getIdentity()`
in the xref:resteasy-reactive.adoc[RESTEasy Reactive] from endpoints annotated with `@RolesAllowed`, `@Authenticated`,
or with respective configuration authorization checks as authentication has already happened.

=== How to customize authentication exception responses

By default, the authentication security constraints are enforced before the JAX-RS chain starts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,8 @@ public void securityExceptionMappers(BuildProducer<ExceptionMapperBuildItem> exc
}

@BuildStep
MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, CombinedIndexBuildItem indexBuildItem) {
MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, CombinedIndexBuildItem indexBuildItem,
HttpBuildTimeConfig httpBuildTimeConfig) {
if (!capabilities.isPresent(Capability.SECURITY)) {
return null;
}
Expand All @@ -1036,7 +1037,8 @@ public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndp
Map<String, Object> methodContext) {
return Objects.requireNonNullElse(
consumeStandardSecurityAnnotations(method, actualEndpointClass, index,
(c) -> Collections.singletonList(new EagerSecurityHandler.Customizer())),
(c) -> Collections.singletonList(
EagerSecurityHandler.Customizer.newInstance(httpBuildTimeConfig.auth.proactive))),
Collections.emptyList());
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.Arrays;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -41,6 +42,8 @@ public void testRolesAllowed() {
RestAssured.given().auth().basic("user", "user").get(path).then().statusCode(200);
RestAssured.given().auth().basic("admin", "admin").get(path + "/admin").then().statusCode(200);
RestAssured.given().auth().basic("user", "user").get(path + "/admin").then().statusCode(403);
RestAssured.given().auth().basic("admin", "admin").get(path + "/admin/security-identity").then().statusCode(200)
.body(Matchers.is("admin"));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.common.annotation.NonBlocking;

/**
* @author Michal Szynkiewicz, [email protected]
Expand All @@ -14,6 +17,10 @@
@PermitAll
@Blocking
public class RolesAllowedBlockingResource {

@Inject
CurrentIdentityAssociation currentIdentityAssociation;

@GET
@RolesAllowed({ "user", "admin" })
public String defaultSecurity() {
Expand All @@ -27,4 +34,12 @@ public String admin() {
return "admin";
}

@NonBlocking
@Path("/admin/security-identity")
@RolesAllowed("admin")
@GET
public String getSecurityIdentity() {
return currentIdentityAssociation.getIdentity().getPrincipal().getName();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.smallrye.common.annotation.NonBlocking;

/**
* @author Michal Szynkiewicz, [email protected]
*/
@Path("/roles")
@PermitAll
public class RolesAllowedResource {

@Inject
CurrentIdentityAssociation currentIdentityAssociation;

@GET
@RolesAllowed({ "user", "admin" })
public String defaultSecurity() {
Expand All @@ -24,4 +32,12 @@ public String admin() {
return "admin";
}

@NonBlocking
@Path("/admin/security-identity")
@RolesAllowed("admin")
@GET
public String getSecurityIdentity() {
return currentIdentityAssociation.getIdentity().getPrincipal().getName();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.quarkus.security.spi.runtime.MethodDescription;
import io.quarkus.security.spi.runtime.SecurityCheck;
import io.quarkus.security.spi.runtime.SecurityCheckStorage;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniSubscriber;
import io.smallrye.mutiny.subscription.UniSubscription;

Expand All @@ -37,10 +38,15 @@ public void apply(SecurityIdentity identity, MethodDescription method, Object[]
}
};

private final boolean isProactiveAuthDisabled;
private volatile InjectableInstance<CurrentIdentityAssociation> currentIdentityAssociation;
private volatile SecurityCheck check;
private volatile AuthorizationController authorizationController;

public EagerSecurityHandler(boolean isProactiveAuthDisabled) {
this.isProactiveAuthDisabled = isProactiveAuthDisabled;
}

@Override
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
if (this.check == NULL_SENTINEL) {
Expand All @@ -66,11 +72,25 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
if (!authorizationController.isAuthorizationEnabled()) {
return;
}

requestContext.requireCDIRequestScope();
SecurityCheck theCheck = check;
if (!theCheck.isPermitAll()) {
requestContext.suspend();
getCurrentIdentityAssociation().get().getDeferredIdentity().map(new Function<SecurityIdentity, Object>() {
Uni<SecurityIdentity> deferredIdentity = getCurrentIdentityAssociation().get().getDeferredIdentity();

// if proactive auth is disabled, then accessing SecurityIdentity is a blocking operation for synchronous methods
// setting identity here will enable SecurityInterceptors registered in Quarkus Security Deployment to run checks
if (isProactiveAuthDisabled && lazyMethod.isNonBlocking) {
deferredIdentity = deferredIdentity.call(securityIdentity -> {
if (securityIdentity != null) {
getCurrentIdentityAssociation().get().setIdentity(securityIdentity);
}
return Uni.createFrom().item(securityIdentity);
});
}

deferredIdentity.map(new Function<SecurityIdentity, Object>() {
@Override
public Object apply(SecurityIdentity securityIdentity) {
theCheck.apply(securityIdentity, methodDescription,
Expand Down Expand Up @@ -105,14 +125,38 @@ private InjectableInstance<CurrentIdentityAssociation> getCurrentIdentityAssocia
return identityAssociation;
}

public static class Customizer implements HandlerChainCustomizer {
public static abstract class Customizer implements HandlerChainCustomizer {

public static Customizer newInstance(boolean isProactiveAuthEnabled) {
return isProactiveAuthEnabled ? new ProactiveAuthEnabledCustomizer() : new ProactiveAuthDisabledCustomizer();
}

protected abstract boolean isProactiveAuthDisabled();

@Override
public List<ServerRestHandler> handlers(Phase phase, ResourceClass resourceClass,
ServerResourceMethod serverResourceMethod) {
if (phase == Phase.AFTER_MATCH) {
return Collections.singletonList(new EagerSecurityHandler());
return Collections.singletonList(new EagerSecurityHandler(isProactiveAuthDisabled()));
}
return Collections.emptyList();
}

public static class ProactiveAuthEnabledCustomizer extends Customizer {

@Override
protected boolean isProactiveAuthDisabled() {
return false;
}
}

public static class ProactiveAuthDisabledCustomizer extends Customizer {

@Override
protected boolean isProactiveAuthDisabled() {
return true;
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz,
}

ResteasyReactiveResourceInfo lazyMethod = new ResteasyReactiveResourceInfo(method.getName(), resourceClass,
parameterDeclaredUnresolvedTypes, classAnnotationNames, method.getMethodAnnotationNames());
parameterDeclaredUnresolvedTypes, classAnnotationNames, method.getMethodAnnotationNames(),
!defaultBlocking && !method.isBlocking());

RuntimeInterceptorDeployment.MethodInterceptorContext interceptorDeployment = runtimeInterceptorDeployment
.forMethod(method, lazyMethod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class ResteasyReactiveResourceInfo implements ResourceInfo {
private final Class[] parameterTypes;
private final Set<String> classAnnotationNames;
private final Set<String> methodAnnotationNames;
/**
* If it's non-blocking method within the runtime that won't always default to blocking
*/
public final boolean isNonBlocking;

private volatile Annotation[] classAnnotations;
private volatile Method method;
Expand All @@ -28,12 +32,13 @@ public class ResteasyReactiveResourceInfo implements ResourceInfo {
private volatile String methodId;

public ResteasyReactiveResourceInfo(String name, Class<?> declaringClass, Class[] parameterTypes,
Set<String> classAnnotationNames, Set<String> methodAnnotationNames) {
Set<String> classAnnotationNames, Set<String> methodAnnotationNames, boolean isNonBlocking) {
this.name = name;
this.declaringClass = declaringClass;
this.parameterTypes = parameterTypes;
this.classAnnotationNames = classAnnotationNames;
this.methodAnnotationNames = methodAnnotationNames;
this.isNonBlocking = isNonBlocking;
}

public String getName() {
Expand Down

0 comments on commit 4491639

Please sign in to comment.