You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2025/08/20 11:51:39 UTC
(streampipes) branch extend-certificate-infos created (now 0d8e95d5eb)
This is an automated email from the ASF dual-hosted git repository.
riemer pushed a change to branch extend-certificate-infos
in repository https://gitbox.apache.org/repos/asf/streampipes.git
at 0d8e95d5eb feat: Extend certificate information shown in UI
This branch includes the following new commits:
new 0d8e95d5eb feat: Extend certificate information shown in UI
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(streampipes) 01/01: feat: Extend certificate information shown in UI
Posted by ri...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch extend-certificate-infos
in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 0d8e95d5eb43e167f507b07a247290844ac60c40
Author: Dominik Riemer <do...@gmail.com>
AuthorDate: Wed Aug 20 13:51:13 2025 +0200
feat: Extend certificate information shown in UI
---
.../security/CompositeCertificateValidator.java | 16 +-
.../connectors/opcua/utils/OpcUaUtils.java | 16 +-
.../streampipes/model/opcua/Certificate.java | 75 +++++++
.../model/opcua/CertificateBuilder.java | 247 +++++++++++++++++++++
.../src/lib/model/gen/streampipes-model.ts | 39 ++--
.../certificate-details-dialog.component.html | 25 ++-
.../certificate-details-dialog.component.ts | 4 +
7 files changed, 377 insertions(+), 45 deletions(-)
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
index 98dc2f5d73..c88392c729 100644
--- 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
@@ -20,7 +20,7 @@ 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.CertificateBuilder;
import org.apache.streampipes.model.opcua.CertificateState;
import org.eclipse.milo.opcua.stack.client.security.ClientCertificateValidator;
@@ -36,7 +36,6 @@ 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;
@@ -138,18 +137,7 @@ public class CompositeCertificateValidator implements ClientCertificateValidator
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
- );
-
+ var certificate = CertificateBuilder.fromX509(cert, CertificateState.REJECTED);
streamPipesClient.customRequest().sendPost(OpcUaUtils.getCoreCertificatePath(), certificate);
} catch (Exception ex) {
LOG.error("Failed to report rejected certificate to API", ex);
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java
index b5e3b65176..df09ed3050 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
@@ -81,7 +81,6 @@ public class OpcUaUtils {
}
var opcUaConfig = SpOpcUaConfigExtractor.extractSharedConfig(parameterExtractor, new OpcUaAdapterConfig(), client);
-
try {
var connectedClient = clientProvider.getClient(opcUaConfig);
OpcUaNodeBrowser nodeBrowser =
@@ -100,13 +99,14 @@ public class OpcUaUtils {
);
}
-
return config;
} catch (UaException e) {
throw new SpConfigurationException(ExceptionMessageExtractor.getDescription(e), e);
} catch (ExecutionException | InterruptedException | URISyntaxException 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);
+ throw new SpConfigurationException(
+ makeExceptionMessage((ExecutionException) e)
+ );
} else {
throw new SpConfigurationException("Could not connect to the OPC UA server with the provided settings", e);
}
@@ -154,4 +154,14 @@ public class OpcUaUtils {
return false;
}
+ private static String makeExceptionMessage(ExecutionException e) {
+ StringBuilder message = new StringBuilder(
+ "The provided certificate could not be trusted. Administrators can accept this certificate in the settings. "
+ );
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ message.append("Reason: ").append(cause.getMessage());
+ }
+ return message.toString();
+ }
}
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
index df1a65b4b0..85ba67e008 100644
--- 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
@@ -24,6 +24,7 @@ import org.apache.streampipes.model.shared.api.Storable;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.google.gson.annotations.SerializedName;
+import java.util.List;
import java.util.Objects;
@TsModel
@@ -44,9 +45,15 @@ public final class Certificate implements Storable {
private String notAfter;
private String sigAlgName;
private String algorithm;
+ private String basicConstraints;
+ private List<String> keyUsages;
+ private List<String> extendedKeyUsages;
+ private List<String> subjectAlternativeNames;
private String certificateDerBase64;
+
private CertificateState state;
+
public Certificate() {
}
@@ -126,6 +133,74 @@ public final class Certificate implements Storable {
return state;
}
+ public void setSubjectDn(String subjectDn) {
+ this.subjectDn = subjectDn;
+ }
+
+ public void setIssuerDn(String issuerDn) {
+ this.issuerDn = issuerDn;
+ }
+
+ public void setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ }
+
+ public void setNotBefore(String notBefore) {
+ this.notBefore = notBefore;
+ }
+
+ public void setNotAfter(String notAfter) {
+ this.notAfter = notAfter;
+ }
+
+ public void setSigAlgName(String sigAlgName) {
+ this.sigAlgName = sigAlgName;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public String getBasicConstraints() {
+ return basicConstraints;
+ }
+
+ public void setBasicConstraints(String basicConstraints) {
+ this.basicConstraints = basicConstraints;
+ }
+
+ public List<String> getKeyUsages() {
+ return keyUsages;
+ }
+
+ public void setKeyUsages(List<String> keyUsages) {
+ this.keyUsages = keyUsages;
+ }
+
+ public List<String> getExtendedKeyUsages() {
+ return extendedKeyUsages;
+ }
+
+ public void setExtendedKeyUsages(List<String> extendedKeyUsages) {
+ this.extendedKeyUsages = extendedKeyUsages;
+ }
+
+ public List<String> getSubjectAlternativeNames() {
+ return subjectAlternativeNames;
+ }
+
+ public void setSubjectAlternativeNames(List<String> subjectAlternativeNames) {
+ this.subjectAlternativeNames = subjectAlternativeNames;
+ }
+
+ public void setCertificateDerBase64(String certificateDerBase64) {
+ this.certificateDerBase64 = certificateDerBase64;
+ }
+
+ public void setState(CertificateState state) {
+ this.state = state;
+ }
+
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateBuilder.java b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateBuilder.java
new file mode 100644
index 0000000000..716e264157
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateBuilder.java
@@ -0,0 +1,247 @@
+/*
+ * 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 javax.security.auth.x500.X500Principal;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+public final class CertificateBuilder {
+
+ // fluent setters
+ public CertificateBuilder subjectDn(String v) {
+ cert.setSubjectDn(v);
+ return this;
+ }
+
+ public CertificateBuilder issuerDn(String v) {
+ cert.setIssuerDn(v);
+ return this;
+ }
+
+ public CertificateBuilder serialNumber(String v) {
+ cert.setSerialNumber(v);
+ return this;
+ }
+
+ public CertificateBuilder notBefore(String v) {
+ cert.setNotBefore(v);
+ return this;
+ }
+
+ public CertificateBuilder notAfter(String v) {
+ cert.setNotAfter(v);
+ return this;
+ }
+
+ public CertificateBuilder sigAlgName(String v) {
+ cert.setSigAlgName(v);
+ return this;
+ }
+
+ public CertificateBuilder algorithm(String v) {
+ cert.setAlgorithm(v);
+ return this;
+ }
+
+ public CertificateBuilder basicConstraints(String v) {
+ cert.setBasicConstraints(v);
+ return this;
+ }
+
+ public CertificateBuilder keyUsages(List<String> v) {
+ cert.setKeyUsages(v);
+ return this;
+ }
+
+ public CertificateBuilder extendedKeyUsages(List<String> v) {
+ cert.setExtendedKeyUsages(v);
+ return this;
+ }
+
+ public CertificateBuilder subjectAlternativeNames(List<String> v) {
+ cert.setSubjectAlternativeNames(v);
+ return this;
+ }
+
+ public CertificateBuilder certificateDerBase64(String v) {
+ cert.setCertificateDerBase64(v);
+ return this;
+ }
+
+ private final Certificate cert;
+
+ private CertificateBuilder() {
+ cert = new Certificate();
+ }
+
+ public Certificate build() {
+ return cert;
+ }
+
+ public static Certificate fromX509(X509Certificate cert, CertificateState state) {
+ Objects.requireNonNull(cert, "cert");
+ var b = new CertificateBuilder();
+
+ var certificate = b
+ .subjectDn(name(cert.getSubjectX500Principal()))
+ .issuerDn(name(cert.getIssuerX500Principal()))
+ .serialNumber(hex(cert.getSerialNumber()))
+ .notBefore(cert.getNotBefore().toString())
+ .notAfter(cert.getNotAfter().toString())
+ .sigAlgName(cert.getSigAlgName())
+ .algorithm(describePublicKey(cert.getPublicKey()))
+ .basicConstraints(describeBasicConstraints(cert.getBasicConstraints()))
+ .keyUsages(describeKeyUsage(cert.getKeyUsage()))
+ .extendedKeyUsages(describeExtendedKeyUsage(safe(cert::getExtendedKeyUsage)))
+ .subjectAlternativeNames(describeSANs(safe(cert::getSubjectAlternativeNames)))
+ .certificateDerBase64(base64(safe(cert::getEncoded)))
+ .build();
+
+ certificate.setState(state);
+
+ return certificate;
+ }
+
+ private static String name(X500Principal p) {
+ return p == null ? "" : p.getName(X500Principal.RFC2253);
+ }
+
+ private static String hex(BigInteger bi) {
+ return bi == null ? "" : bi.toString(16).toUpperCase(Locale.ROOT);
+ }
+
+ private static String base64(byte[] bytes) {
+ return bytes == null ? "" : Base64.getEncoder().encodeToString(bytes);
+ }
+
+ private static <T> T safe(SupplierWithThrow<T> s) {
+ try {
+ return s.get();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @FunctionalInterface
+ private interface SupplierWithThrow<T> {
+ T get() throws Exception;
+ }
+
+ private static String describePublicKey(PublicKey pk) {
+ if (pk == null) {
+ return "";
+ }
+ if (pk instanceof RSAPublicKey rsa) {
+ return "RSA (" + rsa.getModulus().bitLength() + " bits)";
+ }
+ if (pk instanceof ECPublicKey ec) {
+ return "EC (" + ec.getParams().getCurve().getField().getFieldSize() + " bits)";
+ }
+ if (pk instanceof DSAPublicKey dsa && dsa.getParams() != null) {
+ return "DSA (" + dsa.getParams().getP().bitLength() + " bits)";
+ }
+ return pk.getAlgorithm();
+ }
+
+ private static String describeBasicConstraints(int bc) {
+ if (bc < 0) {
+ return "End-entity (no CA)";
+ }
+ if (bc == Integer.MAX_VALUE) {
+ return "CA: true, pathLen: unlimited";
+ }
+ return "CA: true, pathLen: " + bc;
+ }
+
+ private static List<String> describeKeyUsage(boolean[] ku) {
+ if (ku == null) {
+ return List.of();
+ }
+ String[] names = {"digitalSignature", "contentCommitment", "keyEncipherment", "dataEncipherment",
+ "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"};
+ List<String> out = new ArrayList<>();
+ for (int i = 0; i < ku.length && i < names.length; i++) {
+ if (ku[i]) {
+ out.add(names[i]);
+ }
+ }
+ return out;
+ }
+
+ private static final Map<String, String> EKU_KNOWN = Map.ofEntries(
+ Map.entry("2.5.29.37.0", "anyExtendedKeyUsage"),
+ Map.entry("1.3.6.1.5.5.7.3.1", "serverAuth"),
+ Map.entry("1.3.6.1.5.5.7.3.2", "clientAuth"),
+ Map.entry("1.3.6.1.5.5.7.3.3", "codeSigning"),
+ Map.entry("1.3.6.1.5.5.7.3.4", "emailProtection"),
+ Map.entry("1.3.6.1.5.5.7.3.8", "timeStamping"),
+ Map.entry("1.3.6.1.5.5.7.3.9", "OCSPSigning")
+ );
+
+ private static List<String> describeExtendedKeyUsage(List<String> oids) {
+ if (oids == null) {
+ return List.of();
+ }
+ return oids.stream().map(oid -> EKU_KNOWN.getOrDefault(oid, "OID:" + oid)).toList();
+ }
+
+ private static List<String> describeSANs(Collection<List<?>> sans) {
+ if (sans == null) {
+ return List.of();
+ }
+ List<String> out = new ArrayList<>();
+ for (List<?> entry : sans) {
+ if (entry == null || entry.size() < 2) {
+ continue;
+ }
+ int tag = (Integer) entry.get(0);
+ Object val = entry.get(1);
+ String label = switch (tag) {
+ case 1 -> "rfc822Name";
+ case 2 -> "DNS";
+ case 6 -> "URI";
+ case 7 -> "IP";
+ default -> "SAN(" + tag + ")";
+ };
+ if (tag == 7 && val instanceof byte[] bytes) {
+ try {
+ val = InetAddress.getByAddress(bytes).getHostAddress();
+ } catch (Exception ignored) {
+ }
+ }
+ out.add(label + ": " + val);
+ }
+ return out;
+ }
+}
+
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 3b1c5d8e9a..2d8a85b07a 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -1,26 +1,7 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-07-29 10:59:26.
+// Generated using typescript-generator version 3.2.1263 on 2025-08-20 10:54:16.
export class NamedStreamPipesEntity implements Storable {
'@class':
@@ -672,15 +653,19 @@ export class CanvasPosition {
export class Certificate implements Storable {
algorithm: string;
+ basicConstraints: string;
certificateDerBase64: string;
elementId: string;
+ extendedKeyUsages: string[];
issuerDn: string;
+ keyUsages: string[];
notAfter: string;
notBefore: string;
rev: string;
serialNumber: string;
sigAlgName: string;
state: CertificateState;
+ subjectAlternativeNames: string[];
subjectDn: string;
static fromData(data: Certificate, target?: Certificate): Certificate {
@@ -689,15 +674,25 @@ export class Certificate implements Storable {
}
const instance = target || new Certificate();
instance.algorithm = data.algorithm;
+ instance.basicConstraints = data.basicConstraints;
instance.certificateDerBase64 = data.certificateDerBase64;
instance.elementId = data.elementId;
+ instance.extendedKeyUsages = __getCopyArrayFn(__identity<string>())(
+ data.extendedKeyUsages,
+ );
instance.issuerDn = data.issuerDn;
+ instance.keyUsages = __getCopyArrayFn(__identity<string>())(
+ data.keyUsages,
+ );
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.subjectAlternativeNames = __getCopyArrayFn(
+ __identity<string>(),
+ )(data.subjectAlternativeNames);
instance.subjectDn = data.subjectDn;
return instance;
}
@@ -1189,8 +1184,6 @@ export class DashboardModel implements Storable, SpResource {
export class DataExplorerWidgetModel extends DashboardEntity {
baseAppearanceConfig: { [index: string]: any };
dataConfig: { [index: string]: any };
- measureName: string;
- pipelineId: string;
timeSettings: { [index: string]: any };
visualizationConfig: { [index: string]: any };
widgetId: string;
@@ -1211,8 +1204,6 @@ export class DataExplorerWidgetModel extends DashboardEntity {
instance.dataConfig = __getCopyObjectFn(__identity<any>())(
data.dataConfig,
);
- instance.measureName = data.measureName;
- instance.pipelineId = data.pipelineId;
instance.timeSettings = __getCopyObjectFn(__identity<any>())(
data.timeSettings,
);
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
index dd835d6cf3..44bd4f2ecf 100644
--- 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
@@ -26,10 +26,27 @@
key !== 'certificateDerBase64'
) {
<div fxLayout="row" fxLayoutGap="10px" class="p-5">
- <span fxFlex="30"
- ><b>{{ key }}</b></span
- >
- <span fxFlex="70">{{ certificate[key] }}</span>
+ <div fxFlex="30">
+ <b>{{ key }}</b>
+ </div>
+ <div fxFlex="70">
+ @if (isArray(certificate[key])) {
+ <div fxLayout="row wrap" fxLayoutGap="5px">
+ @for (
+ item of certificate[key];
+ track $index
+ ) {
+ <sp-label
+ size="small"
+ [labelText]="item"
+ class="mb-10"
+ ></sp-label>
+ }
+ </div>
+ } @else {
+ <small>{{ certificate[key] }}</small>
+ }
+ </div>
</div>
}
}
diff --git a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
index 1cf2e9a616..16c6b495d6 100644
--- a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
+++ b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
@@ -31,6 +31,10 @@ export class CertificateDetailsDialogComponent {
@Input()
certificate: Certificate;
+ isArray(value: any): boolean {
+ return Array.isArray(value);
+ }
+
close(): void {
this.dialogRef.close();
}