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 <[email protected]>
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>&nbsp;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"
             >

Reply via email to