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/04 12:44:26 UTC
(streampipes) 03/03: Add certificate details dialog, improve exception messages
This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch 3710-centrally-manage-opc-ua-certificates
in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 49a9c2dc2cb228f8ab8f8966e473a79952c141ed
Author: Dominik Riemer <do...@gmail.com>
AuthorDate: Mon Aug 4 14:43:57 2025 +0200
Add certificate details dialog, improve exception messages
---
.../security/CompositeCertificateValidator.java | 18 +++++++-
.../opcua/config/security/SecurityConfig.java | 2 +-
.../connectors/opcua/utils/OpcUaUtils.java | 33 ++++++++++++--
.../connect/RuntimeResolvableResource.java | 2 +-
ui/src/app/configuration/configuration.module.ts | 2 +
.../certificate-details-dialog.component.html | 49 +++++++++++++++++++++
.../certificate-details-dialog.component.ts | 39 +++++++++++++++++
.../certificate-configuration.component.html | 50 ++++++++++++++++++++--
.../certificate-configuration.component.ts | 16 ++++++-
...extensions-service-configuration.component.html | 4 +-
.../registered-extensions-services.component.html | 12 +++---
11 files changed, 209 insertions(+), 18 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 b9d5a1d5fc..98dc2f5d73 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
@@ -24,6 +24,7 @@ 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;
@@ -43,6 +44,15 @@ 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;
@@ -69,7 +79,9 @@ public class CompositeCertificateValidator implements ClientCertificateValidator
trustListManager.getIssuerCertificates()
);
} catch (UaException e) {
- sendToCore(certificateChain.get(0));
+ if (isCertificateRejected(e.getStatusCode().getValue())) {
+ sendToCore(certificateChain.get(0));
+ }
throw e;
}
@@ -143,4 +155,8 @@ public class CompositeCertificateValidator implements ClientCertificateValidator
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 0ff7c0d401..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
@@ -130,7 +130,7 @@ public class SecurityConfig {
private List<X509Certificate> fetchTrustedCertsFromRest() throws SpConfigurationException {
try {
- var response = streamPipesClient.customRequest().getList(OpcUaUtils.getCoreCertificatePath(), Certificate.class);
+ var response = streamPipesClient.customRequest().getList(OpcUaUtils.getCoreTrustedCertificatePath(), Certificate.class);
return response
.stream()
.map(res -> {
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 4f0fbb9b44..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,7 @@ import org.apache.streampipes.extensions.connectors.opcua.client.OpcUaClientProv
import org.apache.streampipes.extensions.connectors.opcua.config.OpcUaAdapterConfig;
import org.apache.streampipes.extensions.connectors.opcua.config.SharedUserConfiguration;
import org.apache.streampipes.extensions.connectors.opcua.config.SpOpcUaConfigExtractor;
+import org.apache.streampipes.extensions.connectors.opcua.config.security.CompositeCertificateValidator;
import org.apache.streampipes.extensions.management.client.StreamPipesClientResolver;
import org.apache.streampipes.model.staticproperty.RuntimeResolvableTreeInputStaticProperty;
@@ -102,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);
}
@@ -125,6 +130,28 @@ public class OpcUaUtils {
}
public static String getCoreCertificatePath() {
- return "/api/v2/admin/certificates/trusted";
+ 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-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/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts
index a8e4d7dbe6..25fe8959aa 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -101,6 +101,7 @@ import { GenericStorageItemComponent } from './export/export-dialog/generic-stor
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: [
@@ -257,6 +258,7 @@ import { CertificateConfigurationComponent } from './extensions-service-manageme
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/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
new file mode 100644
index 0000000000..1cf2e9a616
--- /dev/null
+++ b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
@@ -0,0 +1,39 @@
+/*
+ * 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, Input } from '@angular/core';
+import { DialogRef } from '@streampipes/shared-ui';
+import { Certificate } from '@streampipes/platform-services';
+
+@Component({
+ selector: 'sp-certificate-details-dialog',
+ templateUrl: './certificate-details-dialog.component.html',
+ standalone: false,
+})
+export class CertificateDetailsDialogComponent {
+ dialogRef = inject(DialogRef<CertificateDetailsDialogComponent>);
+
+ @Input()
+ certificate: Certificate;
+
+ close(): void {
+ this.dialogRef.close();
+ }
+
+ 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
index a42ecc2968..5a1182071b 100644
--- 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
@@ -6,8 +6,18 @@
matSort
>
<ng-container matColumnDef="issuer">
- <th mat-header-cell mat-sort-header *matHeaderCellDef>Issuer</th>
+ <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"
@@ -16,8 +26,18 @@
</td>
</ng-container>
<ng-container matColumnDef="expires">
- <th mat-header-cell mat-sort-header *matHeaderCellDef>Expires</th>
+ <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"
@@ -26,13 +46,37 @@
</td>
</ng-container>
<ng-container matColumnDef="actions">
- <th mat-header-cell *matHeaderCellDef>Action</th>
+ <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
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
index e007534430..b96df030be 100644
--- 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
@@ -1,10 +1,12 @@
import { Component, inject, OnInit } from '@angular/core';
import {
Certificate,
- CertificateState,
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',
@@ -14,6 +16,7 @@ import { MatTableDataSource } from '@angular/material/table';
})
export class CertificateConfigurationComponent implements OnInit {
private certificateService = inject(CertificateService);
+ private dialogService = inject(DialogService);
displayedColumns: string[] = ['issuer', 'expires', 'actions'];
dataSource: MatTableDataSource<Certificate> =
@@ -46,4 +49,15 @@ export class CertificateConfigurationComponent implements OnInit {
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/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"
>