diff --git a/bom/pom.xml b/bom/pom.xml
index 7967dcd3bb4..083bb51b51a 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -1255,6 +1255,11 @@
helidon-nima-webclient
${helidon.version}
+
+ io.helidon.nima.webclient
+ helidon-nima-webclient-security
+ ${helidon.version}
+
io.helidon.nima.webclient
helidon-nima-webclient-tracing
diff --git a/nima/webclient/pom.xml b/nima/webclient/pom.xml
index 9802b42aa69..b0dc0dbfe16 100644
--- a/nima/webclient/pom.xml
+++ b/nima/webclient/pom.xml
@@ -38,6 +38,7 @@
webclient
tracing
+ security
diff --git a/nima/webclient/security/pom.xml b/nima/webclient/security/pom.xml
new file mode 100644
index 00000000000..d87c4220a5e
--- /dev/null
+++ b/nima/webclient/security/pom.xml
@@ -0,0 +1,76 @@
+
+
+
+ 4.0.0
+
+ io.helidon.nima.webclient
+ helidon-nima-webclient-project
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ helidon-nima-webclient-security
+ Helidon NĂma WebClient Security
+
+
+
+ io.helidon.nima.webclient
+ helidon-nima-webclient
+
+
+ io.helidon.security
+ helidon-security
+
+
+ io.helidon.security.providers
+ helidon-security-providers-common
+
+
+ io.helidon.common.features
+ helidon-common-features-api
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.helidon.common.features
+ helidon-common-features-processor
+ ${helidon.version}
+
+
+
+
+
+ io.helidon.common.features
+ helidon-common-features-api
+ ${helidon.version}
+
+
+
+
+
+
+
diff --git a/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/WebClientSecurity.java b/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/WebClientSecurity.java
new file mode 100644
index 00000000000..9da8891319b
--- /dev/null
+++ b/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/WebClientSecurity.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.nima.webclient.security;
+
+import java.lang.System.Logger.Level;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import io.helidon.common.context.Context;
+import io.helidon.common.context.Contexts;
+import io.helidon.common.http.ClientRequestHeaders;
+import io.helidon.common.http.Http;
+import io.helidon.nima.webclient.WebClientServiceRequest;
+import io.helidon.nima.webclient.WebClientServiceResponse;
+import io.helidon.nima.webclient.spi.WebClientService;
+import io.helidon.security.EndpointConfig;
+import io.helidon.security.OutboundSecurityClientBuilder;
+import io.helidon.security.OutboundSecurityResponse;
+import io.helidon.security.Security;
+import io.helidon.security.SecurityContext;
+import io.helidon.security.SecurityEnvironment;
+import io.helidon.security.providers.common.OutboundConfig;
+import io.helidon.tracing.Span;
+import io.helidon.tracing.SpanContext;
+import io.helidon.tracing.Tracer;
+
+/**
+ * Client service for security propagation.
+ */
+public class WebClientSecurity implements WebClientService {
+ private static final System.Logger LOGGER = System.getLogger(WebClientSecurity.class.getName());
+
+ private static final String PROVIDER_NAME = "io.helidon.security.rest.client.security.providerName";
+
+ private final Security security;
+
+ private WebClientSecurity() {
+ this(null);
+ }
+
+ private WebClientSecurity(Security security) {
+ this.security = security;
+ }
+
+ /**
+ * Creates new instance of client security service.
+ *
+ * @return client security service
+ */
+ public static WebClientSecurity create() {
+ Context context = Contexts.context().orElseGet(Contexts::globalContext);
+
+ return context.get(Security.class)
+ .map(WebClientSecurity::new) // if available, use constructor with Security parameter
+ .orElseGet(WebClientSecurity::new); // else use constructor without Security parameter
+ }
+
+ /**
+ * Creates new instance of client security service base on {@link io.helidon.security.Security}.
+ *
+ * @param security security instance
+ * @return client security service
+ */
+ public static WebClientSecurity create(Security security) {
+ // if we have one more configuration parameter, we need to switch to builder based pattern
+ return new WebClientSecurity(security);
+ }
+
+ @Override
+ public WebClientServiceResponse handle(Chain chain, WebClientServiceRequest request) {
+ if ("true".equalsIgnoreCase(request.properties().get(OutboundConfig.PROPERTY_DISABLE_OUTBOUND))) {
+ return chain.proceed(request);
+ }
+
+ Context requestContext = request.context();
+ // context either from request or create a new one
+ Optional maybeContext = requestContext.get(SecurityContext.class);
+
+ SecurityContext context;
+
+ if (security == null) {
+ if (maybeContext.isEmpty()) {
+ return chain.proceed(request);
+ } else {
+ context = maybeContext.get();
+ }
+ } else {
+ // we have our own security - we need to use this instance for outbound,
+ // so we cannot re-use the context
+ context = createContext(request);
+ }
+
+ Span span = context.tracer()
+ .spanBuilder("security:outbound")
+ .parent(context.tracingSpan())
+ .start();
+
+ String explicitProvider = request.properties().get(PROVIDER_NAME);
+
+ OutboundSecurityClientBuilder clientBuilder;
+
+ try {
+ SecurityEnvironment.Builder outboundEnv = context.env()
+ .derive()
+ .clearHeaders()
+ .clearQueryParams();
+
+ outboundEnv.method(request.method().text())
+ .path(request.uri().path())
+ .targetUri(URI.create(request.uri().toString()))
+ .queryParams(request.query());
+
+ request.headers()
+ .stream()
+ .forEach(headerValue -> outboundEnv.header(headerValue.name(), headerValue.values()));
+
+ EndpointConfig.Builder outboundEp = context.endpointConfig().derive();
+ Map propMap = request.properties();
+
+ for (String name : propMap.keySet()) {
+ Optional.ofNullable(request.properties().get(name))
+ .ifPresent(property -> outboundEp.addAtribute(name, property));
+ }
+
+ clientBuilder = context.outboundClientBuilder()
+ .outboundEnvironment(outboundEnv)
+ .outboundEndpointConfig(outboundEp)
+ .explicitProvider(explicitProvider);
+
+ } catch (Exception e) {
+ traceError(span, e, null);
+
+ throw e;
+ }
+
+ OutboundSecurityResponse providerResponse = clientBuilder.submit();
+ return processResponse(request, span, providerResponse, chain);
+ }
+
+ private WebClientServiceResponse processResponse(WebClientServiceRequest request,
+ Span span,
+ OutboundSecurityResponse providerResponse,
+ Chain chain) {
+ try {
+ switch (providerResponse.status()) {
+ case FAILURE:
+ case FAILURE_FINISH:
+ traceError(span,
+ providerResponse.throwable().orElse(null),
+ providerResponse.description()
+ .orElse(providerResponse.status().toString()));
+ break;
+ case ABSTAIN:
+ case SUCCESS:
+ case SUCCESS_FINISH:
+ default:
+ break;
+ }
+
+ Map> newHeaders = providerResponse.requestHeaders();
+
+ if (LOGGER.isLoggable(Level.TRACE)) {
+ LOGGER.log(Level.TRACE, "Client filter header(s). SIZE: " + newHeaders.size());
+ }
+
+ ClientRequestHeaders clientHeaders = request.headers();
+ for (Map.Entry> entry : newHeaders.entrySet()) {
+ if (LOGGER.isLoggable(Level.TRACE)) {
+ LOGGER.log(Level.TRACE, " + Header: " + entry.getKey() + ": " + entry.getValue());
+ }
+
+ //replace existing
+ Http.HeaderName headerName = Http.Header.create(entry.getKey());
+ clientHeaders.set(headerName, entry.getValue().toArray(new String[0]));
+ }
+ span.end();
+ return chain.proceed(request);
+ } catch (Exception e) {
+ traceError(span, e, null);
+ throw e;
+ }
+ }
+
+ private SecurityContext createContext(WebClientServiceRequest request) {
+ SecurityContext.Builder builder = security.contextBuilder(UUID.randomUUID().toString())
+ .endpointConfig(EndpointConfig.builder()
+ .build())
+ .env(SecurityEnvironment.builder()
+ .path(request.uri().path())
+ .build());
+ request.context().get(Tracer.class).ifPresent(builder::tracingTracer);
+ request.context().get(SpanContext.class).ifPresent(builder::tracingSpan);
+ return builder.build();
+ }
+
+ static void traceError(Span span, Throwable throwable, String description) {
+ // failed
+ span.status(Span.Status.ERROR);
+
+ if (throwable == null) {
+ span.addEvent("error", Map.of("message", description,
+ "error.kind", "SecurityException"));
+ span.end();
+ } else {
+ span.end(throwable);
+ }
+ }
+}
diff --git a/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/WebClientSecurityProvider.java b/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/WebClientSecurityProvider.java
new file mode 100644
index 00000000000..ee7fc8f40dc
--- /dev/null
+++ b/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/WebClientSecurityProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.nima.webclient.security;
+
+import io.helidon.common.config.Config;
+import io.helidon.nima.webclient.spi.WebClientService;
+import io.helidon.nima.webclient.spi.WebClientServiceProvider;
+
+/**
+ * Client security SPI provider.
+ *
+ * @deprecated This class should only be used via {@link java.util.ServiceLoader}.
+ * Use {@link io.helidon.nima.webclient.security.WebClientSecurity} instead
+ */
+@Deprecated
+public class WebClientSecurityProvider implements WebClientServiceProvider {
+
+ /**
+ * Required public constructor.
+ *
+ * @deprecated This class should only be used via {@link java.util.ServiceLoader}.
+ */
+ @Deprecated
+ public WebClientSecurityProvider() {
+ }
+
+ @Override
+ public String configKey() {
+ return "security";
+ }
+
+ @Override
+ public WebClientService create(Config config) {
+ return WebClientSecurity.create();
+ }
+}
diff --git a/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/package-info.java b/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/package-info.java
new file mode 100644
index 00000000000..4b82b2bbe2d
--- /dev/null
+++ b/nima/webclient/security/src/main/java/io/helidon/nima/webclient/security/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Security propagation service.
+ */
+package io.helidon.nima.webclient.security;
diff --git a/nima/webclient/security/src/main/java/module-info.java b/nima/webclient/security/src/main/java/module-info.java
new file mode 100644
index 00000000000..24731d6ce6d
--- /dev/null
+++ b/nima/webclient/security/src/main/java/module-info.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import io.helidon.common.features.api.Feature;
+import io.helidon.common.features.api.HelidonFlavor;
+
+/**
+ * Helidon WebClient Security.
+ */
+@Feature(value = "Security",
+ description = "Web client support for security",
+ in = HelidonFlavor.SE,
+ path = {"WebClient", "Security"}
+)
+module io.helidon.nima.webclient.security {
+ requires static io.helidon.common.features.api;
+
+ requires io.helidon.nima.webclient;
+ requires io.helidon.security;
+ requires io.helidon.security.providers.common;
+
+ exports io.helidon.nima.webclient.security;
+
+ provides io.helidon.nima.webclient.spi.WebClientServiceProvider
+ with io.helidon.nima.webclient.security.WebClientSecurityProvider;
+}
\ No newline at end of file
diff --git a/nima/webclient/tracing/pom.xml b/nima/webclient/tracing/pom.xml
index 7dd5697bcf0..fdfb196b5e3 100644
--- a/nima/webclient/tracing/pom.xml
+++ b/nima/webclient/tracing/pom.xml
@@ -22,6 +22,7 @@
io.helidon.nima.webclient
helidon-nima-webclient-project
4.0.0-SNAPSHOT
+ ../pom.xml
helidon-nima-webclient-tracing
@@ -39,7 +40,6 @@
io.helidon.common.features
helidon-common-features-api
- provided
true
@@ -78,6 +78,13 @@
+
+
+ io.helidon.common.features
+ helidon-common-features-api
+ ${helidon.version}
+
+
diff --git a/nima/webclient/tracing/src/main/java/module-info.java b/nima/webclient/tracing/src/main/java/module-info.java
index 23c9f8312b8..42c0902cc78 100644
--- a/nima/webclient/tracing/src/main/java/module-info.java
+++ b/nima/webclient/tracing/src/main/java/module-info.java
@@ -14,12 +14,25 @@
* limitations under the License.
*/
+import io.helidon.common.features.api.Feature;
+import io.helidon.common.features.api.HelidonFlavor;
+
+/**
+ * Helidon WebClient Tracing.
+ */
+@Feature(value = "Tracing",
+ description = "Web client support for tracing",
+ in = HelidonFlavor.SE,
+ path = {"WebClient", "Tracing"}
+)
module io.helidon.nima.webclient.tracing {
- exports io.helidon.nima.webclient.tracing;
+ requires static io.helidon.common.features.api;
requires io.helidon.nima.webclient;
requires io.helidon.tracing;
+ exports io.helidon.nima.webclient.tracing;
+
provides io.helidon.nima.webclient.spi.WebClientServiceProvider
with io.helidon.nima.webclient.tracing.WebClientTracingProvider;
}
\ No newline at end of file
diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java
index 3bf48ce7829..138b5a55be0 100644
--- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java
+++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java
@@ -74,7 +74,7 @@ class ClientRequestImpl implements Http1ClientRequest {
Map properties) {
this.method = method;
this.uri = helper;
- this.properties = properties;
+ this.properties = new HashMap<>(properties);
this.clientConfig = clientConfig;
this.mediaContext = clientConfig.mediaContext();