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> 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
+ > {{
+ 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.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,