This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch 4041-show-opc-ua-certificate-info-in-asset-view in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit b0694cf180ea3527244d4fb32db47b02981c2ed5 Author: Dominik Riemer <[email protected]> AuthorDate: Wed Dec 10 10:12:08 2025 +0100 feat(#4041):Show OPC-UA certificate info in asset view --- .../connectors/opcua/adapter/OpcUaAdapter.java | 3 +- .../opcua/client/OpcUaClientProvider.java | 18 +++- .../config/MiloOpcUaConfigurationProvider.java | 2 +- .../opcua/config/OpcUaAdapterConfig.java | 1 + .../connectors/opcua/config/OpcUaConfig.java | 29 ++++++ .../opcua/config/SpOpcUaConfigExtractor.java | 10 ++ .../security/CompositeCertificateValidator.java | 24 ++++- .../opcua/config/security/SecurityConfig.java | 10 +- .../opcua/utils/OpcUaCertificateUtils.java | 101 +++++++++++++++++++++ .../connectors/opcua/utils/OpcUaUtils.java | 54 +---------- .../streampipes/model/opcua/Certificate.java | 27 +++++- .../model/opcua/CertificateBuilder.java | 10 ++ .../streampipes/model/opcua/CertificateUsage.java | 26 +----- .../streampipes/model/opcua/CertificateUtils.java | 48 ++++++++++ .../management/AdapterResourceManager.java | 19 +++- .../management/AdapterResourceManagerTest.java | 2 +- .../rest/impl/admin/CertificateResource.java | 14 +++ .../core/migrations/AvailableMigrations.java | 4 +- .../ComputeCertificateThumbprintMigration.java | 68 ++++++++++++++ .../src/lib/model/gen/streampipes-model.ts | 25 ++--- ui/src/app/assets/assets.module.ts | 2 + .../asset-details-labels.component.ts | 4 +- .../view-assset-basics.component.ts | 8 +- ...asset-link-table-additional-data.component.html | 29 ++++++ .../asset-link-table-additional-data.component.ts | 76 ++++++++++++++++ .../asset-link-table.component.html | 9 +- .../asset-link-table/asset-link-table.component.ts | 35 ++++++- .../view-asset-links/view-asset-links.component.ts | 4 +- ui/src/app/configuration/configuration.module.ts | 2 - .../certificate-configuration.component.ts | 2 +- .../certificate-details-dialog.component.html | 0 .../certificate-details-dialog.component.ts | 0 ui/src/app/core-ui/core-ui.module.ts | 3 + 33 files changed, 545 insertions(+), 124 deletions(-) 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 c73a8b2926..196392dd77 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 @@ -205,7 +205,8 @@ public class OpcUaAdapter implements StreamPipesAdapter, IPullAdapter, SupportsR this.opcUaAdapterConfig = SpOpcUaConfigExtractor.extractAdapterConfig( extractor.getStaticPropertyExtractor(), - adapterRuntimeContext.getStreamPipesClient() + adapterRuntimeContext.getStreamPipesClient(), + extractor.getAdapterDescription().getElementId() ); this.collector = collector; this.prepareAdapter(extractor); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/client/OpcUaClientProvider.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/client/OpcUaClientProvider.java index b3f907bd4e..e9fe90488f 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/client/OpcUaClientProvider.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/client/OpcUaClientProvider.java @@ -20,13 +20,16 @@ package org.apache.streampipes.extensions.connectors.opcua.client; import org.apache.streampipes.commons.exceptions.SpConfigurationException; import org.apache.streampipes.extensions.connectors.opcua.config.OpcUaConfig; +import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaCertificateUtils; import org.eclipse.milo.opcua.stack.core.UaException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -36,6 +39,7 @@ public class OpcUaClientProvider { private final Map<String, ConnectedOpcUaClient> clients = new ConcurrentHashMap<>(); private final Map<String, Integer> consumers = new ConcurrentHashMap<>(); + private final Map<String, String> serverThumbprints = new HashMap<>(); public synchronized <T extends OpcUaConfig> ConnectedOpcUaClient getClient(T config) throws UaException, SpConfigurationException, URISyntaxException, ExecutionException, InterruptedException { @@ -43,12 +47,24 @@ public class OpcUaClientProvider { if (clients.containsKey(serverId)) { LOG.debug("Adding new consumer to client {}", serverId); consumers.put(serverId, consumers.get(config.getUniqueServerId()) + 1); - return clients.get(serverId); + var client = clients.get(serverId); + var associatedResourceId = config.getAssociatedResourceId(); + if (serverThumbprints.containsKey(serverId) && Objects.nonNull(associatedResourceId)) { + OpcUaCertificateUtils.sendUsageToCore( + serverThumbprints.get(serverId), + associatedResourceId, + config.getStreamPipesClient() + ); + } + return client; } else { LOG.debug("Creating new client {}", serverId); var connectedClient = new SpOpcUaClient<>(config).connect(); clients.put(serverId, connectedClient); consumers.put(serverId, 1); + if (config.getCertificateThumbprint() != null) { + serverThumbprints.put(serverId, config.getCertificateThumbprint()); + } return connectedClient; } } diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/MiloOpcUaConfigurationProvider.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/MiloOpcUaConfigurationProvider.java index 8db41a8e11..d1f3564fb9 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/MiloOpcUaConfigurationProvider.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/MiloOpcUaConfigurationProvider.java @@ -42,7 +42,7 @@ public class MiloOpcUaConfigurationProvider { .setApplicationName(LocalizedText.english("Apache StreamPipes")) .setApplicationUri(applicationUri); - spOpcConfig.getSecurityConfig().configureSecurityPolicy(opcServerUrl, endpoints, builder); + spOpcConfig.getSecurityConfig().configureSecurityPolicy(spOpcConfig, endpoints, builder); spOpcConfig.getIdentityConfig().configureIdentity(builder); return builder.build(); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaAdapterConfig.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaAdapterConfig.java index 83cf333b7d..a0bac48efd 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaAdapterConfig.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaAdapterConfig.java @@ -53,4 +53,5 @@ public class OpcUaAdapterConfig extends OpcUaConfig { public void setNamingStrategy(OpcUaNamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } + } diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaConfig.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaConfig.java index 128f1bc1f9..aec8e37342 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaConfig.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/OpcUaConfig.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.connectors.opcua.config.identity.IdentityConfig; import org.apache.streampipes.extensions.connectors.opcua.config.security.SecurityConfig; @@ -29,6 +30,9 @@ public class OpcUaConfig { private List<String> selectedNodeNames; private IdentityConfig identityConfig; private SecurityConfig securityPolicyConfig; + private String associatedResourceId; + private String certificateThumbprint; + private IStreamPipesClient streamPipesClient; public OpcUaConfig() { @@ -69,4 +73,29 @@ public class OpcUaConfig { public String getUniqueServerId() { return String.format("%s-%s-%s", opcServerURL, securityPolicyConfig, identityConfig); } + + public String getAssociatedResourceId() { + return associatedResourceId; + } + + public void setAssociatedResourceId(String associatedResourceId) { + this.associatedResourceId = associatedResourceId; + } + + public String getCertificateThumbprint() { + return certificateThumbprint; + } + + public void setCertificateThumbprint(String certificateThumbprint) { + this.certificateThumbprint = certificateThumbprint; + } + + public IStreamPipesClient getStreamPipesClient() { + return streamPipesClient; + } + + public void setStreamPipesClient(IStreamPipesClient streamPipesClient) { + this.streamPipesClient = streamPipesClient; + } + } 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 e6fea5c2d6..9363339bd8 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 @@ -49,6 +49,15 @@ import static org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaLabe public class SpOpcUaConfigExtractor { + + public static OpcUaAdapterConfig extractAdapterConfig(IStaticPropertyExtractor extractor, + IStreamPipesClient streamPipesClient, + String adapterId) { + var config = extractAdapterConfig(extractor, streamPipesClient); + config.setAssociatedResourceId(adapterId); + return config; + } + /** * Creates {@link OpcUaAdapterConfig} instance in accordance with the given * {@link org.apache.streampipes.sdk.extractor.StaticPropertyExtractor}. @@ -116,6 +125,7 @@ public class SpOpcUaConfigExtractor { streamPipesClient ) ); + config.setStreamPipesClient(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 index f5c391ffa7..b228c8be5e 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 @@ -19,9 +19,11 @@ 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.extensions.connectors.opcua.config.OpcUaConfig; +import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaCertificateUtils; import org.apache.streampipes.model.opcua.CertificateBuilder; import org.apache.streampipes.model.opcua.CertificateState; +import org.apache.streampipes.model.opcua.CertificateUtils; import org.eclipse.milo.opcua.stack.client.security.ClientCertificateValidator; import org.eclipse.milo.opcua.stack.core.StatusCodes; @@ -32,6 +34,8 @@ import org.eclipse.milo.opcua.stack.core.util.validation.ValidationCheck; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; import java.security.cert.PKIXCertPathBuilderResult; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; @@ -52,15 +56,18 @@ public class CompositeCertificateValidator implements ClientCertificateValidator StatusCodes.Bad_SecurityChecksFailed ); + private final OpcUaConfig opcUaConfig; private final TrustListManager trustListManager; private final List<X509Certificate> trustedCerts; private final List<ValidationCheck> validationChecks; private final IStreamPipesClient streamPipesClient; - public CompositeCertificateValidator(TrustListManager trustListManager, + public CompositeCertificateValidator(OpcUaConfig opcUaConfig, + TrustListManager trustListManager, List<X509Certificate> trustedCerts, List<ValidationCheck> validationChecks, IStreamPipesClient streamPipesClient) { + this.opcUaConfig = opcUaConfig; this.trustListManager = trustListManager; this.trustedCerts = trustedCerts; this.validationChecks = validationChecks; @@ -96,6 +103,16 @@ public class CompositeCertificateValidator implements ClientCertificateValidator ValidationCheck.NO_OPTIONAL_CHECKS, false ); + + if (opcUaConfig.getAssociatedResourceId() != null) { + try { + var thumbprint = CertificateUtils.getThumbprint(peer); + opcUaConfig.setCertificateThumbprint(thumbprint); + OpcUaCertificateUtils.sendUsageToCore(thumbprint, opcUaConfig.getAssociatedResourceId(), streamPipesClient); + } catch (NoSuchAlgorithmException | CertificateEncodingException e) { + LOG.warn("Error sending certificate to opcUa", e); + } + } } private X509Certificate getEndEntity(List<X509Certificate> chain) { @@ -146,7 +163,8 @@ public class CompositeCertificateValidator implements ClientCertificateValidator private void sendToCore(X509Certificate cert) { try { var certificate = CertificateBuilder.fromX509(cert, CertificateState.REJECTED); - streamPipesClient.customRequest().sendPost(OpcUaUtils.getCoreCertificatePath(), certificate); + opcUaConfig.setCertificateThumbprint(certificate.getThumbprint()); + streamPipesClient.customRequest().sendPost(OpcUaCertificateUtils.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/config/security/SecurityConfig.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/SecurityConfig.java index d4279162c9..87e624e22d 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 @@ -21,7 +21,8 @@ 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.extensions.connectors.opcua.config.OpcUaConfig; +import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaCertificateUtils; import org.apache.streampipes.model.opcua.Certificate; import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder; @@ -56,11 +57,11 @@ public class SecurityConfig { this.streamPipesClient = streamPipesClient; } - public void configureSecurityPolicy(String opcServerUrl, + public void configureSecurityPolicy(OpcUaConfig config, List<EndpointDescription> endpoints, OpcUaClientConfigBuilder builder) throws SpConfigurationException, URISyntaxException { - String host = opcServerUrl.split("://")[1].split(":")[0]; + String host = config.getOpcServerURL().split("://")[1].split(":")[0]; EndpointDescription tmpEndpoint = endpoints .stream() @@ -87,6 +88,7 @@ public class SecurityConfig { var loadedCerts = new AtomicReference<>(fetchTrustedCertsFromRest()); var compositeValidator = new CompositeCertificateValidator( + config, trustListManager, loadedCerts.get(), List.of(), @@ -130,7 +132,7 @@ public class SecurityConfig { private List<X509Certificate> fetchTrustedCertsFromRest() throws SpConfigurationException { try { - var response = streamPipesClient.customRequest().getList(OpcUaUtils.getCoreTrustedCertificatePath(), Certificate.class); + var response = streamPipesClient.customRequest().getList(OpcUaCertificateUtils.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/OpcUaCertificateUtils.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaCertificateUtils.java new file mode 100644 index 0000000000..ef4769bb7b --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaCertificateUtils.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.extensions.connectors.opcua.utils; + +import org.apache.streampipes.client.api.IStreamPipesClient; +import org.apache.streampipes.extensions.connectors.opcua.config.security.CompositeCertificateValidator; +import org.apache.streampipes.model.opcua.CertificateUsage; + +import org.eclipse.milo.opcua.stack.core.UaException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutionException; + +public class OpcUaCertificateUtils { + + private static final Logger LOG = LoggerFactory.getLogger(OpcUaCertificateUtils.class); + + public static String getCoreCertificatePath() { + return "/api/v2/admin/certificates"; + } + + public static String getCoreCertificateUsagePath() { + return getCoreCertificatePath() + "/usage"; + } + + public static String getCoreTrustedCertificatePath() { + return getCoreCertificatePath() + "/trusted"; + } + + public static boolean isCertificateException(ExecutionException e) { + Throwable cause = e.getCause(); + + if (cause instanceof UaException uaException) { + return checkAndLogCertificateException(uaException); + } + + Throwable nestedCause = cause != null ? cause.getCause() : null; + if (nestedCause instanceof UaException uaException) { + return checkAndLogCertificateException(uaException); + } + + return false; + } + + private static boolean checkAndLogCertificateException(UaException e) { + var containsRejectedStatusCode = CompositeCertificateValidator.REJECTED_STATUS_CODES + .contains(e.getStatusCode().getValue()); + + if (containsRejectedStatusCode) { + var statusCode = CompositeCertificateValidator.REJECTED_STATUS_CODES.stream().filter(code -> code.equals(e.getStatusCode().getValue())).findFirst(); + statusCode.ifPresent(sc -> LOG.warn("Status Code: {}", sc)); + } + return containsRejectedStatusCode; + } + + public 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(); + } + + public static void sendUsageToCore(String thumbprint, + String associatedResourceId, + IStreamPipesClient streamPipesClient) { + try { + var usage = new CertificateUsage( + associatedResourceId, + thumbprint + ); + + streamPipesClient + .customRequest() + .sendPost(OpcUaCertificateUtils.getCoreCertificateUsagePath(), usage); + + } catch (Exception ex) { + LOG.error("Failed to report certificate usage 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 5debd21414..d446168817 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,7 +26,6 @@ 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; @@ -34,8 +33,6 @@ import org.eclipse.milo.opcua.sdk.client.api.UaClient; import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.net.URISyntaxException; import java.util.List; @@ -47,8 +44,6 @@ import java.util.concurrent.ExecutionException; */ public class OpcUaUtils { - private static final Logger LOG = LoggerFactory.getLogger(OpcUaUtils.class); - private static final String OPC_TCP_PREFIX = "opc.tcp://"; /*** @@ -107,9 +102,9 @@ public class OpcUaUtils { } catch (UaException e) { throw new SpConfigurationException(ExceptionMessageExtractor.getDescription(e), e); } catch (ExecutionException | InterruptedException | URISyntaxException e) { - if (e instanceof ExecutionException && isCertificateException((ExecutionException) e)) { + if (e instanceof ExecutionException && OpcUaCertificateUtils.isCertificateException((ExecutionException) e)) { throw new SpConfigurationException( - makeExceptionMessage((ExecutionException) e) + OpcUaCertificateUtils.makeExceptionMessage((ExecutionException) e) ); } else { throw new SpConfigurationException("Could not connect to the OPC UA server with the provided settings", e); @@ -132,49 +127,4 @@ 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 checkAndLogCertificateException(uaException); - } - - Throwable nestedCause = cause != null ? cause.getCause() : null; - if (nestedCause instanceof UaException uaException) { - return checkAndLogCertificateException(uaException); - } - - return false; - } - - private static boolean checkAndLogCertificateException(UaException e) { - var containsRejectedStatusCode = CompositeCertificateValidator.REJECTED_STATUS_CODES - .contains(e.getStatusCode().getValue()); - - if (containsRejectedStatusCode) { - var statusCode = CompositeCertificateValidator.REJECTED_STATUS_CODES.stream().filter(code -> code.equals(e.getStatusCode().getValue())).findFirst(); - statusCode.ifPresent(sc -> LOG.warn("Status Code: {}", sc)); - } - return containsRejectedStatusCode; - } - - 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 85ba67e008..93c6ba1b07 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,8 +24,10 @@ import org.apache.streampipes.model.shared.api.Storable; import com.fasterxml.jackson.annotation.JsonAlias; import com.google.gson.annotations.SerializedName; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; @TsModel public final class Certificate implements Storable { @@ -50,11 +52,14 @@ public final class Certificate implements Storable { private List<String> extendedKeyUsages; private List<String> subjectAlternativeNames; private String certificateDerBase64; + private String thumbprint; + private Set<String> associatedResourceIds; private CertificateState state; public Certificate() { + this.associatedResourceIds = new HashSet<>(); } public Certificate(String subjectDn, @@ -201,13 +206,33 @@ public final class Certificate implements Storable { this.state = state; } + public String getThumbprint() { + return thumbprint; + } + + public void setThumbprint(String thumbprint) { + this.thumbprint = thumbprint; + } + + public Set<String> getAssociatedResourceIds() { + return associatedResourceIds; + } + + public void setAssociatedResourceIds(Set<String> associatedResourceIds) { + this.associatedResourceIds = associatedResourceIds; + } + @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 [...] + if (getThumbprint() != null && that.getThumbprint() != null) { + return getThumbprint().equals(that.getThumbprint()); + } else { + return getCertificateDerBase64().equals(that.getCertificateDerBase64()); + } } @Override 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 index 716e264157..191ff612dc 100644 --- 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 @@ -18,6 +18,9 @@ package org.apache.streampipes.model.opcua; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javax.security.auth.x500.X500Principal; import java.math.BigInteger; @@ -37,6 +40,8 @@ import java.util.Objects; public final class CertificateBuilder { + private static final Logger LOG = LoggerFactory.getLogger(CertificateBuilder.class); + // fluent setters public CertificateBuilder subjectDn(String v) { cert.setSubjectDn(v); @@ -128,6 +133,11 @@ public final class CertificateBuilder { .build(); certificate.setState(state); + try { + certificate.setThumbprint(CertificateUtils.getThumbprint(cert)); + } catch (Exception e) { + LOG.warn("Could not create thumbprint for certificate: {}", e.getMessage()); + } return certificate; } diff --git a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateUsage.java similarity index 54% copy from ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts copy to streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateUsage.java index 16c6b495d6..3b43786a58 100644 --- a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateUsage.java @@ -16,28 +16,8 @@ * */ -import { Component, inject, Input } from '@angular/core'; -import { DialogRef } from '@streampipes/shared-ui'; -import { Certificate } from '@streampipes/platform-services'; +package org.apache.streampipes.model.opcua; -@Component({ - selector: 'sp-certificate-details-dialog', - templateUrl: './certificate-details-dialog.component.html', - standalone: false, -}) -export class CertificateDetailsDialogComponent { - dialogRef = inject(DialogRef<CertificateDetailsDialogComponent>); - - @Input() - certificate: Certificate; - - isArray(value: any): boolean { - return Array.isArray(value); - } - - close(): void { - this.dialogRef.close(); - } - - protected readonly Object = Object; +public record CertificateUsage(String associatedResourceId, + String thumbprint) { } diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateUtils.java b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateUtils.java new file mode 100644 index 0000000000..2ca092e2b8 --- /dev/null +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateUtils.java @@ -0,0 +1,48 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.HexFormat; + +public class CertificateUtils { + + public static String getThumbprint(X509Certificate certificate) throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(certificate.getEncoded()); + return HexFormat.of().withUpperCase().formatHex(digest); + } + + public static String getThumbprint(String certificateDerBase64) throws CertificateException, NoSuchAlgorithmException { + byte[] derBytes = Base64.getDecoder().decode(certificateDerBase64); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate( + new ByteArrayInputStream(derBytes)); + + return getThumbprint(cert); + } +} diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AdapterResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AdapterResourceManager.java index 8e3d4d4a85..f9f99d4a9d 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AdapterResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AdapterResourceManager.java @@ -19,20 +19,27 @@ package org.apache.streampipes.resource.management; import org.apache.streampipes.commons.exceptions.connect.AdapterException; import org.apache.streampipes.model.connect.adapter.AdapterDescription; +import org.apache.streampipes.model.opcua.Certificate; import org.apache.streampipes.model.util.Cloner; import org.apache.streampipes.resource.management.secret.SecretProvider; +import org.apache.streampipes.storage.api.CRUDStorage; import org.apache.streampipes.storage.api.IAdapterStorage; import org.apache.streampipes.storage.management.StorageDispatcher; public class AdapterResourceManager extends AbstractResourceManager<IAdapterStorage> { - public AdapterResourceManager(IAdapterStorage adapterStorage) { + private final CRUDStorage<Certificate> certificateStorage; + + public AdapterResourceManager(IAdapterStorage adapterStorage, + CRUDStorage<Certificate> certificateStorage) { super(adapterStorage); + this.certificateStorage = certificateStorage; } public AdapterResourceManager() { super(StorageDispatcher.INSTANCE.getNoSqlStore() .getAdapterInstanceStorage()); + this.certificateStorage = StorageDispatcher.INSTANCE.getNoSqlStore().getCertificateStorage(); } /** @@ -69,6 +76,16 @@ public class AdapterResourceManager extends AbstractResourceManager<IAdapterStor } public void delete(String elementId) { + // Remove references from certificates + certificateStorage + .findAll() + .stream().filter(c -> c.getAssociatedResourceIds().contains(elementId)) + .forEach(c -> { + c.getAssociatedResourceIds().remove(elementId); + certificateStorage.updateElement(c); + }); + + // Then delete the adapter db.deleteElementById(elementId); } diff --git a/streampipes-resource-management/src/test/java/org/apache/streampipes/resource/management/AdapterResourceManagerTest.java b/streampipes-resource-management/src/test/java/org/apache/streampipes/resource/management/AdapterResourceManagerTest.java index d4616713da..7d9bfcda6a 100644 --- a/streampipes-resource-management/src/test/java/org/apache/streampipes/resource/management/AdapterResourceManagerTest.java +++ b/streampipes-resource-management/src/test/java/org/apache/streampipes/resource/management/AdapterResourceManagerTest.java @@ -43,7 +43,7 @@ public class AdapterResourceManagerTest { @BeforeEach void setUp() { storage = mock(IAdapterStorage.class); - adapterResourceManager = new AdapterResourceManager(storage); + adapterResourceManager = new AdapterResourceManager(storage, null); } @Test 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 index a466b52c20..d39c172afc 100644 --- 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 @@ -20,6 +20,7 @@ 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.model.opcua.CertificateUsage; import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; import org.apache.streampipes.rest.security.AuthConstants; import org.apache.streampipes.storage.api.CRUDStorage; @@ -39,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Objects; @RestController @PreAuthorize(AuthConstants.IS_ADMIN_ROLE) @@ -87,7 +89,19 @@ public class CertificateResource extends AbstractAuthGuardedRestResource { } else { LOG.info("Certificate with IssuerDN {} already exists, skipping creation", certificate.getIssuerDn()); } + } + @PostMapping(value = "usage", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public void updateUsage(@RequestBody CertificateUsage certificateUsage) { + var certificates = certificateStorage.findAll(); + certificates + .stream() + .filter(c -> Objects.nonNull(c.getThumbprint()) && c.getThumbprint().equals(certificateUsage.thumbprint())) + .findFirst() + .ifPresent(c -> { + c.getAssociatedResourceIds().add(certificateUsage.associatedResourceId()); + certificateStorage.updateElement(c); + }); } @DeleteMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_VALUE) diff --git a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java index c1f72c466c..0969bf8f8e 100644 --- a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java +++ b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java @@ -31,6 +31,7 @@ import org.apache.streampipes.service.core.migrations.v0980.FixImportedPermissio import org.apache.streampipes.service.core.migrations.v0980.ModifyAssetLinkTypesMigration; import org.apache.streampipes.service.core.migrations.v0980.ModifyAssetLinksMigration; import org.apache.streampipes.service.core.migrations.v099.AddAssetManagementViewMigration; +import org.apache.streampipes.service.core.migrations.v099.ComputeCertificateThumbprintMigration; import org.apache.streampipes.service.core.migrations.v099.CreateAssetPermissionMigration; import org.apache.streampipes.service.core.migrations.v099.MoveAssetContentMigration; import org.apache.streampipes.service.core.migrations.v099.RemoveObsoletePrivilegesMigration; @@ -70,7 +71,8 @@ public class AvailableMigrations { new MoveAssetContentMigration(), new CreateAssetPermissionMigration(), new RemoveObsoletePrivilegesMigration(), - new UniqueDashboardIdMigration() + new UniqueDashboardIdMigration(), + new ComputeCertificateThumbprintMigration() ); } } diff --git a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/ComputeCertificateThumbprintMigration.java b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/ComputeCertificateThumbprintMigration.java new file mode 100644 index 0000000000..625c843eb7 --- /dev/null +++ b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/ComputeCertificateThumbprintMigration.java @@ -0,0 +1,68 @@ +/* + * 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.service.core.migrations.v099; + +import org.apache.streampipes.model.opcua.Certificate; +import org.apache.streampipes.model.opcua.CertificateUtils; +import org.apache.streampipes.service.core.migrations.Migration; +import org.apache.streampipes.storage.api.CRUDStorage; +import org.apache.streampipes.storage.management.StorageDispatcher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class ComputeCertificateThumbprintMigration implements Migration { + + private static final Logger LOG = LoggerFactory.getLogger(ComputeCertificateThumbprintMigration.class); + + private CRUDStorage<Certificate> certificateStorage = + StorageDispatcher.INSTANCE.getNoSqlStore().getCertificateStorage(); + + @Override + public boolean shouldExecute() { + return true; + } + + @Override + public void executeMigration() throws IOException { + this.certificateStorage + .findAll() + .stream() + .filter(certificate -> certificate.getThumbprint() == null) + .forEach(certificate -> { + try { + certificate.setThumbprint(CertificateUtils.getThumbprint(certificate.getCertificateDerBase64())); + certificateStorage.updateElement(certificate); + } catch (CertificateException e) { + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + LOG.warn("Could not compute thumbprint for existing certificate: {}", e.getMessage()); + } + }); + } + + @Override + public String getDescription() { + return "Adding thumbprint to existing 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 edb971862f..8d6dc8bd5e 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-12-04 17:41:18. +// Generated using typescript-generator version 3.2.1263 on 2025-12-10 08:38:24. export class NamedStreamPipesEntity implements Storable { '@class': @@ -180,23 +180,6 @@ export class AdapterEventPreview { } } -export class AdapterType { - code: string; - description: string; - label: string; - - static fromData(data: AdapterType, target?: AdapterType): AdapterType { - if (!data) { - return data; - } - const instance = target || new AdapterType(); - instance.code = data.code; - instance.description = data.description; - instance.label = data.label; - return instance; - } -} - export class TransformationRuleDescription { '@class': | 'org.apache.streampipes.model.connect.rules.value.ValueTransformationRuleDescription' @@ -749,6 +732,7 @@ export class CanvasPosition { export class Certificate implements Storable { algorithm: string; + associatedResourceIds: string[]; basicConstraints: string; certificateDerBase64: string; elementId: string; @@ -763,6 +747,7 @@ export class Certificate implements Storable { state: CertificateState; subjectAlternativeNames: string[]; subjectDn: string; + thumbprint: string; static fromData(data: Certificate, target?: Certificate): Certificate { if (!data) { @@ -770,6 +755,9 @@ export class Certificate implements Storable { } const instance = target || new Certificate(); instance.algorithm = data.algorithm; + instance.associatedResourceIds = __getCopyArrayFn(__identity<string>())( + data.associatedResourceIds, + ); instance.basicConstraints = data.basicConstraints; instance.certificateDerBase64 = data.certificateDerBase64; instance.elementId = data.elementId; @@ -790,6 +778,7 @@ export class Certificate implements Storable { __identity<string>(), )(data.subjectAlternativeNames); instance.subjectDn = data.subjectDn; + instance.thumbprint = data.thumbprint; return instance; } } diff --git a/ui/src/app/assets/assets.module.ts b/ui/src/app/assets/assets.module.ts index d0ac72f1b6..13bd455aa4 100644 --- a/ui/src/app/assets/assets.module.ts +++ b/ui/src/app/assets/assets.module.ts @@ -68,6 +68,7 @@ import { SpAssetSelectionMenuComponent } from './components/asset-details/edit-a import { AssetLinkTableComponent } from './components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component'; import { AssetLinkTableTypeComponent } from './components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-link-type/asset-link-table-type.component'; import { AssetDetailsCustomFieldsComponent } from './components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-custom-fields/asset-details-custom-fields.component'; +import { AssetLinkTableAdditionalDataComponent } from './components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component'; @NgModule({ imports: [ @@ -151,6 +152,7 @@ import { AssetDetailsCustomFieldsComponent } from './components/asset-details/ed AssetLinkTableComponent, AssetLinkTableTypeComponent, AssetDetailsCustomFieldsComponent, + AssetLinkTableAdditionalDataComponent, ], providers: [], }) diff --git a/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts b/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts index d803cb774e..2fada3c2f8 100644 --- a/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts +++ b/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts @@ -33,9 +33,9 @@ import { import { MatChipInputEvent } from '@angular/material/chips'; import { FormControl } from '@angular/forms'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; -import { filter, map, startWith } from 'rxjs/operators'; +import { map, startWith } from 'rxjs/operators'; import { SpColorizationService } from '@streampipes/shared-ui'; @Component({ diff --git a/ui/src/app/assets/components/asset-details/view-asset/view-asset-basics/view-assset-basics.component.ts b/ui/src/app/assets/components/asset-details/view-asset/view-asset-basics/view-assset-basics.component.ts index 52e7ccc2d4..8a3ae225fa 100644 --- a/ui/src/app/assets/components/asset-details/view-asset/view-asset-basics/view-assset-basics.component.ts +++ b/ui/src/app/assets/components/asset-details/view-asset/view-asset-basics/view-assset-basics.component.ts @@ -16,13 +16,7 @@ * */ -import { - Component, - Input, - OnChanges, - OnInit, - SimpleChanges, -} from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { AssetSiteDesc, Isa95TypeService, diff --git a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component.html b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component.html new file mode 100644 index 0000000000..4cad5a6116 --- /dev/null +++ b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component.html @@ -0,0 +1,29 @@ +<!-- + ~ 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. + ~ + --> + +@if (showCertificateInfo) { + <div> + <button + mat-icon-button + (click)="openCertificateDetailsDialog()" + [disabled]="matchingCertificates.length === 0" + > + <mat-icon>approval</mat-icon> + </button> + </div> +} diff --git a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component.ts b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component.ts new file mode 100644 index 0000000000..1a8fb20318 --- /dev/null +++ b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table-additional-data/asset-link-table-additional-data.component.ts @@ -0,0 +1,76 @@ +/* + * 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, + OnChanges, + SimpleChanges, +} from '@angular/core'; +import { AssetLink, Certificate } from '@streampipes/platform-services'; +import { CertificateDetailsDialogComponent } from '../../../../../../../core-ui/certificate-details/certificate-details-dialog.component'; +import { DialogService, PanelType } from '@streampipes/shared-ui'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'sp-asset-link-table-additional-data', + templateUrl: './asset-link-table-additional-data.component.html', + standalone: false, +}) +export class AssetLinkTableAdditionalDataComponent implements OnChanges { + @Input() + assetLink: AssetLink; + + @Input() + certificates: Certificate[] = []; + + @Input() + isAdminUser = false; + + showCertificateInfo = false; + matchingCertificates: Certificate[] = []; + + private dialogService = inject(DialogService); + private translateService = inject(TranslateService); + + ngOnChanges(changes: SimpleChanges) { + this.findAssociatedCertificates(); + } + + findAssociatedCertificates() { + this.matchingCertificates = this.certificates.filter( + c => + c.associatedResourceIds.find( + resourceId => resourceId === this.assetLink.resourceId, + ) !== undefined, + ); + this.showCertificateInfo = this.matchingCertificates.length > 0; + } + + openCertificateDetailsDialog(): void { + this.dialogService.open(CertificateDetailsDialogComponent, { + title: this.translateService.instant('Certificate details'), + panelType: PanelType.STANDARD_PANEL, + width: '60vw', + data: { + certificate: this.matchingCertificates[0], + }, + }); + } +} diff --git a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.html b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.html index b3f47f1197..2b71fa7ce9 100644 --- a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.html +++ b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.html @@ -66,7 +66,14 @@ </ng-container> <ng-container matColumnDef="additionalInfo"> <th mat-header-cell *matHeaderCellDef>Info</th> - <td mat-cell *matCellDef="let assetLink"></td> + <td mat-cell *matCellDef="let assetLink"> + <sp-asset-link-table-additional-data + [isAdminUser]="isAdminUser" + [assetLink]="assetLink" + [certificates]="certificates" + > + </sp-asset-link-table-additional-data> + </td> </ng-container> <ng-template spTableActions let-element> diff --git a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.ts b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.ts index 07e455c6f5..5e3c06c98c 100644 --- a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.ts +++ b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/asset-link-table/asset-link-table.component.ts @@ -22,6 +22,7 @@ import { inject, Input, OnChanges, + OnDestroy, OnInit, SimpleChanges, ViewChild, @@ -29,15 +30,24 @@ import { import { AssetLink, AssetLinkType, + Certificate, + CertificateService, SpAsset, SpAssetModel, } from '@streampipes/platform-services'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { EditAssetLinkDialogComponent } from '../../../../../dialog/edit-asset-link/edit-asset-link-dialog.component'; -import { DialogService, PanelType } from '@streampipes/shared-ui'; +import { + CurrentUserService, + DialogService, + PanelType, +} from '@streampipes/shared-ui'; import { TranslateService } from '@ngx-translate/core'; import { Router } from '@angular/router'; +import { AuthService } from '../../../../../../services/auth.service'; +import { UserRole } from '../../../../../../_enums/user-role.enum'; +import { Subscription } from 'rxjs'; @Component({ selector: 'sp-asset-link-table', @@ -45,7 +55,7 @@ import { Router } from '@angular/router'; standalone: false, }) export class AssetLinkTableComponent - implements OnInit, AfterViewInit, OnChanges + implements OnInit, AfterViewInit, OnChanges, OnDestroy { @Input() assetModel: SpAssetModel; @@ -70,12 +80,29 @@ export class AssetLinkTableComponent ]; dataSource: MatTableDataSource<AssetLink> = new MatTableDataSource(); + isAdminUser = false; + certificates: Certificate[] = []; + + user$: Subscription; private dialogService = inject(DialogService); private translateService = inject(TranslateService); private router = inject(Router); + private currentUserService = inject(CurrentUserService); + private authService = inject(AuthService); + private certificateService = inject(CertificateService); ngOnInit() { + this.user$ = this.currentUserService.user$.subscribe(user => { + this.isAdminUser = this.authService.hasRole(UserRole.ROLE_ADMIN); + if (this.isAdminUser) { + this.certificateService + .getAllCertificates() + .subscribe(certificates => { + this.certificates = certificates; + }); + } + }); this.dataSource.sortingDataAccessor = (link, column) => { if (column === 'type') { return link.linkType; @@ -141,4 +168,8 @@ export class AssetLinkTableComponent this.asset.assetLinks = [...this.asset.assetLinks]; this.refreshData(); } + + ngOnDestroy() { + this.user$?.unsubscribe(); + } } diff --git a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/view-asset-links.component.ts b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/view-asset-links.component.ts index 97a4bf99a0..1cf6d31c1a 100644 --- a/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/view-asset-links.component.ts +++ b/ui/src/app/assets/components/asset-details/view-asset/view-asset-links/view-asset-links.component.ts @@ -16,7 +16,7 @@ * */ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, inject, Input, OnInit } from '@angular/core'; import { AssetConstants, AssetLinkType, @@ -36,7 +36,7 @@ export class ViewAssetLinksComponent implements OnInit { assetLinkTypes: AssetLinkType[] = []; - constructor(private genericStorageService: GenericStorageService) {} + private genericStorageService = inject(GenericStorageService); ngOnInit() { this.genericStorageService diff --git a/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts index 31069ba465..05f5dd7137 100644 --- a/ui/src/app/configuration/configuration.module.ts +++ b/ui/src/app/configuration/configuration.module.ts @@ -98,7 +98,6 @@ 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'; import { AlternateIdConfigurationComponent } from './security-configuration/alternate-id-configuration/alternate-id-configuration.component'; import { UserAcknowledgmentComponent } from './general-configuration/user-acknowledgement/user-acknowledgment.component'; import { QuillEditorComponent } from 'ngx-quill'; @@ -255,7 +254,6 @@ import { CertificateLabelComponent } from './extensions-service-management/certi PipelineElementInstallationStatusFilter, PipelineElementTypeFilter, CertificateConfigurationComponent, - CertificateDetailsDialogComponent, AlternateIdConfigurationComponent, UserAcknowledgmentComponent, CertificateLabelComponent, 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 8ad8b35405..5458282869 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 @@ -24,7 +24,7 @@ import { } 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'; +import { CertificateDetailsDialogComponent } from '../../../core-ui/certificate-details/certificate-details-dialog.component'; import { TranslateService } from '@ngx-translate/core'; @Component({ diff --git a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html b/ui/src/app/core-ui/certificate-details/certificate-details-dialog.component.html similarity index 100% rename from ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html rename to ui/src/app/core-ui/certificate-details/certificate-details-dialog.component.html diff --git a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts b/ui/src/app/core-ui/certificate-details/certificate-details-dialog.component.ts similarity index 100% rename from ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts rename to ui/src/app/core-ui/certificate-details/certificate-details-dialog.component.ts diff --git a/ui/src/app/core-ui/core-ui.module.ts b/ui/src/app/core-ui/core-ui.module.ts index 1bec1335d0..b55ea56a43 100644 --- a/ui/src/app/core-ui/core-ui.module.ts +++ b/ui/src/app/core-ui/core-ui.module.ts @@ -107,6 +107,7 @@ import { YamlPrettyPrintPipe } from './pipes/yaml-pretty-print.pipe'; import { TopicsComponent } from './topics/topics.component'; import { TranslateModule } from '@ngx-translate/core'; import { TextFieldModule } from '@angular/cdk/text-field'; +import { CertificateDetailsDialogComponent } from './certificate-details/certificate-details-dialog.component'; @NgModule({ imports: [ @@ -203,6 +204,7 @@ import { TextFieldModule } from '@angular/cdk/text-field'; PipelineOperationStatusComponent, JsonPrettyPrintPipe, YamlPrettyPrintPipe, + CertificateDetailsDialogComponent, ], providers: [MatDatepickerModule, DisplayRecommendedPipe], exports: [ @@ -237,6 +239,7 @@ import { TextFieldModule } from '@angular/cdk/text-field'; SingleMarkerMapComponent, JsonPrettyPrintPipe, YamlPrettyPrintPipe, + CertificateDetailsDialogComponent, ], }) export class CoreUiModule {}
