This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch 3710-centrally-manage-opc-ua-certificates in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit b2f136238611a09a9603f41b82cb57ddd1c559f4 Author: Dominik Riemer <[email protected]> AuthorDate: Tue Jul 29 13:50:55 2025 +0200 feat(#3710): Add initial support for centrally managed OPC-UA certificates --- .../streampipes/client/api/ICustomRequestApi.java | 3 + .../streampipes/client/api/AbstractClientApi.java | 7 + .../streampipes/client/api/CustomRequestApi.java | 8 ++ .../connectors/opcua/adapter/OpcUaAdapter.java | 7 +- .../opcua/adapter/OpcUaSchemaProvider.java | 7 +- .../opcua/config/SpOpcUaConfigExtractor.java | 24 ++-- .../security/CompositeCertificateValidator.java | 146 +++++++++++++++++++++ .../opcua/config/security/SecurityConfig.java | 51 ++++++- .../connectors/opcua/sink/OpcUaSink.java | 2 +- .../connectors/opcua/utils/OpcUaUtils.java | 8 +- .../streampipes/model/opcua/Certificate.java | 142 ++++++++++++++++++++ .../CertificateState.java} | 34 +---- .../rest/impl/admin/CertificateResource.java | 102 ++++++++++++++ .../streampipes/storage/api/INoSqlStorage.java | 3 + .../storage/couchdb/CouchDbStorageManager.java | 9 ++ .../src/lib/apis/certificate.service.ts | 54 ++++++++ .../src/lib/model/gen/streampipes-model.ts | 140 ++++++++------------ ui/src/app/configuration/configuration.module.ts | 2 + .../certificate-configuration.component.html | 73 +++++++++++ .../certificate-configuration.component.scss | 0 .../certificate-configuration.component.ts | 46 +++++++ .../extensions-service-management.component.html | 6 + .../services/transformation-rule.service.ts | 38 ------ 23 files changed, 742 insertions(+), 170 deletions(-) diff --git a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java b/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java index 9cbc285e51..c29999ad34 100644 --- a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java +++ b/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java @@ -18,6 +18,7 @@ package org.apache.streampipes.client.api; +import java.util.List; import java.util.Map; public interface ICustomRequestApi { @@ -26,4 +27,6 @@ public interface ICustomRequestApi { <T> T sendGet(String apiPath, Class<T> responseClass); <T> T sendGet(String apiPath, Map<String, String> queryParameters, Class<T> responseClass); + + <T> List<T> getList(String apiPath, Class<T> response); } diff --git a/streampipes-client/src/main/java/org/apache/streampipes/client/api/AbstractClientApi.java b/streampipes-client/src/main/java/org/apache/streampipes/client/api/AbstractClientApi.java index fdf3c1a86a..17be13e40f 100644 --- a/streampipes-client/src/main/java/org/apache/streampipes/client/api/AbstractClientApi.java +++ b/streampipes-client/src/main/java/org/apache/streampipes/client/api/AbstractClientApi.java @@ -25,6 +25,7 @@ import org.apache.streampipes.client.http.PostRequestWithoutPayload; import org.apache.streampipes.client.http.PostRequestWithoutPayloadResponse; import org.apache.streampipes.client.http.PutRequest; import org.apache.streampipes.client.model.StreamPipesClientConfig; +import org.apache.streampipes.client.serializer.ListSerializer; import org.apache.streampipes.client.serializer.ObjectSerializer; import org.apache.streampipes.client.serializer.Serializer; import org.apache.streampipes.client.util.StreamPipesApiPath; @@ -33,6 +34,7 @@ import org.apache.streampipes.commons.exceptions.SpRuntimeException; import org.apache.http.HttpStatus; +import java.util.List; import java.util.Optional; public class AbstractClientApi { @@ -86,6 +88,11 @@ public class AbstractClientApi { return new GetRequest<>(clientConfig, apiPath, targetClass, serializer).executeRequest(); } + protected <T> List<T> getList(StreamPipesApiPath apiPath, Class<T> targetClass) throws SpRuntimeException { + ListSerializer<Void, T> serializer = new ListSerializer<>(); + return new GetRequest<>(clientConfig, apiPath, targetClass, serializer).executeRequest(); + } + protected <T> Optional<T> getSingleOpt(StreamPipesApiPath apiPath, Class<T> targetClass) throws SpRuntimeException { try { diff --git a/streampipes-client/src/main/java/org/apache/streampipes/client/api/CustomRequestApi.java b/streampipes-client/src/main/java/org/apache/streampipes/client/api/CustomRequestApi.java index 42fccc3027..9d3b624d97 100644 --- a/streampipes-client/src/main/java/org/apache/streampipes/client/api/CustomRequestApi.java +++ b/streampipes-client/src/main/java/org/apache/streampipes/client/api/CustomRequestApi.java @@ -20,6 +20,7 @@ package org.apache.streampipes.client.api; import org.apache.streampipes.client.model.StreamPipesClientConfig; import org.apache.streampipes.client.util.StreamPipesApiPath; +import java.util.List; import java.util.Map; public class CustomRequestApi extends AbstractClientApi implements ICustomRequestApi { @@ -46,4 +47,11 @@ public class CustomRequestApi extends AbstractClientApi implements ICustomReques responseClass); } + @Override + public <T> List<T> getList(String apiPath, Class<T> responseClass) { + return getList( + StreamPipesApiPath.fromStreamPipesBasePath(apiPath), responseClass + ); + } + } diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java index 71ff0e1878..75d0b2ec44 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java @@ -204,7 +204,10 @@ public class OpcUaAdapter implements StreamPipesAdapter, IPullAdapter, SupportsR IEventCollector collector, IAdapterRuntimeContext adapterRuntimeContext) throws AdapterException { this.opcUaAdapterConfig = - SpOpcUaConfigExtractor.extractAdapterConfig(extractor.getStaticPropertyExtractor()); + SpOpcUaConfigExtractor.extractAdapterConfig( + extractor.getStaticPropertyExtractor(), + adapterRuntimeContext.getStreamPipesClient() + ); this.collector = collector; this.prepareAdapter(extractor); this.numberOfEventProperties = @@ -252,6 +255,6 @@ public class OpcUaAdapter implements StreamPipesAdapter, IPullAdapter, SupportsR @Override public GuessSchema onSchemaRequested(IAdapterParameterExtractor extractor, IAdapterGuessSchemaContext adapterGuessSchemaContext) throws AdapterException { - return new OpcUaSchemaProvider().getSchema(clientProvider, extractor); + return new OpcUaSchemaProvider().getSchema(clientProvider, extractor, adapterGuessSchemaContext.getStreamPipesClient()); } } diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaSchemaProvider.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaSchemaProvider.java index f40065bf27..c8d18c03d5 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaSchemaProvider.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaSchemaProvider.java @@ -18,6 +18,7 @@ package org.apache.streampipes.extensions.connectors.opcua.adapter; +import org.apache.streampipes.client.api.IStreamPipesClient; import org.apache.streampipes.commons.exceptions.connect.AdapterException; import org.apache.streampipes.commons.exceptions.connect.ParseException; import org.apache.streampipes.extensions.api.extractor.IAdapterParameterExtractor; @@ -51,7 +52,8 @@ public class OpcUaSchemaProvider { * @throws ParseException */ public GuessSchema getSchema(OpcUaClientProvider clientProvider, - IAdapterParameterExtractor extractor) + IAdapterParameterExtractor extractor, + IStreamPipesClient streamPipesClient) throws AdapterException, ParseException { var builder = GuessSchemaBuilder.create(); EventSchema eventSchema = new EventSchema(); @@ -60,7 +62,8 @@ public class OpcUaSchemaProvider { List<EventProperty> allProperties = new ArrayList<>(); var opcUaConfig = SpOpcUaConfigExtractor.extractAdapterConfig( - extractor.getStaticPropertyExtractor() + extractor.getStaticPropertyExtractor(), + streamPipesClient ); try { var connectedClient = clientProvider.getClient(opcUaConfig); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java index 278f3bb691..36f49e4754 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java @@ -18,6 +18,7 @@ package org.apache.streampipes.extensions.connectors.opcua.config; +import org.apache.streampipes.client.api.IStreamPipesClient; import org.apache.streampipes.extensions.api.extractor.IParameterExtractor; import org.apache.streampipes.extensions.api.extractor.IStaticPropertyExtractor; import org.apache.streampipes.extensions.connectors.opcua.config.identity.AnonymousIdentityConfig; @@ -53,8 +54,9 @@ public class SpOpcUaConfigExtractor { * @param extractor extractor for user inputs * @return {@link OpcUaAdapterConfig} instance based on information from {@code extractor} */ - public static OpcUaAdapterConfig extractAdapterConfig(IStaticPropertyExtractor extractor) { - var config = extractSharedConfig(extractor, new OpcUaAdapterConfig()); + public static OpcUaAdapterConfig extractAdapterConfig(IStaticPropertyExtractor extractor, + IStreamPipesClient streamPipesClient) { + var config = extractSharedConfig(extractor, new OpcUaAdapterConfig(), streamPipesClient); boolean usePullMode = extractor.selectedAlternativeInternalId(ADAPTER_TYPE.name()) .equals(PULL_MODE.name()); @@ -78,12 +80,14 @@ public class SpOpcUaConfigExtractor { return config; } - public static OpcUaConfig extractSinkConfig(IParameterExtractor extractor) { - return extractSharedConfig(extractor, new OpcUaConfig()); + public static OpcUaConfig extractSinkConfig(IParameterExtractor extractor, + IStreamPipesClient streamPipesClient) { + return extractSharedConfig(extractor, new OpcUaConfig(), streamPipesClient); } public static <T extends OpcUaConfig> T extractSharedConfig(IParameterExtractor extractor, - T config) { + T config, + IStreamPipesClient streamPipesClient) { String selectedAlternativeConnection = extractor.selectedAlternativeInternalId(OPC_HOST_OR_URL.name()); @@ -103,9 +107,13 @@ public class SpOpcUaConfigExtractor { SharedUserConfiguration.SECURITY_POLICY, String.class ); - config.setSecurityConfig(new SecurityConfig( - MessageSecurityMode.valueOf(selectedSecurityMode), - SecurityPolicy.valueOf(selectedSecurityPolicy))); + config.setSecurityConfig( + new SecurityConfig( + MessageSecurityMode.valueOf(selectedSecurityMode), + SecurityPolicy.valueOf(selectedSecurityPolicy), + streamPipesClient + ) + ); boolean useURL = selectedAlternativeConnection.equals(OPC_URL.name()); if (useURL) { diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java new file mode 100644 index 0000000000..b9d5a1d5fc --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.streampipes.extensions.connectors.opcua.config.security; + +import org.apache.streampipes.client.api.IStreamPipesClient; +import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaUtils; +import org.apache.streampipes.model.opcua.Certificate; +import org.apache.streampipes.model.opcua.CertificateState; + +import org.eclipse.milo.opcua.stack.client.security.ClientCertificateValidator; +import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.security.TrustListManager; +import org.eclipse.milo.opcua.stack.core.util.validation.CertificateValidationUtil; +import org.eclipse.milo.opcua.stack.core.util.validation.ValidationCheck; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.stream.Stream; + +public class CompositeCertificateValidator implements ClientCertificateValidator { + + private static final Logger LOG = LoggerFactory.getLogger(CompositeCertificateValidator.class); + + private final TrustListManager trustListManager; + private final List<X509Certificate> trustedCerts; + private final List<ValidationCheck> validationChecks; + private final IStreamPipesClient streamPipesClient; + + public CompositeCertificateValidator(TrustListManager trustListManager, + List<X509Certificate> trustedCerts, + List<ValidationCheck> validationChecks, + IStreamPipesClient streamPipesClient) { + this.trustListManager = trustListManager; + this.trustedCerts = trustedCerts; + this.validationChecks = validationChecks; + this.streamPipesClient = streamPipesClient; + } + + @Override + public void validateCertificateChain(List<X509Certificate> certificateChain) throws UaException { + PKIXCertPathBuilderResult certPathResult; + + try { + certPathResult = CertificateValidationUtil.buildTrustedCertPath( + certificateChain, + Stream.concat(trustListManager.getTrustedCertificates().stream(), trustedCerts.stream()).toList(), + trustListManager.getIssuerCertificates() + ); + } catch (UaException e) { + sendToCore(certificateChain.get(0)); + throw e; + } + + var crls = new ArrayList<X509CRL>(); + crls.addAll(trustListManager.getTrustedCrls()); + crls.addAll(trustListManager.getIssuerCrls()); + + CertificateValidationUtil.validateTrustedCertPath( + certPathResult.getCertPath(), + certPathResult.getTrustAnchor(), + crls, + ValidationCheck.NO_OPTIONAL_CHECKS, + false + ); + } + + @Override + public void validateCertificateChain( + List<X509Certificate> certificateChain, + String applicationUri, + String... validHostNames + ) throws UaException { + + validateCertificateChain(certificateChain); + + X509Certificate certificate = certificateChain.get(0); + + try { + CertificateValidationUtil.checkApplicationUri(certificate, applicationUri); + } catch (UaException e) { + if (validationChecks.contains(ValidationCheck.APPLICATION_URI)) { + throw e; + } else { + LOG.warn( + "check suppressed: certificate failed application uri check: {} != {}", + applicationUri, CertificateValidationUtil.getSubjectAltNameUri(certificate) + ); + } + } + + try { + CertificateValidationUtil.checkHostnameOrIpAddress(certificate, validHostNames); + } catch (UaException e) { + if (validationChecks.contains(ValidationCheck.HOSTNAME)) { + throw e; + } else { + LOG.warn( + "check suppressed: certificate failed hostname check: {}", + certificate.getSubjectX500Principal().getName() + ); + } + } + } + + private void sendToCore(X509Certificate cert) { + try { + var certificate = new Certificate( + cert.getSubjectX500Principal().getName(), + cert.getIssuerX500Principal().getName(), + cert.getSerialNumber().toString(), + cert.getNotBefore().toString(), + cert.getNotAfter().toString(), + cert.getSigAlgName(), + cert.getPublicKey().getAlgorithm(), + Base64.getEncoder().encodeToString(cert.getEncoded()), + CertificateState.REJECTED + ); + + streamPipesClient.customRequest().sendPost(OpcUaUtils.getCoreCertificatePath(), certificate); + } catch (Exception ex) { + LOG.error("Failed to report rejected certificate to API", ex); + } + } +} diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/SecurityConfig.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/SecurityConfig.java index 7e655b7e3b..0ff7c0d401 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/SecurityConfig.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/SecurityConfig.java @@ -18,30 +18,42 @@ package org.apache.streampipes.extensions.connectors.opcua.config.security; +import org.apache.streampipes.client.api.IStreamPipesClient; import org.apache.streampipes.commons.environment.Environments; import org.apache.streampipes.commons.exceptions.SpConfigurationException; +import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaUtils; +import org.apache.streampipes.model.opcua.Certificate; import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder; -import org.eclipse.milo.opcua.stack.client.security.DefaultClientCertificateValidator; import org.eclipse.milo.opcua.stack.core.security.DefaultTrustListManager; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Paths; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; public class SecurityConfig { private final MessageSecurityMode securityMode; private final SecurityPolicy securityPolicy; + private final IStreamPipesClient streamPipesClient; public SecurityConfig(MessageSecurityMode securityMode, - SecurityPolicy securityPolicy) { + SecurityPolicy securityPolicy, + IStreamPipesClient streamPipesClient) { this.securityMode = securityMode; this.securityPolicy = securityPolicy; + this.streamPipesClient = streamPipesClient; } public void configureSecurityPolicy(String opcServerUrl, @@ -72,12 +84,20 @@ public class SecurityConfig { var securityDir = Paths.get(env.getOpcUaSecurityDir().getValueOrDefault()); var trustListManager = new DefaultTrustListManager(securityDir.resolve("pki").toFile()); - var certificateValidator = new DefaultClientCertificateValidator(trustListManager); + var loadedCerts = new AtomicReference<>(fetchTrustedCertsFromRest()); + + var compositeValidator = new CompositeCertificateValidator( + trustListManager, + loadedCerts.get(), + List.of(), + streamPipesClient + ); + var loader = new KeyStoreLoader().load(env, securityDir); builder.setKeyPair(loader.getClientKeyPair()); builder.setCertificate(loader.getClientCertificate()); builder.setCertificateChain(loader.getClientCertificateChain()); - builder.setCertificateValidator(certificateValidator); + builder.setCertificateValidator(compositeValidator); } catch (Exception e) { throw new SpConfigurationException( "Failed to load keystore - check that all required environment variables " @@ -108,6 +128,29 @@ public class SecurityConfig { original.getSecurityLevel()); } + private List<X509Certificate> fetchTrustedCertsFromRest() throws SpConfigurationException { + try { + var response = streamPipesClient.customRequest().getList(OpcUaUtils.getCoreCertificatePath(), Certificate.class); + return response + .stream() + .map(res -> { + byte[] derBytes = Base64.getDecoder().decode(res.getCertificateDerBase64()); + CertificateFactory certFactory = null; + try { + certFactory = CertificateFactory.getInstance("X.509"); + try (ByteArrayInputStream in = new ByteArrayInputStream(derBytes)) { + return (X509Certificate) certFactory.generateCertificate(in); + } + } catch (CertificateException | IOException e) { + throw new RuntimeException(e); + } + }) + .toList(); + } catch (Exception e) { + throw new SpConfigurationException("Could not fetch trusted certificates from REST API", e); + } + } + @Override public String toString() { return String.format("%s-%s", securityMode, securityPolicy); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java index 1674b2f677..6e908f1102 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java @@ -79,7 +79,7 @@ public class OpcUaSink implements IStreamPipesDataSink, SupportsRuntimeConfig { public void onPipelineStarted(IDataSinkParameters parameters, EventSinkRuntimeContext runtimeContext) { var extractor = parameters.extractor(); - var config = SpOpcUaConfigExtractor.extractSinkConfig(extractor); + var config = SpOpcUaConfigExtractor.extractSinkConfig(extractor, runtimeContext.getStreamPipesClient()); String mappingPropertySelector = extractor.mappingPropertyValue(MAPPING_PROPERY.name()); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java index 81eb6e4fbb..4f0fbb9b44 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java @@ -26,6 +26,7 @@ import org.apache.streampipes.extensions.connectors.opcua.client.OpcUaClientProv import org.apache.streampipes.extensions.connectors.opcua.config.OpcUaAdapterConfig; import org.apache.streampipes.extensions.connectors.opcua.config.SharedUserConfiguration; import org.apache.streampipes.extensions.connectors.opcua.config.SpOpcUaConfigExtractor; +import org.apache.streampipes.extensions.management.client.StreamPipesClientResolver; import org.apache.streampipes.model.staticproperty.RuntimeResolvableTreeInputStaticProperty; import org.eclipse.milo.opcua.sdk.client.api.UaClient; @@ -66,6 +67,7 @@ public class OpcUaUtils { IStaticPropertyExtractor parameterExtractor) throws SpConfigurationException { + var client = new StreamPipesClientResolver().makeStreamPipesClientInstance(); RuntimeResolvableTreeInputStaticProperty config = parameterExtractor .getStaticPropertyByName(internalName, RuntimeResolvableTreeInputStaticProperty.class); // access mode and host/url have to be selected @@ -77,7 +79,7 @@ public class OpcUaUtils { return config; } - var opcUaConfig = SpOpcUaConfigExtractor.extractSharedConfig(parameterExtractor, new OpcUaAdapterConfig()); + var opcUaConfig = SpOpcUaConfigExtractor.extractSharedConfig(parameterExtractor, new OpcUaAdapterConfig(), client); try { var connectedClient = clientProvider.getClient(opcUaConfig); @@ -121,4 +123,8 @@ public class OpcUaUtils { } }).toList(); } + + public static String getCoreCertificatePath() { + return "/api/v2/admin/certificates/trusted"; + } } diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java new file mode 100644 index 0000000000..df1a65b4b0 --- /dev/null +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.streampipes.model.opcua; + +import org.apache.streampipes.model.shared.annotation.TsModel; +import org.apache.streampipes.model.shared.api.Storable; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.google.gson.annotations.SerializedName; + +import java.util.Objects; + +@TsModel +public final class Certificate implements Storable { + + @JsonAlias("_id") + @SerializedName("_id") + private String elementId; + + @JsonAlias("_rev") + @SerializedName("_rev") + private String rev; + + private String subjectDn; + private String issuerDn; + private String serialNumber; + private String notBefore; + private String notAfter; + private String sigAlgName; + private String algorithm; + private String certificateDerBase64; + private CertificateState state; + + public Certificate() { + } + + public Certificate(String subjectDn, + String issuerDn, + String serialNumber, + String notBefore, + String notAfter, + String sigAlgName, + String algorithm, + String certificateDerBase64, + CertificateState state) { + this.subjectDn = subjectDn; + this.issuerDn = issuerDn; + this.serialNumber = serialNumber; + this.notBefore = notBefore; + this.notAfter = notAfter; + this.sigAlgName = sigAlgName; + this.algorithm = algorithm; + this.certificateDerBase64 = certificateDerBase64; + this.state = state; + } + + @Override + public String getRev() { + return rev; + } + + @Override + public void setRev(String rev) { + this.rev = rev; + } + + @Override + public String getElementId() { + return elementId; + } + + @Override + public void setElementId(String elementId) { + this.elementId = elementId; + } + + public String getSubjectDn() { + return subjectDn; + } + + public String getIssuerDn() { + return issuerDn; + } + + public String getSerialNumber() { + return serialNumber; + } + + public String getNotBefore() { + return notBefore; + } + + public String getNotAfter() { + return notAfter; + } + + public String getSigAlgName() { + return sigAlgName; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getCertificateDerBase64() { + return certificateDerBase64; + } + + public CertificateState getState() { + return state; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Certificate that = (Certificate) o; + return Objects.equals(getSubjectDn(), that.getSubjectDn()) && Objects.equals(getIssuerDn(), that.getIssuerDn()) && Objects.equals(getSerialNumber(), that.getSerialNumber()) && Objects.equals(getNotBefore(), that.getNotBefore()) && Objects.equals(getNotAfter(), that.getNotAfter()) && Objects.equals(getSigAlgName(), that.getSigAlgName()) && Objects.equals(getAlgorithm(), that.getAlgorithm()) && Objects.equals(getCertificateDerBase64(), that.getCertificateDerBase64()) && getState() == t [...] + } + + @Override + public int hashCode() { + return Objects.hash(getSubjectDn(), getIssuerDn(), getSerialNumber(), getNotBefore(), getNotAfter(), getSigAlgName(), getAlgorithm(), getCertificateDerBase64(), getState()); + } +} diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/SpDataStreamContainer.java b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateState.java similarity index 53% rename from streampipes-model/src/main/java/org/apache/streampipes/model/SpDataStreamContainer.java rename to streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateState.java index a59d6fb3a7..100ffc0be7 100644 --- a/streampipes-model/src/main/java/org/apache/streampipes/model/SpDataStreamContainer.java +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateState.java @@ -15,36 +15,10 @@ * limitations under the License. * */ -package org.apache.streampipes.model; -import org.apache.streampipes.model.shared.annotation.TsModel; +package org.apache.streampipes.model.opcua; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -import java.util.ArrayList; -import java.util.List; - -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class") -@TsModel -public class SpDataStreamContainer { - - private List<SpDataStream> list; - - public SpDataStreamContainer() { - super(); - this.list = new ArrayList<>(); - } - - public SpDataStreamContainer(List<SpDataStream> dataStreams) { - super(); - this.list = dataStreams; - } - - public List<SpDataStream> getList() { - return list; - } - - public void setList(List<SpDataStream> list) { - this.list = list; - } +public enum CertificateState { + REJECTED, + TRUSTED } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/CertificateResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/CertificateResource.java new file mode 100644 index 0000000000..f8934ea422 --- /dev/null +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/CertificateResource.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.streampipes.rest.impl.admin; + +import org.apache.streampipes.model.opcua.Certificate; +import org.apache.streampipes.model.opcua.CertificateState; +import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; +import org.apache.streampipes.rest.security.AuthConstants; +import org.apache.streampipes.storage.api.CRUDStorage; +import org.apache.streampipes.storage.management.StorageDispatcher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@PreAuthorize(AuthConstants.IS_ADMIN_ROLE) +@RequestMapping("/api/v2/admin/certificates") +public class CertificateResource extends AbstractAuthGuardedRestResource { + + private static final Logger LOG = LoggerFactory.getLogger(CertificateResource.class); + + private final CRUDStorage<Certificate> certificateStorage = StorageDispatcher + .INSTANCE.getNoSqlStore().getCertificateStorage(); + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List<Certificate> getAll() { + return certificateStorage.findAll(); + } + + @GetMapping(value = "trusted", produces = MediaType.APPLICATION_JSON_VALUE) + public List<Certificate> getTrusted() { + return certificateStorage + .findAll() + .stream() + .filter(c -> c.getState() == CertificateState.TRUSTED) + .toList(); + } + + @PutMapping( + value = "{id}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Certificate update(@PathVariable String id, + @RequestBody Certificate certificate) { + if (!id.equals(certificate.getElementId())) { + throw new IllegalArgumentException("ID in path and body do not match"); + } + + return certificateStorage.updateElement(certificate); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public void create(@RequestBody Certificate certificate) { + // check if the certificate already exists + var allCertificates = certificateStorage.findAll(); + if (allCertificates.stream() + .noneMatch(c -> c.equals(certificate))) { + certificateStorage.persist(certificate); + } else { + LOG.info("Certificate with IssuerDN {} already exists, skipping creation", certificate.getIssuerDn()); + } + + } + + @DeleteMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public void delete(@PathVariable String id) { + var certificate = certificateStorage.getElementById(id); + if (certificate == null) { + throw new IllegalArgumentException("Certificate with ID " + id + " does not exist"); + } + certificateStorage.deleteElement(certificate); + } +} diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java index 742adf1409..30bbe30792 100644 --- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java +++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java @@ -28,6 +28,7 @@ import org.apache.streampipes.model.datalake.DataLakeMeasure; import org.apache.streampipes.model.extensions.configuration.SpServiceConfiguration; import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceRegistration; import org.apache.streampipes.model.file.FileMetadata; +import org.apache.streampipes.model.opcua.Certificate; import org.apache.streampipes.model.template.CompactPipelineTemplate; public interface INoSqlStorage { @@ -85,4 +86,6 @@ public interface INoSqlStorage { CRUDStorage<Privilege> getPrivilegeStorage(); CRUDStorage<CompactPipelineTemplate> getPipelineTemplateStorage(); + + CRUDStorage<Certificate> getCertificateStorage(); } diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java index c6edb2cfcf..815130c278 100644 --- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java +++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java @@ -28,6 +28,7 @@ import org.apache.streampipes.model.datalake.DataLakeMeasure; import org.apache.streampipes.model.extensions.configuration.SpServiceConfiguration; import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceRegistration; import org.apache.streampipes.model.file.FileMetadata; +import org.apache.streampipes.model.opcua.Certificate; import org.apache.streampipes.model.template.CompactPipelineTemplate; import org.apache.streampipes.storage.api.CRUDStorage; import org.apache.streampipes.storage.api.IAdapterStorage; @@ -237,4 +238,12 @@ public enum CouchDbStorageManager implements INoSqlStorage { CompactPipelineTemplate.class ); } + + @Override + public CRUDStorage<Certificate> getCertificateStorage() { + return new DefaultCrudStorage<>( + () -> Utils.getCouchDbGsonClient("certificates"), + Certificate.class + ); + } } diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/certificate.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/certificate.service.ts new file mode 100644 index 0000000000..d6afe9f207 --- /dev/null +++ b/ui/projects/streampipes/platform-services/src/lib/apis/certificate.service.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { + Certificate, + PlatformServicesCommons, +} from '@streampipes/platform-services'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class CertificateService { + private http = inject(HttpClient); + private platformServicesCommons = inject(PlatformServicesCommons); + + getAllCertificates(): Observable<Certificate[]> { + return this.http.get<Certificate[]>(this.certificateBasePath); + } + + updateCertificate(certificate: Certificate): Observable<Certificate> { + return this.http.put<Certificate>( + `${this.certificateBasePath}/${certificate.elementId}`, + certificate, + ); + } + + deleteCertificate(certificateId: string): Observable<void> { + return this.http.delete<void>( + `${this.certificateBasePath}/${certificateId}`, + ); + } + + private get certificateBasePath() { + return this.platformServicesCommons.apiBasePath + '/admin/certificates'; + } +} diff --git a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts index d8d6f02e56..0c1539abb4 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts +++ b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts @@ -1,26 +1,7 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - /* tslint:disable */ /* eslint-disable */ // @ts-nocheck -// Generated using typescript-generator version 3.2.1263 on 2025-04-04 12:37:31. +// Generated using typescript-generator version 3.2.1263 on 2025-07-29 10:59:26. export class NamedStreamPipesEntity implements Storable { '@class': @@ -215,7 +196,6 @@ export class TransformationRuleDescription { | 'org.apache.streampipes.model.connect.rules.stream.EventRateTransformationRuleDescription' | 'org.apache.streampipes.model.connect.rules.stream.RemoveDuplicatesTransformationRuleDescription' | 'org.apache.streampipes.model.connect.rules.schema.SchemaTransformationRuleDescription' - | 'org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription' | 'org.apache.streampipes.model.connect.rules.schema.DeleteRuleDescription' | 'org.apache.streampipes.model.connect.rules.schema.RenameRuleDescription' | 'org.apache.streampipes.model.connect.rules.schema.MoveRuleDescription'; @@ -255,8 +235,6 @@ export class TransformationRuleDescription { return RemoveDuplicatesTransformationRuleDescription.fromData( data, ); - case 'org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription': - return CreateNestedRuleDescription.fromData(data); case 'org.apache.streampipes.model.connect.rules.schema.DeleteRuleDescription': return DeleteRuleDescription.fromData(data); case 'org.apache.streampipes.model.connect.rules.schema.RenameRuleDescription': @@ -673,6 +651,39 @@ export class CanvasPosition { } } +export class Certificate implements Storable { + algorithm: string; + certificateDerBase64: string; + elementId: string; + issuerDn: string; + notAfter: string; + notBefore: string; + rev: string; + serialNumber: string; + sigAlgName: string; + state: CertificateState; + subjectDn: string; + + static fromData(data: Certificate, target?: Certificate): Certificate { + if (!data) { + return data; + } + const instance = target || new Certificate(); + instance.algorithm = data.algorithm; + instance.certificateDerBase64 = data.certificateDerBase64; + instance.elementId = data.elementId; + instance.issuerDn = data.issuerDn; + instance.notAfter = data.notAfter; + instance.notBefore = data.notBefore; + instance.rev = data.rev; + instance.serialNumber = data.serialNumber; + instance.sigAlgName = data.sigAlgName; + instance.state = data.state; + instance.subjectDn = data.subjectDn; + return instance; + } +} + export class ChangeDatatypeTransformationRuleDescription extends ValueTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.value.ChangeDatatypeTransformationRuleDescription'; 'originalDatatypeXsd': string; @@ -990,48 +1001,6 @@ export class CorrectionValueTransformationRuleDescription extends ValueTransform } } -export class SchemaTransformationRuleDescription extends TransformationRuleDescription { - '@class': - | 'org.apache.streampipes.model.connect.rules.schema.SchemaTransformationRuleDescription' - | 'org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription' - | 'org.apache.streampipes.model.connect.rules.schema.DeleteRuleDescription' - | 'org.apache.streampipes.model.connect.rules.schema.RenameRuleDescription' - | 'org.apache.streampipes.model.connect.rules.schema.MoveRuleDescription'; - - static 'fromData'( - data: SchemaTransformationRuleDescription, - target?: SchemaTransformationRuleDescription, - ): SchemaTransformationRuleDescription { - if (!data) { - return data; - } - const instance = target || new SchemaTransformationRuleDescription(); - super.fromData(data, instance); - return instance; - } -} - -/** - * @deprecated since 0.97.0, for removal - */ -export class CreateNestedRuleDescription extends SchemaTransformationRuleDescription { - '@class': 'org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription'; - 'runtimeKey': string; - - static 'fromData'( - data: CreateNestedRuleDescription, - target?: CreateNestedRuleDescription, - ): CreateNestedRuleDescription { - if (!data) { - return data; - } - const instance = target || new CreateNestedRuleDescription(); - super.fromData(data, instance); - instance.runtimeKey = data.runtimeKey; - return instance; - } -} - export class CreateOptions { persist: boolean; start: boolean; @@ -1431,6 +1400,26 @@ export class DataSinkType { } } +export class SchemaTransformationRuleDescription extends TransformationRuleDescription { + '@class': + | 'org.apache.streampipes.model.connect.rules.schema.SchemaTransformationRuleDescription' + | 'org.apache.streampipes.model.connect.rules.schema.DeleteRuleDescription' + | 'org.apache.streampipes.model.connect.rules.schema.RenameRuleDescription' + | 'org.apache.streampipes.model.connect.rules.schema.MoveRuleDescription'; + + static 'fromData'( + data: SchemaTransformationRuleDescription, + target?: SchemaTransformationRuleDescription, + ): SchemaTransformationRuleDescription { + if (!data) { + return data; + } + const instance = target || new SchemaTransformationRuleDescription(); + super.fromData(data, instance); + return instance; + } +} + export class DeleteRuleDescription extends SchemaTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.schema.DeleteRuleDescription'; 'runtimeKey': string; @@ -3590,24 +3579,6 @@ export class SpDataStream extends NamedStreamPipesEntity { } } -export class SpDataStreamContainer { - '@class': 'org.apache.streampipes.model.SpDataStreamContainer'; - 'list': SpDataStream[]; - - static 'fromData'( - data: SpDataStreamContainer, - target?: SpDataStreamContainer, - ): SpDataStreamContainer { - if (!data) { - return data; - } - const instance = target || new SpDataStreamContainer(); - instance['@class'] = data['@class']; - instance.list = __getCopyArrayFn(SpDataStream.fromData)(data.list); - return instance; - } -} - export class SpLogEntry { errorMessage: SpLogMessage; timestamp: number; @@ -4156,6 +4127,8 @@ export class WildcardTopicMapping { } } +export type CertificateState = 'REJECTED' | 'TRUSTED'; + export type ConfigurationScope = | 'CONTAINER_STARTUP_CONFIG' | 'CONTAINER_GLOBAL_CONFIG' @@ -4278,7 +4251,6 @@ export type TransformationRuleDescriptionUnion = | UnitTransformRuleDescription | EventRateTransformationRuleDescription | RemoveDuplicatesTransformationRuleDescription - | CreateNestedRuleDescription | DeleteRuleDescription | RenameRuleDescription | RegexTransformationRuleDescription diff --git a/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts index 0f6407cfb5..a8e4d7dbe6 100644 --- a/ui/src/app/configuration/configuration.module.ts +++ b/ui/src/app/configuration/configuration.module.ts @@ -100,6 +100,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; import { GenericStorageItemComponent } from './export/export-dialog/generic-storage-items/generic-storage-item/generic-storage-item.component'; import { GenericStorageItemsComponent } from './export/export-dialog/generic-storage-items/generic-storage-items.component'; import { TranslateModule } from '@ngx-translate/core'; +import { CertificateConfigurationComponent } from './extensions-service-management/certificate-configuration/certificate-configuration.component'; @NgModule({ imports: [ @@ -255,6 +256,7 @@ import { TranslateModule } from '@ngx-translate/core'; PipelineElementNameFilter, PipelineElementInstallationStatusFilter, PipelineElementTypeFilter, + CertificateConfigurationComponent, ], providers: [ OrderByPipe, diff --git a/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.html b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.html new file mode 100644 index 0000000000..a42ecc2968 --- /dev/null +++ b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.html @@ -0,0 +1,73 @@ +<sp-table + *ngIf="dataSource" + data-cy="security-user-config" + [dataSource]="dataSource" + [columns]="displayedColumns" + matSort +> + <ng-container matColumnDef="issuer"> + <th mat-header-cell mat-sort-header *matHeaderCellDef>Issuer</th> + <td + mat-cell + *matCellDef="let certificate" + data-cy="user-accounts-table-row" + > + <b>{{ certificate.issuerDn }}</b> + </td> + </ng-container> + <ng-container matColumnDef="expires"> + <th mat-header-cell mat-sort-header *matHeaderCellDef>Expires</th> + <td + mat-cell + *matCellDef="let certificate" + data-cy="user-accounts-table-row" + > + <b>{{ certificate.notAfter }}</b> + </td> + </ng-container> + <ng-container matColumnDef="actions"> + <th mat-header-cell *matHeaderCellDef>Action</th> + <td + mat-cell + *matCellDef="let certificate" + data-cy="user-accounts-table-row" + > + <div fxLayout="row" fxLayoutGap="10px"> + <button + color="accent" + mat-button + mat-raised-button + (click)=" + certificate.state === 'TRUSTED' + ? onStateChange(certificate, 'REJECTED') + : onStateChange(certificate, 'TRUSTED') + " + data-cy="trust-certificate-button" + > + <i class="material-icons">{{ + certificate.state === 'TRUSTED' ? 'cancel' : 'check' + }}</i> + <span + > {{ + certificate.state === 'TRUSTED' ? 'Reject' : 'Trust' + }}</span + > + </button> + <button + color="warn" + mat-button + mat-raised-button + matTooltip="Delete certificate" + matTooltipPosition="above" + [attr.data-cy]=" + 'certificate-delete-btn-' + certificate.elementId + " + (click)="onDelete(certificate)" + > + <i class="material-icons">delete</i> + <span> Delete</span> + </button> + </div> + </td> + </ng-container> +</sp-table> diff --git a/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.scss b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.ts b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.ts new file mode 100644 index 0000000000..ce2c81fff5 --- /dev/null +++ b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.ts @@ -0,0 +1,46 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { CertificateService } from '../../../../../projects/streampipes/platform-services/src/lib/apis/certificate.service'; +import { Certificate, CertificateState } from '@streampipes/platform-services'; +import { MatTableDataSource } from '@angular/material/table'; + +@Component({ + selector: 'sp-certificate-configuration', + standalone: false, + templateUrl: './certificate-configuration.component.html', + styleUrl: './certificate-configuration.component.scss', +}) +export class CertificateConfigurationComponent implements OnInit { + private certificateService = inject(CertificateService); + + displayedColumns: string[] = ['issuer', 'expires', 'actions']; + dataSource: MatTableDataSource<Certificate> = + new MatTableDataSource<Certificate>(); + + ngOnInit() { + this.loadCertificates(); + } + + loadCertificates() { + this.certificateService.getAllCertificates().subscribe(certs => { + this.dataSource.data = certs; + }); + } + + onStateChange( + certificate: Certificate, + certificateState: CertificateState, + ) { + certificate.state = certificateState; + this.certificateService + .updateCertificate(certificate) + .subscribe(() => this.loadCertificates()); + } + + onDelete(certificate: Certificate) { + this.certificateService + .deleteCertificate(certificate.elementId) + .subscribe(() => { + this.loadCertificates(); + }); + } +} diff --git a/ui/src/app/configuration/extensions-service-management/extensions-service-management.component.html b/ui/src/app/configuration/extensions-service-management/extensions-service-management.component.html index 3958c34611..36ec422513 100644 --- a/ui/src/app/configuration/extensions-service-management/extensions-service-management.component.html +++ b/ui/src/app/configuration/extensions-service-management/extensions-service-management.component.html @@ -34,6 +34,12 @@ > <sp-extensions-service-configuration></sp-extensions-service-configuration> </sp-split-section> + <sp-split-section + title="Certificates" + subtitle="Configure trusted and rejected OPC-UA certificates" + > + <sp-certificate-configuration></sp-certificate-configuration> + </sp-split-section> </div> </div> </sp-basic-nav-tabs> diff --git a/ui/src/app/connect/services/transformation-rule.service.ts b/ui/src/app/connect/services/transformation-rule.service.ts index c8600b1e1a..b274dfc072 100644 --- a/ui/src/app/connect/services/transformation-rule.service.ts +++ b/ui/src/app/connect/services/transformation-rule.service.ts @@ -23,7 +23,6 @@ import { AddValueTransformationRuleDescription, ChangeDatatypeTransformationRuleDescription, CorrectionValueTransformationRuleDescription, - CreateNestedRuleDescription, DeleteRuleDescription, EventProperty, EventPropertyNested, @@ -110,13 +109,6 @@ export class TransformationRuleService { targetSchema, ), ) - .concat( - this.getCreateNestedRules( - targetSchema.eventProperties, - originalSchema, - targetSchema, - ), - ) .concat( this.getMoveRules( targetSchema.eventProperties, @@ -262,36 +254,6 @@ export class TransformationRuleService { return result; } - public getCreateNestedRules( - newEventProperties: EventProperty[], - oldEventSchema: EventSchema, - newEventSchema: EventSchema, - ): CreateNestedRuleDescription[] { - const allNewIds: string[] = this.getAllIds( - newEventSchema.eventProperties, - ); - const allOldIds: string[] = this.getAllIds( - oldEventSchema.eventProperties, - ); - - const result: CreateNestedRuleDescription[] = []; - for (const id of allNewIds) { - if (allOldIds.indexOf(id) === -1) { - const key = this.getCompleteRuntimeNameKey( - newEventSchema.eventProperties, - id, - ); - const rule: CreateNestedRuleDescription = - new CreateNestedRuleDescription(); - rule['@class'] = - 'org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription'; - rule.runtimeKey = key; - result.push(rule); - } - } - return result; - } - public getRenameRules( newEventProperties: EventProperty[], oldEventSchema: EventSchema,
