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

Fix HTTP Security policies when OIDC tenant is selected with the @Tenant annotation on Jakarta REST resources #38772

Merged
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
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,8 +724,21 @@
----
<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"}

If you need a more dynamic configuration for the different tenants you want to support and don't want to end up with multiple
entries in your configuration file, you can use the `io.quarkus.oidc.TenantConfigResolver`.
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
Loading