Skip to content

Commit

Permalink
Fix HTTP permission checks used with @Tenant annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Feb 16, 2024
1 parent 9ace5ba commit 81386fd
Show file tree
Hide file tree
Showing 23 changed files with 1,011 additions and 230 deletions.
13 changes: 13 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,19 @@ public class HelloResource {
----
<1> The `io.quarkus.oidc.Tenant` annotation must be placed on either the resource class or resource method.

[[TIP]]
In the example above, authentication of the `sayHello` endpoint is enforced with the `@Authenticated` annotation.
Alternatively, if you use an the xref:security-authorize-web-endpoints-reference.adoc#authorization-using-configuration[HTTP Security policy]
to secure the endpoint, then, for the `@Tenant` annotation be effective, you must delay this policy's permission check as shown in the example below:

Check warning on line 730 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 730, "column": 93}}}, "severity": "INFO"}
[source,properties]
----
quarkus.http.auth.permission.authenticated.paths=/api/hello
quarkus.http.auth.permission.authenticated.methods=GET
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.http.auth.permission.authenticated.applies-to=JAXRS <1>
----
<1> Tell Quarkus to run the HTTP permission check after the tenant has been selected with the `@Tenant` annotation.

[[tenant-config-resolver]]
== Dynamic tenant configuration resolution

Check warning on line 741 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 741, "column": 18}}}, "severity": "INFO"}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.quarkus.resteasy.runtime.EagerSecurityFilter;
import io.quarkus.resteasy.runtime.ExceptionMapperRecorder;
import io.quarkus.resteasy.runtime.ForbiddenExceptionMapper;
import io.quarkus.resteasy.runtime.JaxRsPermissionChecker;
import io.quarkus.resteasy.runtime.JaxRsSecurityConfig;
import io.quarkus.resteasy.runtime.NotFoundExceptionMapper;
import io.quarkus.resteasy.runtime.SecurityContextFilter;
Expand Down Expand Up @@ -71,6 +72,7 @@ void setUpSecurity(BuildProducer<ResteasyJaxrsProviderBuildItem> providers,
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(SecurityContextFilter.class));
providers.produce(new ResteasyJaxrsProviderBuildItem(EagerSecurityFilter.class.getName()));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityFilter.class));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(JaxRsPermissionChecker.class));
additionalBeanBuildItem.produce(
AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.RolesAllowedInterceptor.class));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

import jakarta.annotation.Priority;
import jakarta.enterprise.event.Event;
Expand Down Expand Up @@ -37,15 +35,7 @@
@Priority(Priorities.AUTHENTICATION)
@Provider
public class EagerSecurityFilter implements ContainerRequestFilter {

private static final Consumer<RoutingContext> NULL_SENTINEL = new Consumer<RoutingContext>() {
@Override
public void accept(RoutingContext routingContext) {

}
};
static final String SKIP_DEFAULT_CHECK = "io.quarkus.resteasy.runtime.EagerSecurityFilter#SKIP_DEFAULT_CHECK";
private final Map<MethodDescription, Consumer<RoutingContext>> cache = new HashMap<>();
private final EagerSecurityInterceptorStorage interceptorStorage;
private final SecurityEventHelper<AuthorizationSuccessEvent, AuthorizationFailureEvent> eventHelper;

Expand All @@ -64,13 +54,16 @@ public void accept(RoutingContext routingContext) {
@Inject
AuthorizationController authorizationController;

@Inject
JaxRsPermissionChecker jaxRsPermissionChecker;

public EagerSecurityFilter() {
var interceptorStorageHandle = Arc.container().instance(EagerSecurityInterceptorStorage.class);
this.interceptorStorage = interceptorStorageHandle.isAvailable() ? interceptorStorageHandle.get() : null;
Event<Object> event = Arc.container().beanManager().getEvent();
this.eventHelper = new SecurityEventHelper<>(event.select(AuthorizationSuccessEvent.class),
event.select(AuthorizationFailureEvent.class), AUTHORIZATION_SUCCESS,
AUTHORIZATION_FAILURE, Arc.container().beanManager(),
event.select(AuthorizationFailureEvent.class), AUTHORIZATION_SUCCESS, AUTHORIZATION_FAILURE,
Arc.container().beanManager(),
ConfigProvider.getConfig().getOptionalValue("quarkus.security.events.enabled", Boolean.class).orElse(false));
}

Expand All @@ -81,6 +74,9 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
if (interceptorStorage != null) {
applyEagerSecurityInterceptors(description);
}
if (jaxRsPermissionChecker.shouldRunPermissionChecks()) {
jaxRsPermissionChecker.applyPermissionChecks(eventHelper);
}
applySecurityChecks(description);
}
}
Expand Down Expand Up @@ -138,19 +134,9 @@ private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity secur
}

private void applyEagerSecurityInterceptors(MethodDescription description) {
var interceptor = cache.get(description);
if (interceptor != NULL_SENTINEL) {
if (interceptor != null) {
interceptor.accept(routingContext);
} else {
interceptor = interceptorStorage.getInterceptor(description);
if (interceptor == null) {
cache.put(description, NULL_SENTINEL);
} else {
cache.put(description, interceptor);
interceptor.accept(routingContext);
}
}
var interceptor = interceptorStorage.getInterceptor(description);
if (interceptor != null) {
interceptor.accept(routingContext);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package io.quarkus.resteasy.runtime;

import static io.quarkus.vertx.http.runtime.PolicyMappingConfig.AppliesTo.JAXRS;

import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import io.quarkus.security.ForbiddenException;
import io.quarkus.security.UnauthorizedException;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.security.AbstractPathMatchingHttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.DefaultAuthorizationRequestContext;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.vertx.ext.web.RoutingContext;

/**
* Checks HTTP permissions specific for Jakarta REST.
*
* @see io.quarkus.vertx.http.runtime.PolicyMappingConfig.AppliesTo#JAXRS
*/
@ApplicationScoped
public class JaxRsPermissionChecker {
private final AbstractPathMatchingHttpSecurityPolicy jaxRsPathMatchingPolicy;
private final HttpSecurityPolicy.AuthorizationRequestContext authorizationRequestContext;

@Inject
RoutingContext routingContext;

@Inject
CurrentIdentityAssociation identityAssociation;

JaxRsPermissionChecker(HttpConfiguration httpConfig, Instance<HttpSecurityPolicy> installedPolicies,
HttpBuildTimeConfig httpBuildTimeConfig, BlockingSecurityExecutor blockingSecurityExecutor) {
var jaxRsPathMatchingPolicy = new AbstractPathMatchingHttpSecurityPolicy(httpConfig.auth.permissions,
httpConfig.auth.rolePolicy, httpBuildTimeConfig.rootPath, installedPolicies, JAXRS);
if (jaxRsPathMatchingPolicy.hasNoPermissions()) {
this.jaxRsPathMatchingPolicy = null;
this.authorizationRequestContext = null;
} else {
this.jaxRsPathMatchingPolicy = jaxRsPathMatchingPolicy;
this.authorizationRequestContext = new DefaultAuthorizationRequestContext(blockingSecurityExecutor);
}
}

boolean shouldRunPermissionChecks() {
return jaxRsPathMatchingPolicy != null;
}

void applyPermissionChecks(SecurityEventHelper<AuthorizationSuccessEvent, AuthorizationFailureEvent> eventHelper) {
HttpSecurityPolicy.CheckResult checkResult = jaxRsPathMatchingPolicy
.checkPermission(routingContext, identityAssociation.getDeferredIdentity(), authorizationRequestContext)
.await().indefinitely();
final SecurityIdentity newIdentity;
if (checkResult.getAugmentedIdentity() == null) {
if (checkResult.isPermitted()) {
// do not require authentication when permission checks didn't require it
newIdentity = null;
} else {
newIdentity = identityAssociation.getIdentity();
}
} else if (checkResult.getAugmentedIdentity() != identityAssociation.getIdentity()) {
newIdentity = checkResult.getAugmentedIdentity();
routingContext.setUser(new QuarkusHttpUser(newIdentity));
identityAssociation.setIdentity(newIdentity);
} else {
newIdentity = checkResult.getAugmentedIdentity();
}

if (checkResult.isPermitted()) {
if (eventHelper.fireEventOnSuccess()) {
eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(newIdentity,
AbstractPathMatchingHttpSecurityPolicy.class.getName(),
Map.of(RoutingContext.class.getName(), routingContext)));
}
return;
}

// access denied
final RuntimeException exception;
if (newIdentity.isAnonymous()) {
exception = new UnauthorizedException();
} else {
exception = new ForbiddenException();
}
if (eventHelper.fireEventOnFailure()) {
eventHelper.fireFailureEvent(new AuthorizationFailureEvent(newIdentity, exception,
AbstractPathMatchingHttpSecurityPolicy.class.getName(),
Map.of(RoutingContext.class.getName(), routingContext)));
}
throw exception;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
import io.quarkus.resteasy.reactive.common.deployment.ResourceScanningResultBuildItem;
import io.quarkus.resteasy.reactive.common.deployment.SerializersUtil;
import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem;
import io.quarkus.resteasy.reactive.common.runtime.JaxRsSecurityConfig;
import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig;
import io.quarkus.resteasy.reactive.server.EndpointDisabled;
import io.quarkus.resteasy.reactive.server.runtime.QuarkusServerFileBodyHandler;
Expand All @@ -179,10 +180,10 @@
import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.AuthenticationRedirectExceptionMapper;
import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.ForbiddenExceptionMapper;
import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.UnauthorizedExceptionMapper;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityContext;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityHandler;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityInterceptorHandler;
import io.quarkus.resteasy.reactive.server.runtime.security.SecurityContextOverrideHandler;
import io.quarkus.resteasy.reactive.server.runtime.security.SecurityEventContext;
import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem;
import io.quarkus.resteasy.reactive.server.spi.ContextTypeBuildItem;
import io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem;
Expand Down Expand Up @@ -1510,42 +1511,39 @@ public void securityExceptionMappers(BuildProducer<ExceptionMapperBuildItem> exc

@BuildStep
MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, CombinedIndexBuildItem indexBuildItem,
HttpBuildTimeConfig httpBuildTimeConfig, Optional<EagerSecurityInterceptorBuildItem> eagerSecurityInterceptors) {
HttpBuildTimeConfig httpBuildTimeConfig, Optional<EagerSecurityInterceptorBuildItem> eagerSecurityInterceptors,
JaxRsSecurityConfig securityConfig) {
if (!capabilities.isPresent(Capability.SECURITY)) {
return null;
}

final boolean applySecurityInterceptors = eagerSecurityInterceptors.isPresent();
final boolean denyJaxRs = ConfigProvider.getConfig()
.getOptionalValue("quarkus.security.jaxrs.deny-unannotated-endpoints", Boolean.class).orElse(false);
final boolean hasDefaultJaxRsRolesAllowed = ConfigProvider.getConfig()
.getOptionalValues("quarkus.security.jaxrs.default-roles-allowed", String.class).map(l -> !l.isEmpty())
.orElse(false);
final boolean denyJaxRs = securityConfig.denyJaxRs();
final boolean hasDefaultJaxRsRolesAllowed = !securityConfig.defaultRolesAllowed().orElse(List.of()).isEmpty();
var index = indexBuildItem.getComputingIndex();
return new MethodScannerBuildItem(new MethodScanner() {
@Override
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
Map<String, Object> methodContext) {
List<HandlerChainCustomizer> securityHandlerList = consumeStandardSecurityAnnotations(method,
actualEndpointClass, index,
(c) -> Collections.singletonList(
EagerSecurityHandler.Customizer.newInstance(httpBuildTimeConfig.auth.proactive)));
if (securityHandlerList == null && (denyJaxRs || hasDefaultJaxRsRolesAllowed)) {
securityHandlerList = Collections
.singletonList(EagerSecurityHandler.Customizer.newInstance(httpBuildTimeConfig.auth.proactive));
}
if (applySecurityInterceptors && eagerSecurityInterceptors.get().applyInterceptorOn(method)) {
List<HandlerChainCustomizer> nextSecurityHandlerList = new ArrayList<>();
nextSecurityHandlerList.add(EagerSecurityInterceptorHandler.Customizer.newInstance());

// EagerSecurityInterceptorHandler must be run before EagerSecurityHandler
if (securityHandlerList != null) {
nextSecurityHandlerList.addAll(securityHandlerList);
// EagerSecurityHandler needs to be present whenever the method requires eager interceptor
// because JAX-RS specific HTTP Security policies are defined by runtime config properties
// for example: when you annotate resource method with @Tenant("hr") you select OIDC tenant,
// so we can't authenticate before the tenant is selected, only after then HTTP perms can be checked
return List.of(EagerSecurityInterceptorHandler.Customizer.newInstance(),
EagerSecurityHandler.Customizer.newInstance(httpBuildTimeConfig.auth.proactive));
} else {
if (denyJaxRs || hasDefaultJaxRsRolesAllowed) {
return List.of(EagerSecurityHandler.Customizer.newInstance(httpBuildTimeConfig.auth.proactive));
} else {
return Objects
.requireNonNullElse(
consumeStandardSecurityAnnotations(method, actualEndpointClass, index,
(c) -> Collections.singletonList(EagerSecurityHandler.Customizer
.newInstance(httpBuildTimeConfig.auth.proactive))),
Collections.emptyList());
}

securityHandlerList = List.copyOf(nextSecurityHandlerList);
}
return Objects.requireNonNullElse(securityHandlerList, Collections.emptyList());
}
});
}
Expand Down Expand Up @@ -1608,7 +1606,7 @@ void registerSecurityInterceptors(Capabilities capabilities,
StandardSecurityCheckInterceptor.AuthenticatedInterceptor.class,
StandardSecurityCheckInterceptor.PermitAllInterceptor.class,
StandardSecurityCheckInterceptor.PermissionsAllowedInterceptor.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(SecurityEventContext.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityContext.class));
}
}

Expand Down
Loading

0 comments on commit 81386fd

Please sign in to comment.