Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve Sec. Identity in RESTEasy Reactive when Proactive Auth disabled #26457

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
@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