This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new fb262c56b1 feat(#3710): Centrally manage OPC-UA certificates (#3720)
fb262c56b1 is described below

commit fb262c56b1115538446b09b5bf2b4e572c6a18f1
Author: Dominik Riemer <[email protected]>
AuthorDate: Mon Aug 11 11:31:54 2025 +0200

    feat(#3710): Centrally manage OPC-UA certificates (#3720)
    
    * feat(#3710): Add initial support for centrally managed OPC-UA certificates
    
    * Modify imports
    
    * Add certificate details dialog, improve exception messages
    
    * Add missing headers
    
    * Fix checkstyle
---
 .../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    | 162 +++++++++++++++++++++
 .../opcua/config/security/SecurityConfig.java      |  51 ++++++-
 .../connectors/opcua/sink/OpcUaSink.java           |   2 +-
 .../connectors/opcua/utils/OpcUaUtils.java         |  39 ++++-
 .../streampipes/model/opcua/Certificate.java       | 142 ++++++++++++++++++
 .../streampipes/model/opcua/CertificateState.java  |  13 +-
 .../connect/RuntimeResolvableResource.java         |   2 +-
 .../rest/impl/admin/CertificateResource.java       | 101 +++++++++++++
 .../streampipes/storage/api/INoSqlStorage.java     |   3 +
 .../storage/couchdb/CouchDbStorageManager.java     |   9 ++
 .../src/lib/apis/certificate.service.ts            |  52 +++++++
 .../src/lib/model/gen/streampipes-model.ts         | 121 +++++++--------
 .../platform-services/src/public-api.ts            |   1 +
 ui/src/app/configuration/configuration.module.ts   |   4 +
 .../certificate-details-dialog.component.html      |  49 +++++++
 .../certificate-details-dialog.component.ts        |  43 ++----
 .../certificate-configuration.component.html       | 135 +++++++++++++++++
 .../certificate-configuration.component.ts         |  80 ++++++++++
 ...extensions-service-configuration.component.html |   4 +-
 .../extensions-service-management.component.html   |   6 +
 .../registered-extensions-services.component.html  |  12 +-
 .../services/transformation-rule.service.ts        |  38 -----
 28 files changed, 957 insertions(+), 168 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..98dc2f5d73
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java
@@ -0,0 +1,162 @@
+/*
+ * 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.StatusCodes;
+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);
+
+  public static final List<Long> REJECTED_STATUS_CODES = List.of(
+      StatusCodes.Bad_CertificateChainIncomplete,
+      StatusCodes.Bad_CertificateInvalid,
+      StatusCodes.Bad_NoValidCertificates,
+      StatusCodes.Bad_CertificateUntrusted,
+      StatusCodes.Bad_CertificateUseNotAllowed,
+      StatusCodes.Bad_SecurityChecksFailed
+  );
+
+  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) {
+      if (isCertificateRejected(e.getStatusCode().getValue())) {
+        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);
+    }
+  }
+
+  private boolean isCertificateRejected(long statusCode) {
+    return REJECTED_STATUS_CODES.contains(statusCode);
+  }
+}
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..d4279162c9 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.getCoreTrustedCertificatePath(),
 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..b5e3b65176 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,8 @@ 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.connectors.opcua.config.security.CompositeCertificateValidator;
+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 +68,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 +80,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);
@@ -100,9 +103,13 @@ public class OpcUaUtils {
 
       return config;
     } catch (UaException e) {
-      throw new 
SpConfigurationException(ExceptionMessageExtractor.getDescription(e), e);
+        throw new 
SpConfigurationException(ExceptionMessageExtractor.getDescription(e), e);
     } catch (ExecutionException | InterruptedException | URISyntaxException e) 
{
-      throw new SpConfigurationException("Could not connect to the OPC UA 
server with the provided settings", e);
+      if (e instanceof ExecutionException && 
isCertificateException((ExecutionException) e)) {
+        throw new SpConfigurationException("The provided certificate could not 
be trusted. Administrators can accept this certificate in the settings.", e);
+      } else {
+        throw new SpConfigurationException("Could not connect to the OPC UA 
server with the provided settings", e);
+      }
     } finally {
       clientProvider.releaseClient(opcUaConfig);
     }
@@ -121,4 +128,30 @@ public class OpcUaUtils {
       }
     }).toList();
   }
+
+  public static String getCoreCertificatePath() {
+    return "/api/v2/admin/certificates";
+  }
+
+  public static String getCoreTrustedCertificatePath() {
+    return getCoreCertificatePath() + "/trusted";
+  }
+
+  private static boolean isCertificateException(ExecutionException e) {
+    Throwable cause = e.getCause();
+
+    if (cause instanceof UaException uaException) {
+      return CompositeCertificateValidator.REJECTED_STATUS_CODES
+          .contains(uaException.getStatusCode().getValue());
+    }
+
+    Throwable nestedCause = cause != null ? cause.getCause() : null;
+    if (nestedCause instanceof UaException uaException) {
+      return CompositeCertificateValidator.REJECTED_STATUS_CODES
+          .contains(uaException.getStatusCode().getValue());
+    }
+
+    return false;
+  }
+
 }
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-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateState.java
similarity index 72%
copy from 
streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java
copy to 
streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateState.java
index 9cbc285e51..100ffc0be7 100644
--- 
a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateState.java
@@ -16,14 +16,9 @@
  *
  */
 
-package org.apache.streampipes.client.api;
+package org.apache.streampipes.model.opcua;
 
-import java.util.Map;
-
-public interface ICustomRequestApi {
-  <T> void sendPost(String apiPath, T payload);
-
-  <T> T sendGet(String apiPath, Class<T> responseClass);
-
-  <T> T sendGet(String apiPath, Map<String, String> queryParameters, Class<T> 
responseClass);
+public enum CertificateState {
+  REJECTED,
+  TRUSTED
 }
diff --git 
a/streampipes-rest-extensions/src/main/java/org/apache/streampipes/rest/extensions/connect/RuntimeResolvableResource.java
 
b/streampipes-rest-extensions/src/main/java/org/apache/streampipes/rest/extensions/connect/RuntimeResolvableResource.java
index 105596a74a..be5726955f 100644
--- 
a/streampipes-rest-extensions/src/main/java/org/apache/streampipes/rest/extensions/connect/RuntimeResolvableResource.java
+++ 
b/streampipes-rest-extensions/src/main/java/org/apache/streampipes/rest/extensions/connect/RuntimeResolvableResource.java
@@ -68,7 +68,7 @@ public class RuntimeResolvableResource extends 
AbstractSharedRestInterface {
             "This element does not support dynamic options - is the pipeline 
element description up to date?");
       }
     } catch (SpConfigurationException e) {
-      LOG.warn("Error when fetching runtime configurations: {}", 
e.getMessage(), e);
+      LOG.warn("Error when fetching runtime configurations: {}", 
e.getMessage());
       return ResponseEntity
           .status(HttpStatus.BAD_REQUEST)
           .body(e);
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..a466b52c20
--- /dev/null
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/CertificateResource.java
@@ -0,0 +1,101 @@
+/*
+ * 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 ec330a4a7e..3f130bbb02 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
@@ -27,6 +27,7 @@ import 
org.apache.streampipes.model.datalake.DataExplorerWidgetModel;
 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 {
@@ -84,4 +85,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 ea54cf6e0a..c669121a05 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;
@@ -239,4 +240,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..fa22fad809
--- /dev/null
+++ 
b/ui/projects/streampipes/platform-services/src/lib/apis/certificate.service.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { Observable } from 'rxjs';
+import { PlatformServicesCommons } from './commons.service';
+import { Certificate } from '../model/gen/streampipes-model';
+
+@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..3b1c5d8e9a 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
@@ -20,7 +20,7 @@
 /* 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 +215,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 +254,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 +670,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 +1020,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 +1419,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 +3598,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 +4146,8 @@ export class WildcardTopicMapping {
     }
 }
 
+export type CertificateState = 'REJECTED' | 'TRUSTED';
+
 export type ConfigurationScope =
     | 'CONTAINER_STARTUP_CONFIG'
     | 'CONTAINER_GLOBAL_CONFIG'
@@ -4278,7 +4270,6 @@ export type TransformationRuleDescriptionUnion =
     | UnitTransformRuleDescription
     | EventRateTransformationRuleDescription
     | RemoveDuplicatesTransformationRuleDescription
-    | CreateNestedRuleDescription
     | DeleteRuleDescription
     | RenameRuleDescription
     | RegexTransformationRuleDescription
diff --git a/ui/projects/streampipes/platform-services/src/public-api.ts 
b/ui/projects/streampipes/platform-services/src/public-api.ts
index d27b0e0b8f..ec91f1a39a 100644
--- a/ui/projects/streampipes/platform-services/src/public-api.ts
+++ b/ui/projects/streampipes/platform-services/src/public-api.ts
@@ -27,6 +27,7 @@ export * from './lib/apis/adapter.service';
 export * from './lib/apis/adapter-monitoring.service';
 export * from './lib/apis/asset-management.service';
 export * from './lib/apis/compact-pipeline.service';
+export * from './lib/apis/certificate.service';
 export * from './lib/apis/chart.service';
 export * from './lib/apis/dashboard.service';
 export * from './lib/apis/datalake-rest.service';
diff --git a/ui/src/app/configuration/configuration.module.ts 
b/ui/src/app/configuration/configuration.module.ts
index 0f6407cfb5..25fe8959aa 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -100,6 +100,8 @@ 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';
+import { CertificateDetailsDialogComponent } from 
'./dialog/certificate-details/certificate-details-dialog.component';
 
 @NgModule({
     imports: [
@@ -255,6 +257,8 @@ import { TranslateModule } from '@ngx-translate/core';
         PipelineElementNameFilter,
         PipelineElementInstallationStatusFilter,
         PipelineElementTypeFilter,
+        CertificateConfigurationComponent,
+        CertificateDetailsDialogComponent,
     ],
     providers: [
         OrderByPipe,
diff --git 
a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
 
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
new file mode 100644
index 0000000000..dd835d6cf3
--- /dev/null
+++ 
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
@@ -0,0 +1,49 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<div class="sp-dialog-container">
+    <div class="sp-dialog-content p-15">
+        <div fxLayout="column">
+            @for (key of Object.keys(certificate); track $index) {
+                @if (
+                    key !== 'elementId' &&
+                    key !== 'rev' &&
+                    key !== 'certificateDerBase64'
+                ) {
+                    <div fxLayout="row" fxLayoutGap="10px" class="p-5">
+                        <span fxFlex="30"
+                            ><b>{{ key }}</b></span
+                        >
+                        <span fxFlex="70">{{ certificate[key] }}</span>
+                    </div>
+                }
+            }
+        </div>
+    </div>
+    <mat-divider></mat-divider>
+    <div class="sp-dialog-actions actions-align-right">
+        <button
+            mat-button
+            mat-raised-button
+            class="mat-basic"
+            (click)="close()"
+        >
+            Close
+        </button>
+    </div>
+</div>
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/SpDataStreamContainer.java
 
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
similarity index 54%
rename from 
streampipes-model/src/main/java/org/apache/streampipes/model/SpDataStreamContainer.java
rename to 
ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
index a59d6fb3a7..1cf2e9a616 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/SpDataStreamContainer.java
+++ 
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
@@ -15,36 +15,25 @@
  * limitations under the License.
  *
  */
-package org.apache.streampipes.model;
 
-import org.apache.streampipes.model.shared.annotation.TsModel;
+import { Component, inject, Input } from '@angular/core';
+import { DialogRef } from '@streampipes/shared-ui';
+import { Certificate } from '@streampipes/platform-services';
 
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
+@Component({
+    selector: 'sp-certificate-details-dialog',
+    templateUrl: './certificate-details-dialog.component.html',
+    standalone: false,
+})
+export class CertificateDetailsDialogComponent {
+    dialogRef = inject(DialogRef<CertificateDetailsDialogComponent>);
 
-import java.util.ArrayList;
-import java.util.List;
+    @Input()
+    certificate: Certificate;
 
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
-@TsModel
-public class SpDataStreamContainer {
+    close(): void {
+        this.dialogRef.close();
+    }
 
-  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;
-  }
+    protected readonly Object = Object;
 }
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..f2131b60ad
--- /dev/null
+++ 
b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.html
@@ -0,0 +1,135 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<sp-table
+    *ngIf="dataSource"
+    data-cy="security-user-config"
+    [dataSource]="dataSource"
+    [columns]="displayedColumns"
+    matSort
+>
+    <ng-container matColumnDef="issuer">
+        <th
+            fxFlex="40"
+            fxLayoutAlign="start center"
+            mat-header-cell
+            mat-sort-header
+            *matHeaderCellDef
+        >
+            Issuer
+        </th>
+        <td
+            fxFlex="40"
+            fxLayoutAlign="start center"
+            mat-cell
+            *matCellDef="let certificate"
+            data-cy="user-accounts-table-row"
+        >
+            <b>{{ certificate.issuerDn }}</b>
+        </td>
+    </ng-container>
+    <ng-container matColumnDef="expires">
+        <th
+            fxFlex
+            fxLayoutAlign="start center"
+            mat-header-cell
+            mat-sort-header
+            *matHeaderCellDef
+        >
+            Expires
+        </th>
+        <td
+            fxFlex
+            fxLayoutAlign="start center"
+            mat-cell
+            *matCellDef="let certificate"
+            data-cy="user-accounts-table-row"
+        >
+            <b>{{ certificate.notAfter }}</b>
+        </td>
+    </ng-container>
+    <ng-container matColumnDef="actions">
+        <th fxFlex fxLayoutAlign="end center" mat-header-cell 
*matHeaderCellDef>
+            <button
+                mat-icon-button
+                color="accent"
+                matTooltip="Refresh"
+                (click)="loadCertificates()"
+            >
+                <mat-icon>refresh</mat-icon>
+            </button>
+        </th>
+        <td
+            fxFlex
+            fxLayoutAlign="end center"
+            mat-cell
+            *matCellDef="let certificate"
+            data-cy="user-accounts-table-row"
+        >
+            <div fxLayout="row" fxLayoutGap="10px">
+                <button
+                    color="accent"
+                    mat-button
+                    matTooltip="Certificate Details"
+                    matTooltipPosition="above"
+                    [attr.data-cy]="
+                        'certificate-details-btn-' + certificate.elementId
+                    "
+                    (click)="openDetailsDialog(certificate)"
+                >
+                    <i class="material-icons">search</i>
+                    <span>&nbsp;Details</span>
+                </button>
+                <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
+                        >&nbsp;{{
+                            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>&nbsp;Delete</span>
+                </button>
+            </div>
+        </td>
+    </ng-container>
+</sp-table>
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..89c908c95a
--- /dev/null
+++ 
b/ui/src/app/configuration/extensions-service-management/certificate-configuration/certificate-configuration.component.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 { Component, inject, OnInit } from '@angular/core';
+import {
+    Certificate,
+    CertificateService,
+    CertificateState,
+} from '@streampipes/platform-services';
+import { MatTableDataSource } from '@angular/material/table';
+import { DialogService, PanelType } from '@streampipes/shared-ui';
+import { CertificateDetailsDialogComponent } from 
'../../dialog/certificate-details/certificate-details-dialog.component';
+
+@Component({
+    selector: 'sp-certificate-configuration',
+    standalone: false,
+    templateUrl: './certificate-configuration.component.html',
+})
+export class CertificateConfigurationComponent implements OnInit {
+    private certificateService = inject(CertificateService);
+    private dialogService = inject(DialogService);
+
+    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();
+            });
+    }
+
+    openDetailsDialog(certificate: Certificate): void {
+        this.dialogService.open(CertificateDetailsDialogComponent, {
+            title: 'Certificate details',
+            panelType: PanelType.STANDARD_PANEL,
+            width: '60vw',
+            data: {
+                certificate,
+            },
+        });
+    }
+}
diff --git 
a/ui/src/app/configuration/extensions-service-management/extensions-service-configuration/extensions-service-configuration.component.html
 
b/ui/src/app/configuration/extensions-service-management/extensions-service-configuration/extensions-service-configuration.component.html
index 984cd5c928..27024f3da6 100644
--- 
a/ui/src/app/configuration/extensions-service-management/extensions-service-configuration/extensions-service-configuration.component.html
+++ 
b/ui/src/app/configuration/extensions-service-management/extensions-service-configuration/extensions-service-configuration.component.html
@@ -64,7 +64,7 @@
         <ng-container matColumnDef="action">
             <th
                 fxFlex="10"
-                fxLayoutAlign="start center"
+                fxLayoutAlign="end center"
                 mat-header-cell
                 *matHeaderCellDef
             >
@@ -79,7 +79,7 @@
             </th>
             <td
                 fxFlex="10"
-                fxLayoutAlign="start center"
+                fxLayoutAlign="end center"
                 mat-cell
                 *matCellDef="let element"
             >
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/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
 
b/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
index a127963627..9def2a35ac 100644
--- 
a/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
+++ 
b/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
@@ -55,7 +55,7 @@
 
         <ng-container matColumnDef="group">
             <th
-                fxFlex="40"
+                fxFlex="45"
                 fxLayoutAlign="start center"
                 mat-header-cell
                 *matHeaderCellDef
@@ -63,7 +63,7 @@
                 Service Group
             </th>
             <td
-                fxFlex="40"
+                fxFlex="45"
                 fxLayoutAlign="start center"
                 mat-cell
                 *matCellDef="let element"
@@ -74,7 +74,7 @@
 
         <ng-container matColumnDef="name">
             <th
-                fxFlex="40"
+                fxFlex="35"
                 fxLayoutAlign="start center"
                 mat-header-cell
                 *matHeaderCellDef
@@ -82,7 +82,7 @@
                 Service ID
             </th>
             <td
-                fxFlex="40"
+                fxFlex="35"
                 fxLayoutAlign="start center"
                 mat-cell
                 *matCellDef="let element"
@@ -94,7 +94,7 @@
         <ng-container matColumnDef="action">
             <th
                 fxFlex="10"
-                fxLayoutAlign="start center"
+                fxLayoutAlign="end center"
                 mat-header-cell
                 *matHeaderCellDef
             >
@@ -109,7 +109,7 @@
             </th>
             <td
                 fxFlex="10"
-                fxLayoutAlign="start center"
+                fxLayoutAlign="end center"
                 mat-cell
                 *matCellDef="let element"
             >
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,

Reply via email to