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> 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" >
