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();