Skip to content

Commit f41b7a0

Browse files
authored
Merge pull request #38195 from michalvavrik/feature/fix-arc-slow-ctx-activation-support-grpc-sec-event
Fix Arc request context state restoration on Vert.x duplicated context which allows to support security events in gRPC
2 parents 1b7c88e + 81a872d commit f41b7a0

File tree

8 files changed

+93
-92
lines changed

8 files changed

+93
-92
lines changed

docs/src/main/asciidoc/security-customization.adoc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,6 @@ Depending on the application, that can be a lot of the `AuthenticationSuccessEve
691691
For that reason, asynchronous processing can have positive effect on performance.
692692
<2> Common code for all supported security event types is possible because they all implement the `io.quarkus.security.spi.runtime.SecurityEvent` interface.
693693

694-
IMPORTANT: The gRPC extension currently does not support security events.
695-
696694
== References
697695

698696
* xref:security-overview.adoc[Quarkus Security overview]

extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@
4545
import io.quarkus.arc.deployment.BeanContainerBuildItem;
4646
import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem;
4747
import io.quarkus.arc.deployment.RecorderBeanInitializedBuildItem;
48-
import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
4948
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
5049
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
5150
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
5251
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
5352
import io.quarkus.arc.processor.AnnotationsTransformer;
5453
import io.quarkus.arc.processor.BeanInfo;
5554
import io.quarkus.arc.processor.BuiltinScope;
56-
import io.quarkus.arc.processor.ObserverInfo;
5755
import io.quarkus.deployment.ApplicationArchive;
5856
import io.quarkus.deployment.Capabilities;
5957
import io.quarkus.deployment.Capability;
@@ -69,7 +67,6 @@
6967
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
7068
import io.quarkus.deployment.builditem.FeatureBuildItem;
7169
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
72-
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
7370
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
7471
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
7572
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
@@ -93,7 +90,6 @@
9390
import io.quarkus.kubernetes.spi.KubernetesPortBuildItem;
9491
import io.quarkus.netty.deployment.MinNettyAllocatorMaxOrderBuildItem;
9592
import io.quarkus.runtime.LaunchMode;
96-
import io.quarkus.security.spi.runtime.SecurityEvent;
9793
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;
9894
import io.quarkus.vertx.deployment.VertxBuildItem;
9995
import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem;
@@ -797,36 +793,4 @@ void initGrpcSecurityInterceptor(List<BindableServiceBuildItem> bindables, Capab
797793
}
798794
}
799795

800-
@Record(RUNTIME_INIT)
801-
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
802-
@BuildStep
803-
void validateSecurityEventsNotObserved(SynthesisFinishedBuildItem synthesisFinished,
804-
Capabilities capabilities,
805-
GrpcSecurityRecorder recorder,
806-
BeanArchiveIndexBuildItem indexBuildItem) {
807-
if (!capabilities.isPresent(Capability.SECURITY)) {
808-
return;
809-
}
810-
811-
// collect all SecurityEvent classes
812-
Set<DotName> knownSecurityEventClasses = new HashSet<>();
813-
knownSecurityEventClasses.add(DotName.createSimple(SecurityEvent.class));
814-
indexBuildItem
815-
.getIndex()
816-
.getAllKnownImplementors(SecurityEvent.class)
817-
.stream()
818-
.map(ClassInfo::name)
819-
.forEach(knownSecurityEventClasses::add);
820-
821-
// find at least one CDI observer and validate security events are disabled
822-
knownClasses: for (DotName knownSecurityEventClass : knownSecurityEventClasses) {
823-
for (ObserverInfo observer : synthesisFinished.getObservers()) {
824-
if (observer.getObservedType().name().equals(knownSecurityEventClass)) {
825-
recorder.validateSecurityEventsDisabled(knownSecurityEventClass.toString());
826-
break knownClasses;
827-
}
828-
}
829-
}
830-
}
831-
832796
}

extensions/grpc/deployment/src/test/java/io/quarkus/grpc/auth/GrpcAuthTestBase.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
import static com.example.security.Security.ThreadInfo.newBuilder;
44
import static io.quarkus.grpc.auth.BlockingHttpSecurityPolicy.BLOCK_REQUEST;
5+
import static io.quarkus.security.spi.runtime.AuthorizationSuccessEvent.AUTHORIZATION_CONTEXT;
56
import static org.assertj.core.api.Assertions.assertThat;
67
import static org.awaitility.Awaitility.await;
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertFalse;
10+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
11+
import static org.junit.jupiter.api.Assertions.assertNotNull;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
713

814
import java.util.List;
915
import java.util.concurrent.CopyOnWriteArrayList;
@@ -12,11 +18,13 @@
1218
import java.util.concurrent.atomic.AtomicReference;
1319

1420
import jakarta.annotation.security.RolesAllowed;
21+
import jakarta.inject.Inject;
1522

1623
import org.jboss.shrinkwrap.api.ShrinkWrap;
1724
import org.jboss.shrinkwrap.api.asset.StringAsset;
1825
import org.jboss.shrinkwrap.api.spec.JavaArchive;
1926
import org.junit.jupiter.api.Assertions;
27+
import org.junit.jupiter.api.BeforeEach;
2028
import org.junit.jupiter.api.Test;
2129

2230
import com.example.security.SecuredService;
@@ -26,6 +34,11 @@
2634
import io.quarkus.grpc.GrpcClient;
2735
import io.quarkus.grpc.GrpcClientUtils;
2836
import io.quarkus.grpc.GrpcService;
37+
import io.quarkus.security.UnauthorizedException;
38+
import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck;
39+
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
40+
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
41+
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
2942
import io.quarkus.test.QuarkusUnitTest;
3043
import io.smallrye.common.annotation.Blocking;
3144
import io.smallrye.mutiny.Multi;
@@ -54,7 +67,7 @@ protected static QuarkusUnitTest createQuarkusUnitTest(String extraProperty, boo
5467
props += extraProperty;
5568
}
5669
var jar = ShrinkWrap.create(JavaArchive.class)
57-
.addClasses(Service.class, BlockingHttpSecurityPolicy.class)
70+
.addClasses(Service.class, BlockingHttpSecurityPolicy.class, SecurityEventObserver.class)
5871
.addPackage(SecuredService.class.getPackage())
5972
.add(new StringAsset(props), "application.properties");
6073
return useGrpcAuthMechanism ? jar.addClass(BasicGrpcSecurityMechanism.class) : jar;
@@ -67,6 +80,14 @@ protected static QuarkusUnitTest createQuarkusUnitTest(String extraProperty, boo
6780
@GrpcClient
6881
SecuredService securityClient;
6982

83+
@Inject
84+
SecurityEventObserver securityEventObserver;
85+
86+
@BeforeEach
87+
void clearEvents() {
88+
securityEventObserver.getStorage().clear();
89+
}
90+
7091
@Test
7192
void shouldSecureUniEndpoint() {
7293
Metadata headers = new Metadata();
@@ -83,6 +104,7 @@ void shouldSecureUniEndpoint() {
83104

84105
await().atMost(10, TimeUnit.SECONDS)
85106
.until(() -> resultCount.get() == 1);
107+
assertSecurityEventsFired("john");
86108
}
87109

88110
@Test
@@ -101,6 +123,7 @@ void shouldSecureBlockingUniEndpoint() {
101123

102124
await().atMost(10, TimeUnit.SECONDS)
103125
.until(() -> resultCount.get() == 1);
126+
assertSecurityEventsFired("john");
104127
}
105128

106129
@Test
@@ -117,6 +140,7 @@ void shouldSecureMultiEndpoint() {
117140
.until(() -> results.size() == 5);
118141

119142
assertThat(results.stream().filter(e -> !e)).isEmpty();
143+
assertSecurityEventsFired("paul");
120144
}
121145

122146
@Test
@@ -133,6 +157,7 @@ void shouldSecureBlockingMultiEndpoint() {
133157
.until(() -> results.size() == 5);
134158

135159
assertThat(results.stream().filter(e -> e)).isEmpty();
160+
assertSecurityEventsFired("paul");
136161
}
137162

138163
@Test
@@ -167,6 +192,16 @@ void shouldFailWithInvalidInsufficientRole() {
167192

168193
await().atMost(10, TimeUnit.SECONDS)
169194
.until(() -> error.get() != null);
195+
196+
// we don't check exact count as HTTP Security policies are not supported when gRPC is running on separate server
197+
assertFalse(securityEventObserver.getStorage().isEmpty());
198+
// fails RolesAllowed check as the anonymous identity has no roles
199+
AuthorizationFailureEvent event = (AuthorizationFailureEvent) securityEventObserver
200+
.getStorage().get(securityEventObserver.getStorage().size() - 1);
201+
assertNotNull(event.getSecurityIdentity());
202+
assertTrue(event.getSecurityIdentity().isAnonymous());
203+
assertInstanceOf(UnauthorizedException.class, event.getAuthorizationFailure());
204+
assertEquals(RolesAllowedCheck.class.getName(), event.getAuthorizationContext());
170205
}
171206

172207
@Test
@@ -186,6 +221,7 @@ void shouldSecureUniEndpointWithBlockingHttpSecurityPolicy() {
186221

187222
await().atMost(10, TimeUnit.SECONDS)
188223
.until(() -> resultCount.get() == 1);
224+
assertSecurityEventsFired("john");
189225
}
190226

191227
@Test
@@ -205,6 +241,7 @@ void shouldSecureBlockingUniEndpointWithBlockingHttpSecurityPolicy() {
205241

206242
await().atMost(10, TimeUnit.SECONDS)
207243
.until(() -> resultCount.get() == 1);
244+
assertSecurityEventsFired("john");
208245
}
209246

210247
@Test
@@ -224,6 +261,7 @@ void shouldSecureMultiEndpointWithBlockingHttpSecurityPolicy() {
224261
.until(() -> results.size() == 5);
225262

226263
assertThat(results.stream().filter(e -> !e)).isEmpty();
264+
assertSecurityEventsFired("paul");
227265
}
228266

229267
@Test
@@ -241,6 +279,19 @@ void shouldSecureBlockingMultiEndpointWithBlockingHttpSecurityPolicy() {
241279
.until(() -> results.size() == 5);
242280

243281
assertThat(results.stream().filter(e -> e)).isEmpty();
282+
assertSecurityEventsFired("paul");
283+
}
284+
285+
private void assertSecurityEventsFired(String username) {
286+
// expect at least authentication success and RolesAllowed security check success
287+
// we don't check exact count as HTTP Security policies are not supported when gRPC is running on separate server
288+
assertTrue(securityEventObserver.getStorage().size() >= 2);
289+
assertTrue(securityEventObserver.getStorage().stream().anyMatch(e -> e instanceof AuthenticationSuccessEvent));
290+
AuthorizationSuccessEvent event = (AuthorizationSuccessEvent) securityEventObserver.getStorage()
291+
.get(securityEventObserver.getStorage().size() - 1);
292+
assertNotNull(event.getSecurityIdentity());
293+
assertEquals(username, event.getSecurityIdentity().getPrincipal().getName());
294+
assertEquals(RolesAllowedCheck.class.getName(), event.getEventProperties().get(AUTHORIZATION_CONTEXT));
244295
}
245296

246297
private static void addBlockingHeaders(Metadata headers) {

extensions/grpc/deployment/src/test/java/io/quarkus/grpc/auth/SecurityEventObserver.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import java.util.List;
44
import java.util.concurrent.CopyOnWriteArrayList;
55

6+
import jakarta.enterprise.context.ApplicationScoped;
67
import jakarta.enterprise.event.Observes;
78

89
import io.quarkus.security.spi.runtime.SecurityEvent;
910

11+
@ApplicationScoped
1012
public class SecurityEventObserver {
1113

1214
private final List<SecurityEvent> storage = new CopyOnWriteArrayList<>();

extensions/grpc/deployment/src/test/java/io/quarkus/grpc/auth/SecurityEventsValidationFailureTest.java

Lines changed: 0 additions & 31 deletions
This file was deleted.

extensions/grpc/runtime/src/main/java/io/quarkus/grpc/auth/GrpcSecurityInterceptor.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.grpc.auth;
22

3+
import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHENTICATION_FAILURE;
4+
import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHENTICATION_SUCCESS;
35
import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.isExplicitlyMarkedAsUnsafe;
46
import static io.quarkus.vertx.http.runtime.security.QuarkusHttpUser.DEFERRED_IDENTITY_KEY;
57
import static io.smallrye.common.vertx.VertxContext.isDuplicatedContext;
@@ -10,8 +12,11 @@
1012
import java.util.List;
1113
import java.util.Map;
1214
import java.util.concurrent.Executor;
15+
import java.util.function.Consumer;
1316

17+
import jakarta.enterprise.event.Event;
1418
import jakarta.enterprise.inject.Instance;
19+
import jakarta.enterprise.inject.spi.BeanManager;
1520
import jakarta.enterprise.inject.spi.Prioritized;
1621
import jakarta.inject.Inject;
1722
import jakarta.inject.Singleton;
@@ -29,6 +34,9 @@
2934
import io.quarkus.security.identity.IdentityProviderManager;
3035
import io.quarkus.security.identity.SecurityIdentity;
3136
import io.quarkus.security.identity.request.AuthenticationRequest;
37+
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
38+
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
39+
import io.quarkus.security.spi.runtime.SecurityEventHelper;
3240
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
3341
import io.smallrye.mutiny.Uni;
3442
import io.vertx.core.Context;
@@ -55,14 +63,20 @@ public final class GrpcSecurityInterceptor implements ServerInterceptor, Priorit
5563
private final Map<String, List<String>> serviceToBlockingMethods = new HashMap<>();
5664
private boolean hasBlockingMethods = false;
5765
private final boolean notUsingSeparateGrpcServer;
66+
private final SecurityEventHelper<AuthenticationSuccessEvent, AuthenticationFailureEvent> securityEventHelper;
5867

5968
@Inject
6069
public GrpcSecurityInterceptor(
6170
CurrentIdentityAssociation identityAssociation,
6271
IdentityProviderManager identityProviderManager,
6372
Instance<GrpcSecurityMechanism> securityMechanisms,
6473
Instance<AuthExceptionHandlerProvider> exceptionHandlers,
65-
@ConfigProperty(name = "quarkus.grpc.server.use-separate-server") boolean usingSeparateGrpcServer) {
74+
@ConfigProperty(name = "quarkus.grpc.server.use-separate-server") boolean usingSeparateGrpcServer,
75+
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled,
76+
BeanManager beanManager, Event<AuthenticationFailureEvent> authFailureEvent,
77+
Event<AuthenticationSuccessEvent> authSuccessEvent) {
78+
this.securityEventHelper = new SecurityEventHelper<>(authSuccessEvent, authFailureEvent, AUTHENTICATION_SUCCESS,
79+
AUTHENTICATION_FAILURE, beanManager, securityEventsEnabled);
6680
this.identityAssociation = identityAssociation;
6781
this.identityProviderManager = identityProviderManager;
6882
this.notUsingSeparateGrpcServer = !usingSeparateGrpcServer;
@@ -131,6 +145,23 @@ public void handle(Void event) {
131145
}
132146
}
133147
});
148+
if (securityEventHelper.fireEventOnSuccess()) {
149+
auth = auth.invoke(new Consumer<SecurityIdentity>() {
150+
@Override
151+
public void accept(SecurityIdentity securityIdentity) {
152+
securityEventHelper
153+
.fireSuccessEvent(new AuthenticationSuccessEvent(securityIdentity, null));
154+
}
155+
});
156+
}
157+
if (securityEventHelper.fireEventOnFailure()) {
158+
auth = auth.onFailure().invoke(new Consumer<Throwable>() {
159+
@Override
160+
public void accept(Throwable throwable) {
161+
securityEventHelper.fireFailureEvent(new AuthenticationFailureEvent(throwable, null));
162+
}
163+
});
164+
}
134165
identityAssociation.setIdentity(auth);
135166
error = null;
136167
identityAssociationNotSet = false;
@@ -143,8 +174,11 @@ public void handle(Void event) {
143174
}
144175
}
145176
if (error != null) { // if parsing for all security mechanisms failed, let's propagate the last exception
146-
identityAssociation.setIdentity(Uni.createFrom()
147-
.failure(new AuthenticationFailedException("Failed to parse authentication data", error)));
177+
var authFailedEx = new AuthenticationFailedException("Failed to parse authentication data", error);
178+
if (securityEventHelper.fireEventOnFailure()) {
179+
securityEventHelper.fireFailureEvent(new AuthenticationFailureEvent(authFailedEx, null));
180+
}
181+
identityAssociation.setIdentity(Uni.createFrom().failure(authFailedEx));
148182
}
149183
}
150184
if (identityAssociationNotSet && notUsingSeparateGrpcServer) {

0 commit comments

Comments
 (0)