luigidemasi commented on code in PR #27435:
URL: https://github.com/apache/camel/pull/27435#discussion_r4194419752
##########
components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java:
##########
@@ -165,7 +175,7 @@ public HttpHandler registerHandler(
private Undertow registerHandler(UndertowConsumer consumer,
Undertow.Builder builder, HttpHandler handler) {
UndertowSecurityProvider securityProvider = consumer == null
- ? null
+ ? this.securityProvider
: consumer.getEndpoint().getComponent().getSecurityProvider()
!= null
?
consumer.getEndpoint().getComponent().getSecurityProvider()
: consumer.getEndpoint().getSecurityProvider();
Review Comment:
[P2] Include the producer endpoint's provider when deciding on servlet
context
When a WebSocket producer registers first, `consumer` is null and this
selects only the component's provider. A producer-only endpoint configured with
`securityProvider=#provider` (or endpoint `securityConfiguration`) can have a
provider whose `requireServletContext()` is true while the component provider
is null. The server then starts without a servlet context, but the new upgrade
path calls that endpoint provider's `authenticate()`. Providers such as
`SpringSecurityProvider` dereference `ServletRequestContext` and fail with HTTP
500. The same provider configured at component scope receives a servlet context
and succeeds.
Please pass the producer endpoint's servlet requirements through
registration before starting the host, and cover endpoint-scoped configuration
in the regression tests. `ProviderWithServletWebSocketTest` currently
configures the provider only on the component.
_AI-generated review by Codex on behalf of luigidemasi._
##########
components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandler.java:
##########
@@ -142,37 +169,127 @@ private static void send(
*/
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
- UndertowConsumer currentConsumer = getConsumer();
- if (currentConsumer != null &&
currentConsumer.getEndpoint().getOauthHttpSecurity() != null) {
- if (exchange.isInIoThread()) {
- exchange.dispatch(this);
- return;
+ entryHandler.handleRequest(exchange);
+ }
+
+ /**
+ * The handler that applies the security settings of the path to an
upgrade request and then performs the WebSocket
+ * handshake. A consumer can run its own handlers, such as its access log,
before it.
+ */
+ public HttpHandler getUpgradeHandler() {
+ return upgradeHandler;
+ }
+
+ /**
+ * Lets the given security provider wrap this handler, as it wraps the
handlers of HTTP endpoints. The handler stays
+ * registered as is, so that it remains shared by the consumer and the
producers of the path.
+ */
+ public void wrapWith(UndertowSecurityProvider securityProvider) throws
Exception {
+ HttpHandler wrapped =
securityProvider.wrapHttpHandler(consumerRequestHandler);
+ // a provider that returns no handler disables the path, as it does
for HTTP endpoints
+ this.entryHandler = wrapped != null ? wrapped :
ResponseCodeHandler.HANDLE_405;
+ }
+
+ private void handleConsumerRequest(HttpServerExchange exchange) throws
Exception {
+ consumerHandler.handleRequest(exchange);
+ }
+
+ private void upgrade(HttpServerExchange exchange) throws Exception {
+ List<UndertowEndpoint> endpoints = securityEndpoints();
+ if
(endpoints.stream().noneMatch(CamelWebSocketHandler::hasSecurityChecks)) {
+ this.delegate.handleRequest(exchange);
+ return;
+ }
+ if (exchange.isInIoThread()) {
+ exchange.dispatch(upgradeHandler);
+ return;
+ }
+ Set<String> authenticatedEndpoints = new HashSet<>();
+ Map<String, Object> headers = new HashMap<>();
+ for (UndertowEndpoint endpoint : endpoints) {
+ OAuthHttpSecuritySupport oauthHttpSecurity =
endpoint.getOauthHttpSecurity();
+ if (oauthHttpSecurity != null) {
+ Validation validation =
oauthHttpSecurity.validate(endpoint.getCamelContext(),
authorizationHeaders(exchange));
+ exchange.getRequestHeaders().remove(Headers.AUTHORIZATION);
+ if (!validation.isAuthenticated()) {
+ reject(exchange, validation);
+ return;
+ }
+
exchange.putAttachment(OAUTH_TOKEN_VALIDATION_RESULT_ATTACHMENT,
validation.getValidationResult());
}
- OAuthHttpSecuritySupport oauthHttpSecurity =
currentConsumer.getEndpoint().getOauthHttpSecurity();
- Validation validation =
oauthHttpSecurity.validate(currentConsumer.getEndpoint().getCamelContext(),
- authorizationHeaders(exchange));
- exchange.getRequestHeaders().remove(Headers.AUTHORIZATION);
- if (!validation.isAuthenticated()) {
- reject(exchange, validation);
- return;
+ if (endpoint.requiresAuthentication()) {
+ int statusCode = endpoint.authenticate(exchange);
+ if (statusCode != StatusCodes.OK) {
+ exchange.setStatusCode(statusCode);
+ exchange.endExchange();
+ return;
+ }
+ authenticatedEndpoints.add(endpoint.getEndpointUri());
+ if (endpoint.getSecurityProvider() != null) {
+ endpoint.getSecurityProvider().addHeader(headers::put,
exchange);
+ }
}
- exchange.putAttachment(OAUTH_TOKEN_VALIDATION_RESULT_ATTACHMENT,
validation.getValidationResult());
+ }
+ if (!authenticatedEndpoints.isEmpty()) {
+ exchange.putAttachment(SECURITY_PROVIDER_RESULT_ATTACHMENT,
+ new SecurityProviderResult(authenticatedEndpoints,
headers));
}
this.delegate.handleRequest(exchange);
}
- private UndertowConsumer getConsumer() {
+ /**
+ * The endpoints whose security settings apply to the path: the
consumer's, also while it is stopped, otherwise the
+ * producers'. All the Camel endpoints of a path share its WebSocket
connections.
+ */
+ private List<UndertowEndpoint> securityEndpoints() {
consumerLock.lock();
try {
- return consumer;
+ if (consumerEndpoint != null) {
+ return List.of(consumerEndpoint);
+ }
} finally {
consumerLock.unlock();
}
+ return producerEndpoints.stream().distinct().toList();
}
- private boolean requiresOAuth() {
- UndertowConsumer currentConsumer = getConsumer();
- return currentConsumer != null &&
currentConsumer.getEndpoint().getOauthHttpSecurity() != null;
+ private static boolean hasSecurityChecks(UndertowEndpoint endpoint) {
+ return endpoint.getOauthHttpSecurity() != null ||
endpoint.requiresAuthentication();
+ }
+
+ private static boolean isAuthenticated(WebSocketChannel channel,
List<UndertowEndpoint> endpoints) {
+ for (UndertowEndpoint endpoint : endpoints) {
+ if (!isAuthenticated(channel, endpoint)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Whether the handshake of the given channel passed the security checks
of the given endpoint: its OAuth
+ * validation, its security provider and its allowed roles. Always {@code
true} for an endpoint without such checks.
+ */
+ public static boolean isAuthenticated(WebSocketChannel channel,
UndertowEndpoint endpoint) {
+ if (endpoint.getOauthHttpSecurity() != null && (channel == null
+ || !(channel.getAttribute(
+
OAuthHttpSecuritySupport.OAUTH_TOKEN_VALIDATION_RESULT) instanceof
OAuthTokenValidationResult))) {
+ return false;
+ }
+ if (endpoint.requiresAuthentication()) {
+ return channel != null
+ && channel.getAttribute(SECURITY_PROVIDER_RESULT)
instanceof SecurityProviderResult result
+ && result.endpointUris.contains(endpoint.getEndpointUri());
+ }
+ return true;
Review Comment:
[P1] Account for custom authentication handlers when checking existing
channels
This returns `true` when the endpoint has neither `oauthProfile` nor a
provider/allowed roles, even if its `handlers` chain performs authentication. A
client can connect while only an unsecured producer owns the path, then
continue sending messages to the route and receiving broadcasts after a
consumer with `handlers=#basicAuth` starts. New unauthenticated upgrade
requests correctly receive 401, but the existing connection never passed that
handler chain and is still accepted here.
Please record whether a channel passed the applicable consumer handler chain
and reject channels that did not when that consumer starts, covering both
inbound delivery and outbound sends. Add a regression case alongside the
existing late-consumer test using `handlers=#basicAuth`. This is a gap in the
new handler enforcement; previously WebSocket endpoints ignored these handlers
entirely.
_AI-generated review by Codex on behalf of luigidemasi._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]