diff --git a/.ort.yml b/.ort.yml index deff8f065a..c6e70b0869 100644 --- a/.ort.yml +++ b/.ort.yml @@ -181,6 +181,12 @@ resolutions: - message: "property:advertising-clause license LicenseRef-scancode-rsa-md4 in Gradle:ee.ria.xroad:src:1.0." reason: "LICENSE_ACQUIRED_EXCEPTION" comment: "The LicenseRef-scancode-rsa-md4 in src/libs/iaikPkcs11Wrapper.AUTHORS is taken into account, and therefore the license conditions are satisfied." + - message: "property:advertising-clause license LicenseRef-scancode-rsa-md4 in Gradle:org.niis.xroad:x-road-core:1.0." + reason: "LICENSE_ACQUIRED_EXCEPTION" + comment: "The LicenseRef-scancode-rsa-md4 in src/libs/iaikPkcs11Wrapper.AUTHORS is taken into account, and therefore the license conditions are satisfied." + - message: "property:advertising-clause license LicenseRef-scancode-rsa-md4 in Unmanaged::X-Road:.*" + reason: "LICENSE_ACQUIRED_EXCEPTION" + comment: "The LicenseRef-scancode-rsa-md4 in src/libs/iaikPkcs11Wrapper.AUTHORS is taken into account, and therefore the license conditions are satisfied." - message: "commercial license LicenseRef-scancode-proprietary-license in Maven:org.apache.commons:commons-compress:1.26.*" reason: "LICENSE_ACQUIRED_EXCEPTION" comment: "This PKWare technology is not in use, therefore license is sufficient." @@ -196,6 +202,9 @@ resolutions: - message: "proprietary-free license LicenseRef-verbatim-no-modifications in Maven:org.hsqldb:hsqldb:2.7.*" reason: "NOT_MODIFIED_EXCEPTION" comment: "The license represented by LicenseRef-verbatim-no-modifications allows redistributing without modifications. As long as the files licensed with the said license are redistributed without modifications, the condition is satisfied." + - message: "copyleft-strong license CC-BY-SA-3.0 in Unmanaged::X-Road:.*" + reason: "LICENSE_ACQUIRED_EXCEPTION" + comment: "The files meant by this license hit are not distributed with X-Road." license_choices: repository_license_choices: diff --git a/ansible/roles/xroad-ca/files/home/ca/CA/sign_req.sh b/ansible/roles/xroad-ca/files/home/ca/CA/sign_req.sh index 223332537a..101dc6d3c1 100644 --- a/ansible/roles/xroad-ca/files/home/ca/CA/sign_req.sh +++ b/ansible/roles/xroad-ca/files/home/ca/CA/sign_req.sh @@ -32,7 +32,36 @@ trap 'status=$?; rm -rf "lock"; exit $status' INT TERM EXIT set -e SER=$(cat serial) openssl req -in $2 -inform $INFORM -out csr/${SER}.csr -openssl ca -batch -config CA.cnf -extensions $EXT -days 7300 -notext -md sha256 -in csr/${SER}.csr + +function opensslCA() { + openssl ca -batch -config CA.cnf \ + -extensions $EXT \ + -days 7300 \ + -notext \ + -md sha256 \ + -in csr/${SER}.csr \ + "$@" +} + +if [ "$1" == "auth" ]; then + subjectAltName=$(openssl req -in csr/${SER}.csr -text -noout | grep -A1 "Subject Alternative Name" | tail -n1 | sed 's/^[ \t]*//') + if [ ! -z "$subjectAltName" ]; then + extensionsOverride=" +[ auth_ext ] +basicConstraints = CA:FALSE +keyUsage = critical, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement +extendedKeyUsage = clientAuth, serverAuth +subjectAltName = ${subjectAltName} +" + fi +fi + +if [ ! -z "${extensionsOverride}" ]; then + opensslCA -extfile <(echo "$extensionsOverride") +else + opensslCA +fi + chmod 0664 index.txt chmod 0664 serial echo $SER>changed diff --git a/doc/Manuals/ug-ss_x-road_6_security_server_user_guide.md b/doc/Manuals/ug-ss_x-road_6_security_server_user_guide.md index 5e836f9038..9050dd2e6c 100644 --- a/doc/Manuals/ug-ss_x-road_6_security_server_user_guide.md +++ b/doc/Manuals/ug-ss_x-road_6_security_server_user_guide.md @@ -2,7 +2,7 @@ **X-ROAD 7** -Version: 2.101 +Version: 2.102 Doc. ID: UG-SS --- @@ -130,6 +130,7 @@ Doc. ID: UG-SS | 18.06.2025 | 2.99 | ACME-related updates | Petteri Kivimäki | | 01.07.2025 | 2.100 | Added configuration notes for external op-monitor's gRPC | Mikk-Erik Bachmann | | 07.07.2025 | 2.101 | Added chapter on Security Server Traffic visualisation | Madis Loitmaa | +| 01.12.2025 | 2.102 | Added chapter on Security Server Connection Testing | Eneli Reimets | ## Table of Contents @@ -247,9 +248,12 @@ Doc. ID: UG-SS - [14.1 Diagnostics Overview](#141-diagnostics-overview) - [14.1.1 Examine Security Server services status information](#1411-examine-security-server-services-status-information) - [14.1.2 Examine Security Server Java version information](#1412-examine-security-server-java-version-information) - - [14.3.3 Examine Security Server encryption status information](#1433-examine-security-server-encryption-status-information) + - [14.1.3 Examine Security Server encryption status information](#1413-examine-security-server-encryption-status-information) - [14.1.4 Download diagnostics report](#1414-download-diagnostics-report) - [14.2 Security Server Traffic](#142-security-server-traffic) + - [14.3 Security Server Connection Testing](#143-security-server-connection-testing) + - [14.3.1 Testing the connection to the Central Server](#1431-testing-the-connection-to-the-central-server) + - [14.3.2 Testing the connection to other Security Servers](#1432-testing-the-connection-to-other-security-servers) - [15 Operational Monitoring](#15-operational-monitoring) - [15.1 Operational Monitoring Buffer](#151-operational-monitoring-buffer) - [15.1.1 Stopping the Collecting of Operational Data](#1511-stopping-the-collecting-of-operational-data) @@ -2498,6 +2502,7 @@ Click on **DIAGNOSTICS** in the **Navigation tabs**. Diangostics view contains the following tabs: - **Overview** – overview of the Security Server status information - **Traffic** – visual overview of the Security Server traffic +- **Connection Testing** – test connectivity to the Central Server and other Security Servers ### 14.1 Diagnostics Overview @@ -2553,7 +2558,7 @@ The status colors indicate the following: - **Red indicator** – Security Server's java version number isn't supported - **Green indicator** – Security Server's java version number is supported -#### 14.3.3 Examine Security Server encryption status information +#### 14.1.3 Examine Security Server encryption status information **Backup encryption status** @@ -2616,6 +2621,72 @@ By default, the page displays all the requests handled during the last 7 days. T - **Exchange role** - the role of this Security Server in the message exchange. The options are "Producer" and "Consumer". - **Status** - the status of the message exchange. The options are "Success" and "Failure". +### 14.3 Security Server Connection Testing + +The "Connection Testing" tab in the Diagnostics page allows testing connectivity from the Security Server to the Central Server and other Security Servers. + +The page is divided into three logical blocks: +- Central Server +- Other Security Server +- Management Security Server + +Each block contains predefined tests that validate communication with the corresponding service. Test results include a status indicator ("Green" or "Red") and a detailed message to assist troubleshooting. + +A **Test** button next to each row allows re-running the specific connection test. + +## 14.3.1 Testing the connection to the Central Server + +This block allows verifying that the Security Server can reach the Central Server and download the configuration necessary for normal operation. + +**Global Configuration Download** + +Tests ports `80` and `443` to verify that the Global Configuration can be downloaded from the Central Server. If the Central Server is clustered, then all clustered node addresses are included in the test. For federated instances, if the `configuration-client.allowed-federations` property is enabled, the configuration download URLs for the allowed federated instances are also included. Note that even if the global configuration contains multiple federated instances, not all of them may be enabled on the Security Server. + +✔ `Everything ok` — indicates that the Central Server global configuration access via `HTTP`/`HTTPS` on ports `80`/`443` is reachable. + +Examples of error messages: +- `Connection error, unknown host - cs: Name or service not known` — the Central Server hostname cannot be resolved. Check DNS configuration. +- `IO error - (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException...` — the Security Server doesn't trust the CA that issued Central Server's TLS certificate. The root certificate of the CA that was used to issue Central Server's TLS certificate must be added to the Security Server's Java truststore. For the guidelines on Publish global configuration over HTTPS, please refer to [UG-SEC](ug-sec_x_road_security_hardening.md). + +**Authentication Certificate Registration Service** + +Tests connectivity to the Central Server on port `4001` (used by the registration service and must be accessible by every Security Server registered to the ecosystem). + +✔ `Everything ok` — indicates that the Authentication Certificate Registration Service is reachable and that the Security Server’s authentication certificate has been added. However, only the existence of the authentication certificate is checked, not its validity. + +Examples of error messages: +- `Connection error, unknown host - cs: Name or service not known | Certificate not found - No auth cert found` — the Central Server hostname cannot be resolved, and the Security Server has no authentication certificate added. +- `Certificate not found - No auth cert found` — there are no connection issues, but the Security Server's authentication certificate has not been added. + +## 14.3.2 Testing the connection to other Security Servers + +This block enables testing communication with any other Security Server in the same X-Road instance (or federated instances). The functionality uses the `listMethods` meta service to test communication with other Security Servers. Passing the test requires that the target Security Server allows incoming connections to ports `5500` and `5577` from the source Security Server. + +Field descriptions: +- **Source Client** — a list of members and subsystems registered on the client Security Server that can be used as a Source Client. +- **REST/SOAP** - the protocol (`REST` or `SOAP`) that's used to complete the connection test. +- **Target Instance** - the X-Road instance where the Target Client is registered. This can be the same instance where the Source Client is registered or a federated instance. +- **Target Client** - a list of clients registered on other Security Servers. Also, clients registered on the same Security Server with the Source Client are included to allow local testing. If federation is enabled and federated instances exist in the configuration, registered clients of federated instances are included as well. +- **Target Security Server** — a list of Security Servers where the Target Client is registered. If the Target Client is registered on multiple Security Servers, all of them are listed for selection. + +✔ `Everything ok` — indicates that there are no network, configuration, or certificate issues preventing communication between the two Security Server client. + +Examples of error messages: +- `server.clientproxy.ssl_authentication_failed - Security server has no valid authentication certificate`. + +## 14.3.3 Testing the connection to Management Security Server + +This block tests communication with the Management Security Server, including capability to send management requests (such as client register, client disable, ...). + +Field descriptions: +- **Source Client** - the owner member of the client Security Server. +- **REST/SOAP** - `SOAP` since management services only support `SOAP`. +- **Target Instance** - the same instance where the Source Client is registered. +- **Target Client** - the subsystem providing the management services. +- **Target Security Server** - if management services are registered on multiple Security Servers, the user is able to select the desired target Security Server. + +✔ `Everything ok` - indicates that there are no network, configuration, or certificate issues preventing communication with the management Security Server. + ## 15 Operational Monitoring **Operational monitoring data** contains data about request exchange (such as the ID-s of the client and the service, various attributes of the message read from the message header, request and response timestamps, SOAP sizes etc.) of the X-Road Security Server(s). diff --git a/src/addons/op-monitoring/src/main/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessor.java b/src/addons/op-monitoring/src/main/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessor.java index 09cf88843d..51f983bc8f 100644 --- a/src/addons/op-monitoring/src/main/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessor.java +++ b/src/addons/op-monitoring/src/main/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessor.java @@ -34,21 +34,25 @@ import org.niis.xroad.opmonitor.api.OpMonitoringData; import org.niis.xroad.opmonitor.api.StoreOpMonitoringDataRequest; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; +import java.time.Duration; +import java.time.Instant; import java.util.List; -import static java.net.NetworkInterface.getNetworkInterfaces; -import static java.util.Collections.list; +import static java.net.NetworkInterface.networkInterfaces; @Slf4j public class OpMonitoringDataProcessor { - private static final ObjectWriter OBJECT_WRITER = JsonUtils.getObjectWriter(); + private static final String NO_ADDRESS_FOUND = "No suitable IP address is bound to network interfaces"; - private static final String NO_ADDRESS_FOUND = "No suitable IP address is bound to the network interface "; - private static final String NO_INTERFACE_FOUND = "No non-loopback network interface found"; + private static final ObjectWriter OBJECT_WRITER = JsonUtils.getObjectWriter(); + private static final Duration IP_RESOLUTION_CACHE_DURATION = Duration.ofMinutes(10); private String ipAddress; + private Instant ipAddressLastResolutionAt; String prepareMonitoringMessage(List dataToProcess) throws JsonProcessingException { StoreOpMonitoringDataRequest request = new StoreOpMonitoringDataRequest(); @@ -62,30 +66,31 @@ String prepareMonitoringMessage(List dataToProcess) throws Jso String getIpAddress() { try { - if (ipAddress == null) { - NetworkInterface ni = list(getNetworkInterfaces()).stream() - .filter(OpMonitoringDataProcessor::isNonLoopback) - .findFirst() - .orElseThrow(() -> XrdRuntimeException.systemInternalError(NO_INTERFACE_FOUND)); - - Exception addressNotFound = XrdRuntimeException.systemInternalError(NO_ADDRESS_FOUND + ni.getDisplayName()); - - ipAddress = list(ni.getInetAddresses()).stream() - .filter(addr -> !addr.isLinkLocalAddress()) - .findFirst() - .orElseThrow(() -> addressNotFound) - .getHostAddress(); - - if (ipAddress == null) { - throw addressNotFound; - } + if (ipAddress != null && ipAddressLastResolutionAt != null + && !ipAddressLastResolutionAt.isBefore(Instant.now().minus(IP_RESOLUTION_CACHE_DURATION))) { + return ipAddress; } + ipAddress = networkInterfaces() + .filter(OpMonitoringDataProcessor::isNonLoopback) + .filter(OpMonitoringDataProcessor::hasAnyUsableAddress) + .min(OpMonitoringDataProcessor::compareNetworkInterfaces) + .stream() + .flatMap(NetworkInterface::inetAddresses) + .filter(OpMonitoringDataProcessor::isUsableAddress) + .min(OpMonitoringDataProcessor::compareInetAddresses) + .map(InetAddress::getHostAddress) + .orElseThrow(OpMonitoringDataProcessor::addressNotFoundException); + + ipAddressLastResolutionAt = Instant.now(); + return ipAddress; } catch (Exception e) { log.error("Cannot get IP address of a non-loopback network interface", e); - - return "0.0.0.0"; + // keep the failed resolution cached for resolution cache duration as well. + ipAddressLastResolutionAt = Instant.now(); + ipAddress = "0.0.0.0"; + return ipAddress; } } @@ -96,4 +101,27 @@ private static boolean isNonLoopback(NetworkInterface ni) { throw XrdRuntimeException.systemException(e); } } + + private static boolean hasAnyUsableAddress(NetworkInterface ni) { + return ni.inetAddresses().anyMatch(OpMonitoringDataProcessor::isUsableAddress); + } + + private static int compareNetworkInterfaces(NetworkInterface ni1, NetworkInterface ni2) { + return Integer.compare(ni1.getIndex(), ni2.getIndex()); + } + + private static boolean isUsableAddress(InetAddress addr) { + return !addr.isLoopbackAddress() + && !addr.isLinkLocalAddress() + && addr.getHostAddress() != null; + } + + private static int compareInetAddresses(InetAddress a1, InetAddress a2) { + // prefer IPv4 addresses + return Boolean.compare(a1 instanceof Inet6Address, a2 instanceof Inet6Address); + } + + private static XrdRuntimeException addressNotFoundException() { + return XrdRuntimeException.systemInternalError(NO_ADDRESS_FOUND); + } } diff --git a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilter.java b/src/addons/op-monitoring/src/test/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessorTest.java similarity index 70% rename from src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilter.java rename to src/addons/op-monitoring/src/test/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessorTest.java index 7f24adff35..90dabde785 100644 --- a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilter.java +++ b/src/addons/op-monitoring/src/test/java/org/niis/xroad/proxy/core/opmonitoring/OpMonitoringDataProcessorTest.java @@ -23,17 +23,21 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.niis.xroad.confclient.core; +package org.niis.xroad.proxy.core.opmonitoring; -/** - * A small interface for deciding if the additional configuration for a given X-Road instance should be downloaded. - * Not downloading the configuration prevents federation with that instance for this security server. - */ -public interface FederationConfigurationSourceFilter { +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +@Slf4j +public class OpMonitoringDataProcessorTest { + @Test + public void verifyIpAddressIsResolved() { + OpMonitoringDataProcessor proc = new OpMonitoringDataProcessor(); + + String ip = proc.getIpAddress(); - /** - * @param instanceIdentifier the instance identifier of an existing federation partner - * @return true if the configuration should be downloaded, false otherwise - */ - boolean shouldDownloadConfigurationFor(String instanceIdentifier); + assertNotNull(ip); + } } diff --git a/src/common/common-core/src/main/java/ee/ria/xroad/common/util/UriUtils.java b/src/common/common-core/src/main/java/ee/ria/xroad/common/util/UriUtils.java index 91a8263b8e..9860fcf8f2 100644 --- a/src/common/common-core/src/main/java/ee/ria/xroad/common/util/UriUtils.java +++ b/src/common/common-core/src/main/java/ee/ria/xroad/common/util/UriUtils.java @@ -25,6 +25,9 @@ */ package ee.ria.xroad.common.util; +import jakarta.annotation.Nonnull; + +import java.net.URI; import java.nio.charset.StandardCharsets; /** @@ -35,6 +38,16 @@ public final class UriUtils { private UriUtils() { } + /** + * Percent-decodes and normalizes a URI path, assuming UTF-8 character set. + * + * @see #uriPathPercentDecode(String, boolean) + */ + public static String decodeAndNormalize(@Nonnull final String path) { + String decoded = uriPathPercentDecode(path, true); + return URI.create(decoded).normalize().getPath(); + } + /** * Percent-decodes a URI segment to a string, assuming UTF-8 character set. * The allowed chars in a URI segment are defined as follows: @@ -45,14 +58,16 @@ private UriUtils() { * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" * pct-encoded = "%" HEXDIG HEXDIG * + * * @see RFC 3986 */ - public static String uriSegmentPercentDecode(final String src) { + public static String uriSegmentPercentDecode(@Nonnull final String src) { return uriPathPercentDecode(src, false); } /** * Percent-decodes a URI path, assuming UTF-8 character set; optionally allows a path separator ('/'). + * * @param src URI path to percent-decode * @param allowSeparator If true, path separators are allowed and any %2d ('/') escape sequence is preserved * (normalized to %2D) so that it is possible to distinguish literal '/' from an encoded one. @@ -60,7 +75,7 @@ public static String uriSegmentPercentDecode(final String src) { * @see #uriSegmentPercentDecode(String) */ @SuppressWarnings({"squid:S3776", "checkstyle:magicnumber"}) - public static String uriPathPercentDecode(final String src, final boolean allowSeparator) { + public static String uriPathPercentDecode(@Nonnull final String src, final boolean allowSeparator) { final int length = src.length(); if (length == 0) { return src; diff --git a/src/common/common-core/src/test/java/ee/ria/xroad/common/util/UriUtilsTest.java b/src/common/common-core/src/test/java/ee/ria/xroad/common/util/UriUtilsTest.java index 6d8f70f44c..8412668269 100644 --- a/src/common/common-core/src/test/java/ee/ria/xroad/common/util/UriUtilsTest.java +++ b/src/common/common-core/src/test/java/ee/ria/xroad/common/util/UriUtilsTest.java @@ -87,4 +87,18 @@ public void shouldKeepPathSeparator() { public void shouldFailIfPathSeparatorPresent() { uriPathPercentDecode("zy%2dggy/", false); } + + @Test + public void shouldSuccessfullyNormalizeTraversalPaths() { + assertEquals("/a/b/c", UriUtils.decodeAndNormalize("/a/b/c")); + assertEquals("/a/b/c", UriUtils.decodeAndNormalize("/a/b/./c")); + assertEquals("/a/b/c", UriUtils.decodeAndNormalize("/a/b/d/../c")); + assertEquals("/c", UriUtils.decodeAndNormalize("/a/b/../../c")); + //Encoded variants + assertEquals("/a/b/c", UriUtils.decodeAndNormalize("/a/b/%2e/c")); + assertEquals("/a/b/c", UriUtils.decodeAndNormalize("/a/b/d/%2e%2e/c")); + assertEquals("/c", UriUtils.decodeAndNormalize("/a/b/%2e%2e/%2e%2e/c")); + assertEquals("/../c/d", UriUtils.decodeAndNormalize("/a/b/../../%2e%2e/c/d")); + } + } diff --git a/src/common/common-int-test/src/main/java/org/niis/xroad/common/test/ui/utils/VuetifyHelper.java b/src/common/common-int-test/src/main/java/org/niis/xroad/common/test/ui/utils/VuetifyHelper.java index a35aeb5987..39daefef7a 100644 --- a/src/common/common-int-test/src/main/java/org/niis/xroad/common/test/ui/utils/VuetifyHelper.java +++ b/src/common/common-int-test/src/main/java/org/niis/xroad/common/test/ui/utils/VuetifyHelper.java @@ -68,7 +68,6 @@ public static Select vSelect(final SelenideElement vuetifySelectField) { return new Select(vuetifySelectField); } - public static SelenideElement selectorOptionOf(String value) { var xpath = "//div[@role='listbox']//div[contains(@class, 'v-list-item') and contains(./descendant-or-self::*/text(),'%s')]"; return $x(format(xpath, value)); @@ -244,6 +243,10 @@ public void select(final String val) { selectorOptionOf(val).click(); } + public void selectCombobox(final String val) { + selectorComboboxOf(val).click(); + } + public void clickAndSelect(final String val) { click().select(val); } diff --git a/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/EmptyGlobalConf.java b/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/EmptyGlobalConf.java index 78d5412fc9..e3d7da5c6b 100644 --- a/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/EmptyGlobalConf.java +++ b/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/EmptyGlobalConf.java @@ -185,10 +185,20 @@ public X509Certificate getCentralServerSslCertificate() { } @Override - public Set findSourceAddresses() { + public Set getSourceAddresses(String instanceIdentifier) { return Set.of(); } + @Override + public Set getAllowedFederationInstances() { + return Set.of(); + } + + @Override + public String getConfigurationDirectoryPath(String instanceIdentifier) { + return ""; + } + @Override public boolean isSecurityServerClient(ClientId client, SecurityServerId securityServer) { diff --git a/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/TestGlobalConfWrapper.java b/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/TestGlobalConfWrapper.java index 8f338a31ff..d32c53d347 100644 --- a/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/TestGlobalConfWrapper.java +++ b/src/common/common-test/src/main/java/org/niis/xroad/test/globalconf/TestGlobalConfWrapper.java @@ -282,10 +282,20 @@ public X509Certificate getCentralServerSslCertificate() { } @Override - public Set findSourceAddresses() { + public Set getSourceAddresses(String instanceIdentifier) { return Set.of(); } + @Override + public Set getAllowedFederationInstances() { + return Set.of(); + } + + @Override + public String getConfigurationDirectoryPath(String instanceIdentifier) { + return ""; + } + @Override public int getOcspFreshnessSeconds() { return globalConfProvider.getOcspFreshnessSeconds(); diff --git a/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/BasicACMECertificateProfileInfoProvider.java b/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/BasicACMECertificateProfileInfoProvider.java new file mode 100644 index 0000000000..b27a10e5ee --- /dev/null +++ b/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/BasicACMECertificateProfileInfoProvider.java @@ -0,0 +1,164 @@ +/* + * The MIT License + * Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS) + * Copyright (c) 2018 Estonian Information System Authority (RIA), + * Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK) + * Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package ee.ria.xroad.common.certificateprofile.impl; + +import ee.ria.xroad.common.certificateprofile.AuthCertificateProfileInfo; +import ee.ria.xroad.common.certificateprofile.CertificateProfileInfoProvider; +import ee.ria.xroad.common.certificateprofile.DnFieldDescription; +import ee.ria.xroad.common.certificateprofile.SignCertificateProfileInfo; +import ee.ria.xroad.common.identifier.ClientId; +import ee.ria.xroad.common.util.CertUtils; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.niis.xroad.common.core.exception.ErrorCode; +import org.niis.xroad.common.core.exception.XrdRuntimeException; + +import javax.security.auth.x500.X500Principal; + +import java.security.cert.X509Certificate; + +/** + * Basic ACME certificate profile + */ +public class BasicACMECertificateProfileInfoProvider + implements CertificateProfileInfoProvider { + + @Override + public AuthCertificateProfileInfo getAuthCertProfile(AuthCertificateProfileInfo.Parameters params) { + return new BasicACMEAuthCertificateProfileInfo(params); + } + + @Override + public SignCertificateProfileInfo getSignCertProfile(SignCertificateProfileInfo.Parameters params) { + return new BasicACMESignCertificateProfileInfo(params); + } + + /** + * Auth cert + *

+ * C = country + * O = member name + * serialNumber = member code + * CN = server code + * subjectAltName = server DNS (mapped to subject alternative name in certificate) + */ + private static class BasicACMEAuthCertificateProfileInfo extends AbstractCertificateProfileInfo + implements AuthCertificateProfileInfo { + + BasicACMEAuthCertificateProfileInfo(Parameters params) { + super(new DnFieldDescription[] { + // Country Code + new EnumLocalizedFieldDescriptionImpl("C", DnFieldLabelLocalizationKey.COUNTRY_CODE, "") + .setReadOnly(false), + + // Organization name + new EnumLocalizedFieldDescriptionImpl("O", DnFieldLabelLocalizationKey.ORGANIZATION_NAME, params.getMemberName()) + .setReadOnly(true), + + // Serial number + new EnumLocalizedFieldDescriptionImpl("serialNumber", DnFieldLabelLocalizationKey.MEMBER_CODE_SN, + params.getServerId().getMemberCode()) + .setReadOnly(true), + + // Server code + new EnumLocalizedFieldDescriptionImpl("CN", DnFieldLabelLocalizationKey.SERVER_DNS_NAME, "") + .setReadOnly(false), + + // server DNS name as subject alternative name + new EnumLocalizedFieldDescriptionImpl("subjectAltName", DnFieldLabelLocalizationKey.SUBJECT_ALTERNATIVE_NAME, "") + .setReadOnly(false), + }); + } + } + + /** + * Sign cert + *

+ * CN = member code + * O = member name + * businessCategory = member class + * C = country + * serialNumber = member code + */ + private static class BasicACMESignCertificateProfileInfo extends AbstractCertificateProfileInfo + implements SignCertificateProfileInfo { + + private final String instanceIdentifier; + + BasicACMESignCertificateProfileInfo(Parameters params) { + super(new DnFieldDescription[] { + // Country Code + new EnumLocalizedFieldDescriptionImpl("C", DnFieldLabelLocalizationKey.COUNTRY_CODE, "") + .setReadOnly(false), + + // Organization name + new EnumLocalizedFieldDescriptionImpl("O", DnFieldLabelLocalizationKey.ORGANIZATION_NAME, + params.getMemberName()) + .setReadOnly(true), + + // Business category + new EnumLocalizedFieldDescriptionImpl("businessCategory", DnFieldLabelLocalizationKey.MEMBER_CLASS_BC, + params.getClientId().getMemberClass()) + .setReadOnly(true), + + // Serial number + new EnumLocalizedFieldDescriptionImpl("serialNumber", DnFieldLabelLocalizationKey.MEMBER_CODE_SN, + params.getClientId().getMemberCode()) + .setReadOnly(true), + + // Member code + new EnumLocalizedFieldDescriptionImpl("CN", DnFieldLabelLocalizationKey.ORGANIZATION_NAME_CN, + params.getMemberName()) + .setReadOnly(true) + }); + + instanceIdentifier = params.getClientId().getXRoadInstance(); + } + + @Override + public ClientId.Conf getSubjectIdentifier(X509Certificate cert) { + X500Principal principal = cert.getSubjectX500Principal(); + X500Name x500name = new X500Name(principal.getName()); + + String memberClass = CertUtils.getRDNValue(x500name, BCStyle.BUSINESS_CATEGORY); + if (memberClass == null) { + throw XrdRuntimeException.businessException(ErrorCode.INVALID_CERTIFICATE) + .details("Certificate subject name does not contain business category") + .build(); + } + + String memberCode = CertUtils.getRDNValue(x500name, BCStyle.SERIALNUMBER); + if (memberCode == null) { + throw XrdRuntimeException.businessException(ErrorCode.INVALID_CERTIFICATE) + .details("Subject name does not contain serial number") + .build(); + } + + return ClientId.Conf.create(instanceIdentifier, memberClass, memberCode); + } + } +} diff --git a/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKSignCertificateProfileInfo.java b/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKSignCertificateProfileInfo.java index 73b2a2e0aa..efb8e69c67 100644 --- a/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKSignCertificateProfileInfo.java +++ b/src/lib/globalconf-core/src/main/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKSignCertificateProfileInfo.java @@ -48,7 +48,7 @@ public class FiVRKSignCertificateProfileInfo * @param params the parameters */ public FiVRKSignCertificateProfileInfo(Parameters params) { - super(new DnFieldDescription[]{ + super(new DnFieldDescription[] { // Country Code new EnumLocalizedFieldDescriptionImpl("C", DnFieldLabelLocalizationKey.COUNTRY_CODE, "FI" @@ -59,7 +59,7 @@ public FiVRKSignCertificateProfileInfo(Parameters params) { "" ).setReadOnly(false), - // Serialnumber + // Serial number new EnumLocalizedFieldDescriptionImpl("serialNumber", DnFieldLabelLocalizationKey.SERIAL_NUMBER, params.getClientId().getXRoadInstance() + "/" + params.getServerId().getServerCode() + "/" diff --git a/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/GlobalConfProvider.java b/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/GlobalConfProvider.java index dfc9ee9198..f600de99a7 100644 --- a/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/GlobalConfProvider.java +++ b/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/GlobalConfProvider.java @@ -347,9 +347,28 @@ boolean isSecurityServerClient(ClientId client, X509Certificate getCentralServerSslCertificate(); /** - * @return a set containing all configured source addresses + * Returns the set of source addresses for the given instance identifier. + * + * @param instanceIdentifier the instance identifier + * @return the set of source addresses for the given instance + */ + Set getSourceAddresses(String instanceIdentifier); + + /** + * Returns all allowed federation instances. + * Also taking into account the value of the {@code configuration-client.allowed-federations} property. + * + * @return a set of allowed federation instances + */ + Set getAllowedFederationInstances(); + + /** + * Returns the configuration directory path for the given instance identifier. + * + * @param instanceIdentifier the instance identifier + * @return the configuration directory path for the given instance */ - Set findSourceAddresses(); + String getConfigurationDirectoryPath(String instanceIdentifier); /** * @return maximum allowed validity time of OCSP responses. If thisUpdate diff --git a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterImpl.java b/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/util/FederationConfigurationSourceFilter.java similarity index 95% rename from src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterImpl.java rename to src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/util/FederationConfigurationSourceFilter.java index 7a1b2ae5e9..38242edfb1 100644 --- a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterImpl.java +++ b/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/util/FederationConfigurationSourceFilter.java @@ -23,7 +23,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.niis.xroad.confclient.core; +package org.niis.xroad.globalconf.util; import ee.ria.xroad.common.SystemProperties; @@ -54,7 +54,7 @@ * */ @Slf4j -public class FederationConfigurationSourceFilterImpl implements FederationConfigurationSourceFilter { +public class FederationConfigurationSourceFilter { private static final String COMMA_SEPARATOR = "\\s*,\\s*"; @@ -63,7 +63,7 @@ public class FederationConfigurationSourceFilterImpl implements FederationConfig private SystemProperties.AllowedFederationMode allowedFederationMode = null; private Set allowedFederationPartners = null; - FederationConfigurationSourceFilterImpl(String ownInstance) { + public FederationConfigurationSourceFilter(String ownInstance) { this.ownInstance = ownInstance; String filterString = SystemProperties.getConfigurationClientAllowedFederations(); log.info("The federation filter system property value is: '{}'", filterString); @@ -72,7 +72,6 @@ public class FederationConfigurationSourceFilterImpl implements FederationConfig allowedFederationMode, allowedFederationPartners); } - @Override public boolean shouldDownloadConfigurationFor(String instanceIdentifier) { if (ownInstance.equalsIgnoreCase(instanceIdentifier)) { return true; diff --git a/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/util/GlobalConfUtils.java b/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/util/GlobalConfUtils.java new file mode 100644 index 0000000000..080fea542d --- /dev/null +++ b/src/lib/globalconf-core/src/main/java/org/niis/xroad/globalconf/util/GlobalConfUtils.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS) + * Copyright (c) 2018 Estonian Information System Authority (RIA), + * Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK) + * Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.niis.xroad.globalconf.util; + +import org.niis.xroad.common.exception.ConflictException; +import org.niis.xroad.globalconf.model.ConfigurationLocation; +import org.niis.xroad.globalconf.model.ConfigurationSource; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.niis.xroad.common.core.exception.ErrorCode.INVALID_DOWNLOAD_URL_FORMAT; + +public final class GlobalConfUtils { + private static final Pattern CONF_PATTERN = Pattern.compile("http://[^/]*/"); + private static final String HTTP = "http"; + private static final String HTTPS = "https"; + + private GlobalConfUtils() { + } + + public static String getConfigurationDirectory(ConfigurationSource source) { + var firstHttpDownloadUrl = source.getLocations().stream() + .map(ConfigurationLocation::getDownloadURL) + .filter(GlobalConfUtils::startWithHttpAndNotWithHttps).findFirst(); + if (firstHttpDownloadUrl.isPresent()) { + Matcher matcher = CONF_PATTERN.matcher(firstHttpDownloadUrl.get()); + if (matcher.find()) { + return firstHttpDownloadUrl.get().substring(matcher.end()); + } + } + throw new ConflictException(INVALID_DOWNLOAD_URL_FORMAT.build()); + } + + public static boolean startWithHttpAndNotWithHttps(String url) { + return url.startsWith(HTTP) && !url.startsWith(HTTPS); + } +} diff --git a/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/BasicACMECertificateProfileInfoProviderTest.java b/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/BasicACMECertificateProfileInfoProviderTest.java new file mode 100644 index 0000000000..e8e4566d42 --- /dev/null +++ b/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/BasicACMECertificateProfileInfoProviderTest.java @@ -0,0 +1,243 @@ +/* + * The MIT License + * Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS) + * Copyright (c) 2018 Estonian Information System Authority (RIA), + * Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK) + * Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK) + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package ee.ria.xroad.common.certificateprofile.impl; + +import ee.ria.xroad.common.certificateprofile.AuthCertificateProfileInfo; +import ee.ria.xroad.common.certificateprofile.CertificateProfileInfoProvider; +import ee.ria.xroad.common.certificateprofile.DnFieldDescription; +import ee.ria.xroad.common.certificateprofile.DnFieldValue; +import ee.ria.xroad.common.certificateprofile.SignCertificateProfileInfo; +import ee.ria.xroad.common.identifier.ClientId; +import ee.ria.xroad.common.identifier.SecurityServerId; + +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * Tests the Basic ACME implementation of CertificateProfileInfoProvider. + */ +public class BasicACMECertificateProfileInfoProviderTest { + + /** + * Tests whether getting expected subject fields succeeds as expected. + */ + @Test + public void signProfileSubjectFields() { + DnFieldDescription[] expectedFields = { + new EnumLocalizedFieldDescriptionImpl("C", DnFieldLabelLocalizationKey.COUNTRY_CODE, + "" + ).setReadOnly(false), + new EnumLocalizedFieldDescriptionImpl("O", DnFieldLabelLocalizationKey.ORGANIZATION_NAME, + "foobar" + ).setReadOnly(true), + new EnumLocalizedFieldDescriptionImpl("businessCategory", DnFieldLabelLocalizationKey.MEMBER_CLASS_BC, + "bar" + ).setReadOnly(true), + new EnumLocalizedFieldDescriptionImpl("serialNumber", DnFieldLabelLocalizationKey.MEMBER_CODE_SN, + "baz" + ).setReadOnly(true), + new EnumLocalizedFieldDescriptionImpl("CN", DnFieldLabelLocalizationKey.ORGANIZATION_NAME_CN, + "foobar" + ).setReadOnly(true) + }; + assertArrayEquals( + "Did not get expected fields", + expectedFields, + getSignProfile().getSubjectFields() + ); + } + + /** + * Tests whether validating a correct subject field succeeds as expected. + * + */ + @Test + public void signProfileValidateFieldSuccessfully() { + getSignProfile().validateSubjectField( + new DnFieldValueImpl("CN", "XX") + ); + } + + /** + * Tests whether validating an unknown subject field fails as expected. + * + */ + @Test(expected = Exception.class) + public void signProfileFailToValidateUnknownField() { + getSignProfile().validateSubjectField( + new DnFieldValueImpl("X", "foo") + ); + } + + /** + * Tests whether validating blank subject field of sign profile fails + * as expected. + * + */ + @Test(expected = Exception.class) + public void signProfileFailToValidateBlankField() { + getSignProfile().validateSubjectField( + new DnFieldValueImpl("serialNumber", "") + ); + } + + /** + * Tests whether creating subject Dn of sign profile succeeds as expected. + */ + @Test + public void signProfileCreateSubjectDn() { + X500Principal x500PrincipalTest = new X500Principal("CN=XX, O=abc, serialNumber=baz"); + X500Principal x500PrincipalReal = getSignProfile().createSubjectDn( + new DnFieldValue[] { + new DnFieldValueImpl("CN", "XX"), + new DnFieldValueImpl("O", "abc"), + new DnFieldValueImpl("serialNumber", "baz") + } + ); + assertEquals(x500PrincipalTest, x500PrincipalReal); + } + + /** + * Tests whether getting expected fields of auth profile succeeds + * as expected. + */ + @Test + public void authProfileSubjectFields() { + DnFieldDescription[] expectedFields = { + new EnumLocalizedFieldDescriptionImpl("C", DnFieldLabelLocalizationKey.COUNTRY_CODE, "") + .setReadOnly(false), + new EnumLocalizedFieldDescriptionImpl("O", DnFieldLabelLocalizationKey.ORGANIZATION_NAME, "foobar") + .setReadOnly(true), + new EnumLocalizedFieldDescriptionImpl("serialNumber", DnFieldLabelLocalizationKey.MEMBER_CODE_SN, "bar") + .setReadOnly(true), + new EnumLocalizedFieldDescriptionImpl("CN", DnFieldLabelLocalizationKey.SERVER_DNS_NAME, "") + .setReadOnly(false), + new EnumLocalizedFieldDescriptionImpl("subjectAltName", DnFieldLabelLocalizationKey.SUBJECT_ALTERNATIVE_NAME, "") + .setReadOnly(false) + }; + assertArrayEquals( + "Did not get expected fields", + expectedFields, + getAuthProfile().getSubjectFields() + ); + } + + /** + * Tests whether validating correct subject field of auth profile succeeds + * as expected. + * + */ + @Test + public void authProfileValidateFieldSuccessfully() { + getAuthProfile().validateSubjectField( + new DnFieldValueImpl("O", "bar") + ); + } + + /** + * Tests whether validating unknown subject field of auth profile fails + * as expected. + * + */ + @Test(expected = Exception.class) + public void authProfileFailToValidateUnknownField() { + getAuthProfile().validateSubjectField( + new DnFieldValueImpl("X", "foo") + ); + } + + /** + * Tests whether validating blank subject field of auth profile fails + * as expected. + * + */ + @Test(expected = Exception.class) + public void authProfileFailToValidateBlankField() { + getAuthProfile().validateSubjectField( + new DnFieldValueImpl("serialNumber", "") + ); + } + + /** + * Tests whether creating subject Dn of auth profile succeeds as expected. + */ + @Test + public void authProfileCreateSubjectDn() { + assertEquals( + new X500Principal("CN=server, serialNumber=foo, O=bar"), + getAuthProfile().createSubjectDn( + new DnFieldValue[] { + new DnFieldValueImpl("CN", "server"), + new DnFieldValueImpl("serialNumber", "foo"), + new DnFieldValueImpl("O", "bar"), + } + ) + ); + } + + // ------------------------------------------------------------------------ + + private CertificateProfileInfoProvider provider() { + return new BasicACMECertificateProfileInfoProvider(); + } + + private SignCertificateProfileInfo getSignProfile() { + return provider().getSignCertProfile(new SignCertificateProfileInfo.Parameters() { + @Override + public ClientId getClientId() { + return ClientId.Conf.create("XX", "bar", "baz"); + } + + @Override + public String getMemberName() { + return "foobar"; + } + + @Override + public SecurityServerId getServerId() { + return null; + } + }); + } + + private AuthCertificateProfileInfo getAuthProfile() { + return provider().getAuthCertProfile(new AuthCertificateProfileInfo.Parameters() { + @Override + public SecurityServerId getServerId() { + return SecurityServerId.Conf.create("XX", "foo", "bar", "server"); + } + + @Override + public String getMemberName() { + return "foobar"; + } + }); + } +} diff --git a/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKCertificateProfileInfoProviderTest.java b/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKCertificateProfileInfoProviderTest.java index 58c7ea27f0..0db6281d01 100644 --- a/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKCertificateProfileInfoProviderTest.java +++ b/src/lib/globalconf-core/src/test/java/ee/ria/xroad/common/certificateprofile/impl/FiVRKCertificateProfileInfoProviderTest.java @@ -122,7 +122,7 @@ public void signProfileFailToValidateUnknownField() throws Exception { } /** - * Tests whether validating black subject field of sign profile fails + * Tests whether validating blank subject field of sign profile fails * as expected. * @throws Exception in case of any unexpected errors */ @@ -134,7 +134,7 @@ public void signProfileFailToValidateBlankField() throws Exception { } /** - * Tests whether creating subject Dn of sign profile succeeds as expected. + * Tests whether creating the subject Dn of sign profile succeeds as expected. */ @Test public void signProfileCreateSubjectDn() { diff --git a/src/lib/globalconf-impl/src/main/java/org/niis/xroad/globalconf/impl/GlobalConfImpl.java b/src/lib/globalconf-impl/src/main/java/org/niis/xroad/globalconf/impl/GlobalConfImpl.java index 0a878307a9..14bb114f41 100644 --- a/src/lib/globalconf-impl/src/main/java/org/niis/xroad/globalconf/impl/GlobalConfImpl.java +++ b/src/lib/globalconf-impl/src/main/java/org/niis/xroad/globalconf/impl/GlobalConfImpl.java @@ -57,6 +57,8 @@ import org.niis.xroad.globalconf.model.PrivateParameters; import org.niis.xroad.globalconf.model.SharedParameters; import org.niis.xroad.globalconf.model.SharedParametersCache; +import org.niis.xroad.globalconf.util.FederationConfigurationSourceFilter; +import org.niis.xroad.globalconf.util.GlobalConfUtils; import java.io.IOException; import java.security.cert.CertificateEncodingException; @@ -667,13 +669,34 @@ public X509Certificate getCentralServerSslCertificate() { } @Override - public Set findSourceAddresses() { - return getSharedParameters(getInstanceIdentifier()).getSources().stream() + public Set getSourceAddresses(String instanceIdentifier) { + return getSharedParameters(instanceIdentifier).getSources().stream() .map(SharedParameters.ConfigurationSource::getAddress) .filter(StringUtils::isNotBlank) .collect(Collectors.toSet()); } + @Override + public Set getAllowedFederationInstances() { + var localInstance = getInstanceIdentifier(); + var sourceFilter = new FederationConfigurationSourceFilter(localInstance); + + return getInstanceIdentifiers().stream() + .filter(instance -> !localInstance.equals(instance)) + .filter(sourceFilter::shouldDownloadConfigurationFor) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + } + + @Override + public String getConfigurationDirectoryPath(String instanceIdentifier) { + return getPrivateParameters().getConfigurationAnchors().stream() + .filter(configurationAnchor -> instanceIdentifier.equals(configurationAnchor.getInstanceIdentifier())) + .map(GlobalConfUtils::getConfigurationDirectory) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Configuration directory not found for instance " + instanceIdentifier)); + } + @Override public int getOcspFreshnessSeconds() { return getSharedParameters(getInstanceIdentifier()) diff --git a/src/lib/serverconf-impl/src/main/java/org/niis/xroad/serverconf/impl/ServerConfImpl.java b/src/lib/serverconf-impl/src/main/java/org/niis/xroad/serverconf/impl/ServerConfImpl.java index 65efa6f45b..48f4585f47 100644 --- a/src/lib/serverconf-impl/src/main/java/org/niis/xroad/serverconf/impl/ServerConfImpl.java +++ b/src/lib/serverconf-impl/src/main/java/org/niis/xroad/serverconf/impl/ServerConfImpl.java @@ -84,7 +84,6 @@ import org.niis.xroad.serverconf.model.TimestampingService; import java.io.IOException; -import java.net.URI; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; @@ -461,7 +460,13 @@ private boolean checkAccessRights(Session session, ClientId clientId, ServiceId if (path == null) { normalizedPath = null; } else { - normalizedPath = UriUtils.uriPathPercentDecode(URI.create(path).normalize().getRawPath(), true); + normalizedPath = UriUtils.decodeAndNormalize(path); + + // Explicitly reject any remaining traversal sequences + if (normalizedPath.contains("..")) { + log.warn("Path traversal detected in request path: {}. Access will be rejected.", path); + return false; + } } return getAclEndpoints(session, clientId, serviceId).stream() .anyMatch(ep -> ep.matches(method, normalizedPath)); diff --git a/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/CachingServerConfTest.java b/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/CachingServerConfTest.java index d7a992997e..eac2109e7d 100644 --- a/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/CachingServerConfTest.java +++ b/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/CachingServerConfTest.java @@ -256,6 +256,10 @@ public void isQueryAllowed() { assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "POST", "/api/test/foo/bar")); assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "DELETE", "/api/test")); assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest)); + + assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "GET", "/%2e%2e/secret")); + assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "GET", "/api/%2e%2e/secret")); + assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "GET", "/api/test/%2e%2e/%2e%2e/secret")); } /** diff --git a/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/ServerConfTest.java b/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/ServerConfTest.java index aa322a4992..9557aa57b3 100644 --- a/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/ServerConfTest.java +++ b/src/lib/serverconf-impl/src/test/java/org/niis/xroad/serverconf/impl/ServerConfTest.java @@ -249,6 +249,10 @@ public void isQueryAllowed() { assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "POST", "/api/test/foo/bar")); assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "DELETE", "/api/test")); assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest)); + + assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "GET", "/%2e%2e/secret")); + assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "GET", "/api/%2e%2e/secret")); + assertFalse(serverConfProvider.isQueryAllowed(client1, serviceRest, "GET", "/api/test/%2e%2e/%2e%2e/secret")); } /** diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/ClientSslKeyManager.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/config/ClientSslKeyManager.java similarity index 94% rename from src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/ClientSslKeyManager.java rename to src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/config/ClientSslKeyManager.java index 97e72341ef..944d9d7797 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/ClientSslKeyManager.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/config/ClientSslKeyManager.java @@ -23,7 +23,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.niis.xroad.securityserver.restapi.wsdl; +package org.niis.xroad.securityserver.restapi.config; import ee.ria.xroad.common.conf.InternalSSLKey; @@ -42,7 +42,7 @@ @Slf4j @RequiredArgsConstructor -class ClientSslKeyManager extends X509ExtendedKeyManager { +public class ClientSslKeyManager extends X509ExtendedKeyManager { private static final String ALIAS = "ClientSslKeyManager"; @@ -65,7 +65,7 @@ public X509Certificate[] getCertificateChain(String alias) { @Override public String[] getClientAliases(String keyType, Principal[] issuers) { - return null; + return new String[]{}; } @Override @@ -75,7 +75,7 @@ public PrivateKey getPrivateKey(String alias) { @Override public String[] getServerAliases(String keyType, Principal[] issuers) { - return null; + return new String[]{}; } @Override diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/AuthCertStatusConverter.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/ConnectionStatusConverter.java similarity index 98% rename from src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/AuthCertStatusConverter.java rename to src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/ConnectionStatusConverter.java index 1f75baf022..1f543db1aa 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/AuthCertStatusConverter.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/ConnectionStatusConverter.java @@ -34,7 +34,7 @@ import java.util.Optional; @Component -public class AuthCertStatusConverter { +public class ConnectionStatusConverter { public ConnectionStatusDto convert(ConnectionStatus connectionStatus) { return new ConnectionStatusDto() .error(getCodeWithDetailsDto(connectionStatus)) diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/GlobalConfStatusConverter.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/GlobalConfStatusConverter.java index 0827ad5598..5b5f33de5c 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/GlobalConfStatusConverter.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/converter/GlobalConfStatusConverter.java @@ -34,7 +34,7 @@ @Component public class GlobalConfStatusConverter { - private final AuthCertStatusConverter authCertStatusConverter = new AuthCertStatusConverter(); + private final ConnectionStatusConverter connectionStatusConverter = new ConnectionStatusConverter(); public List convert(List connectionStatuses) { return connectionStatuses.stream() @@ -45,6 +45,6 @@ public List convert(List> findClients(String name, String instance, String memberClass, String memberCode, String subsystemCode, Boolean showMembers, Boolean internalSearch, - Boolean localValidSignCert, Boolean excludeLocal) { + Boolean localValidSignCert, Boolean excludeLocal, + Boolean includeManagementServiceCheck) { ClientService.SearchParameters searchParams = ClientService.SearchParameters.builder() .name(name) .instance(instance) @@ -183,6 +191,10 @@ public ResponseEntity> findClients(String name, String instance, .hasValidLocalSignCert(localValidSignCert) .build(); Set clients = clientConverter.convert(clientService.findClients(searchParams)); + if (BooleanUtils.isTrue(includeManagementServiceCheck)) { + clients.forEach(clientDto -> clientDto.setIsManagementServicesProvider( + clientService.isManagementServiceProvider(clientIdConverter.convertId(clientDto.getId())))); + } return new ResponseEntity<>(clients, HttpStatus.OK); } @@ -442,6 +454,16 @@ public ResponseEntity getClientOrphans(String encodedClien } } + @Override + @PreAuthorize("hasAuthority('VIEW_SECURITY_SERVERS')") + public ResponseEntity> getClientSecurityServers(String encodedClientId) { + ClientId clientId = clientIdConverter.convertId(encodedClientId); + Set securityServerIds = globalConfProvider.getSecurityServers().stream() + .filter(ssId -> globalConfProvider.isSecurityServerClient(clientId, ssId)) + .collect(toSet()); + return new ResponseEntity<>(securityServerConverter.convert(securityServerIds), HttpStatus.OK); + } + @Override @PreAuthorize("hasAuthority('SEND_CLIENT_REG_REQ')") @AuditEventMethod(event = REGISTER_CLIENT) diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiController.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiController.java index 1446fa7d2a..574a81307e 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiController.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiController.java @@ -37,11 +37,12 @@ import org.niis.xroad.common.exception.InternalServerErrorException; import org.niis.xroad.opmonitor.api.OperationalDataInterval; import org.niis.xroad.restapi.converter.ClientIdConverter; +import org.niis.xroad.restapi.converter.SecurityServerIdConverter; import org.niis.xroad.restapi.converter.ServiceIdConverter; import org.niis.xroad.restapi.openapi.ControllerUtil; import org.niis.xroad.securityserver.restapi.converter.AddOnStatusConverter; -import org.niis.xroad.securityserver.restapi.converter.AuthCertStatusConverter; import org.niis.xroad.securityserver.restapi.converter.BackupEncryptionStatusConverter; +import org.niis.xroad.securityserver.restapi.converter.ConnectionStatusConverter; import org.niis.xroad.securityserver.restapi.converter.GlobalConfDiagnosticConverter; import org.niis.xroad.securityserver.restapi.converter.GlobalConfStatusConverter; import org.niis.xroad.securityserver.restapi.converter.MessageLogEncryptionStatusConverter; @@ -50,6 +51,7 @@ import org.niis.xroad.securityserver.restapi.converter.ProxyMemoryUsageStatusConverter; import org.niis.xroad.securityserver.restapi.converter.TimestampingServiceDiagnosticConverter; import org.niis.xroad.securityserver.restapi.dto.OcspResponderDiagnosticsStatus; +import org.niis.xroad.securityserver.restapi.dto.ServiceProtocolType; import org.niis.xroad.securityserver.restapi.openapi.model.AddOnStatusDto; import org.niis.xroad.securityserver.restapi.openapi.model.BackupEncryptionStatusDto; import org.niis.xroad.securityserver.restapi.openapi.model.CaOcspDiagnosticsDto; @@ -102,8 +104,9 @@ public class DiagnosticsApiController implements DiagnosticsApi { private final ProxyMemoryUsageStatusConverter proxyMemoryUsageStatusConverter; private final OperationalInfoConverter operationalInfoConverter; private final ClientIdConverter clientIdConverter; + private final SecurityServerIdConverter securityServerIdConverter; private final ServiceIdConverter serviceIdConverter; - private final AuthCertStatusConverter authCertStatusConverter; + private final ConnectionStatusConverter connectionStatusConverter; private final GlobalConfStatusConverter globalConfStatusConverter; @Override @@ -192,7 +195,7 @@ public ResponseEntity> getOperationalDataInterv @Override @PreAuthorize("hasAuthority('DIAGNOSTICS')") public ResponseEntity getAuthCertReqStatus() { - return new ResponseEntity<>(authCertStatusConverter.convert(diagnosticConnectionService.getAuthCertReqStatus()), HttpStatus.OK); + return new ResponseEntity<>(connectionStatusConverter.convert(diagnosticConnectionService.getAuthCertReqStatus()), HttpStatus.OK); } @Override @@ -201,6 +204,20 @@ public ResponseEntity> getGlobalConfStatus() return new ResponseEntity<>(globalConfStatusConverter.convert(diagnosticConnectionService.getGlobalConfStatus()), HttpStatus.OK); } + @Override + @PreAuthorize("hasAuthority('DIAGNOSTICS')") + public ResponseEntity getOtherSecurityServerStatus(String protocolType, String clientId, String targetClientId, + String securityServerId) { + return new ResponseEntity<>(connectionStatusConverter.convert( + diagnosticConnectionService.getOtherSecurityServerStatus( + ServiceProtocolType.valueOf(protocolType), + clientIdConverter.convertId(clientId), + clientIdConverter.convertId(targetClientId), + securityServerIdConverter.convertId(securityServerId) + )), + HttpStatus.OK); + } + private String systemInformationFilename() { return "diagnostic-report-%s.json".formatted(FORMATTER.format(LocalDateTime.now())); } diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/XroadInstancesApiController.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/XRoadInstancesApiController.java similarity index 78% rename from src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/XroadInstancesApiController.java rename to src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/XRoadInstancesApiController.java index 6080a85de0..5e97ec2b9d 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/XroadInstancesApiController.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/openapi/XRoadInstancesApiController.java @@ -28,6 +28,7 @@ import lombok.RequiredArgsConstructor; import org.niis.xroad.globalconf.GlobalConfProvider; import org.niis.xroad.restapi.openapi.ControllerUtil; +import org.niis.xroad.securityserver.restapi.openapi.model.XRoadInstanceDto; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import java.util.Set; +import java.util.stream.Collectors; /** * controller for xroad instance identifiers @@ -43,14 +45,17 @@ @RequestMapping(ControllerUtil.API_V1_PREFIX) @PreAuthorize("denyAll") @RequiredArgsConstructor -public class XroadInstancesApiController implements XroadInstancesApi { +public class XRoadInstancesApiController implements XRoadInstancesApi { private final GlobalConfProvider globalConfProvider; @Override @PreAuthorize("hasAuthority('VIEW_XROAD_INSTANCES')") - public ResponseEntity> getXroadInstances() { - Set xroadInstances = globalConfProvider.getInstanceIdentifiers(); - return new ResponseEntity<>(xroadInstances, HttpStatus.OK); + public ResponseEntity> getXRoadInstances() { + Set xRoadInstances = globalConfProvider.getInstanceIdentifiers(); + String localInstance = globalConfProvider.getInstanceIdentifier(); + return new ResponseEntity<>(xRoadInstances.stream() + .map(instance -> new XRoadInstanceDto(instance, localInstance.equals(instance))) + .collect(Collectors.toSet()), HttpStatus.OK); } } diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ClientService.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ClientService.java index c995d3f1a4..e0e153175d 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ClientService.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ClientService.java @@ -444,22 +444,6 @@ List searchClientEntities(SearchParameters searchParameters, List< .collect(Collectors.toList()); } - /** - * Find client by ClientId - * - * @param clientId - * @return - */ - Optional findEntityByClientId(ClientId clientId) { - List localClients = getAllLocalClientEntities(); - List globalClients = getAllGlobalClientEntities(); - List distinctClients = mergeClientEntitiesDistinctively(globalClients, localClients); - return distinctClients.stream() - .filter(clientType -> clientType.getIdentifier().toShortString().trim() - .equals(clientId.toShortString().trim())) - .findFirst(); - } - /** * Find from all clients (local or global) */ diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionService.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionService.java index a86f313a3e..8606bcface 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionService.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionService.java @@ -27,9 +27,34 @@ import ee.ria.xroad.common.CodedException; import ee.ria.xroad.common.SystemProperties; +import ee.ria.xroad.common.identifier.ClientId; +import ee.ria.xroad.common.identifier.SecurityServerId; +import ee.ria.xroad.common.identifier.ServiceId; +import ee.ria.xroad.common.message.ProtocolVersion; +import ee.ria.xroad.common.message.RestMessage; +import ee.ria.xroad.common.message.Soap; +import ee.ria.xroad.common.message.SoapBuilder; +import ee.ria.xroad.common.message.SoapFault; +import ee.ria.xroad.common.message.SoapHeader; +import ee.ria.xroad.common.message.SoapMessageImpl; +import ee.ria.xroad.common.message.SoapParserImpl; +import ee.ria.xroad.common.util.CryptoUtils; +import ee.ria.xroad.common.util.HttpSender; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.soap.SOAPException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.niis.xroad.common.core.dto.ConnectionStatus; import org.niis.xroad.common.core.dto.DownloadUrlConnectionStatus; import org.niis.xroad.common.core.exception.ErrorCode; @@ -37,23 +62,44 @@ import org.niis.xroad.common.core.exception.XrdRuntimeException; import org.niis.xroad.common.core.util.HttpUrlConnectionConfigurer; import org.niis.xroad.globalconf.GlobalConfProvider; +import org.niis.xroad.securityserver.restapi.config.ClientSslKeyManager; +import org.niis.xroad.securityserver.restapi.dto.ServiceProtocolType; import org.niis.xroad.securityserver.restapi.util.AuthCertVerifier; +import org.niis.xroad.serverconf.ServerConfProvider; import org.niis.xroad.signer.api.dto.CertificateInfo; import org.niis.xroad.signer.protocol.dto.KeyUsageInfo; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Stream; import static ee.ria.xroad.common.ErrorCodes.X_INVALID_REQUEST; +import static ee.ria.xroad.common.util.AbstractHttpSender.CHUNKED_LENGTH; +import static ee.ria.xroad.common.util.MimeTypes.TEXT_XML_UTF8; +import static ee.ria.xroad.common.util.MimeUtils.HEADER_CLIENT_ID; +import static ee.ria.xroad.common.util.MimeUtils.HEADER_SECURITY_SERVER; +import static ee.ria.xroad.common.util.MimeUtils.getBaseContentType; import static org.niis.xroad.securityserver.restapi.service.PossibleActionsRuleEngine.SOFTWARE_TOKEN_ID; @Slf4j @@ -64,6 +110,7 @@ public class DiagnosticConnectionService { private static final String HTTP = "http"; private static final String HTTPS = "https"; + private static final Integer HTTP_200 = 200; private static final Integer PORT_80 = 80; private static final Integer PORT_443 = 443; @@ -72,17 +119,35 @@ public class DiagnosticConnectionService { private final AuthCertVerifier authCertVerifier; private final ManagementRequestSenderService managementRequestSenderService; private final HttpUrlConnectionConfigurer connectionConfigurer = new HttpUrlConnectionConfigurer(); + private final ServerConfProvider serverConfProvider; public List getGlobalConfStatus() { - Set addresses = globalConfProvider.findSourceAddresses(); + List downloadUrls = new ArrayList<>(getDownloadUrls( + globalConfProvider.getSourceAddresses(globalConfProvider.getInstanceIdentifier()), + getCenterInternalDirectory() + )); + + var allowedFederationInstances = globalConfProvider.getAllowedFederationInstances(); + allowedFederationInstances.forEach(allowedFederationInstance -> + downloadUrls.addAll( + getDownloadUrls( + globalConfProvider.getSourceAddresses(allowedFederationInstance), + globalConfProvider.getConfigurationDirectoryPath(allowedFederationInstance) + ) + ) + ); + + return downloadUrls.stream() + .map(this::checkAndGetConnectionStatus) + .toList(); + } + private static List getDownloadUrls(Set addresses, String configurationDirectory) { return addresses.stream() .flatMap(address -> Stream.of( - getUrl(HTTP, address, PORT_80), - getUrl(HTTPS, address, PORT_443) + getUrl(HTTP, address, PORT_80, configurationDirectory), + getUrl(HTTPS, address, PORT_443, configurationDirectory) )) - .distinct() - .map(this::checkAndGetConnectionStatus) .toList(); } @@ -90,21 +155,21 @@ private static String getCenterInternalDirectory() { return SystemProperties.getCenterInternalDirectory(); } - private URL getUrl(String protocol, String address, int port) { + private static URL getUrl(String protocol, String address, int port, String directory) { try { - return URI.create(getDownloadUrl(protocol, address, port)).toURL(); + return URI.create(getDownloadUrl(protocol, address, port, directory)).toURL(); } catch (MalformedURLException e) { log.error("Could not create URL from address {}", address, e); } return null; } - private String getDownloadUrl(String protocol, String address, int port) { - return String.format("%s://%s:%d/%s", protocol, address, port, getCenterInternalDirectory()); + private static String getDownloadUrl(String protocol, String address, int port, String directory) { + return String.format("%s://%s:%d/%s", protocol, address, port, directory); } - private String getDownloadUrl(URL url) { - return getDownloadUrl(url.getProtocol(), url.getHost(), url.getPort()); + private static String getDownloadUrl(URL url) { + return getDownloadUrl(url.getProtocol(), url.getHost(), url.getPort(), url.getPath().replaceFirst("^/", "")); } private DownloadUrlConnectionStatus checkAndGetConnectionStatus(URL url) { @@ -158,6 +223,182 @@ public ConnectionStatus getAuthCertReqStatus() { } } + public ConnectionStatus getOtherSecurityServerStatus(ServiceProtocolType protocolType, ClientId clientId, + ClientId targetClientId, + SecurityServerId securityServerId) { + if (!ServiceProtocolType.REST.equals(protocolType) && !ServiceProtocolType.SOAP.equals(protocolType)) { + throw new IllegalStateException("Unsupported protocol type: " + protocolType); + } + + try (CloseableHttpClient proxyHttpClient = createProxyHttpClient()) { + switch (protocolType) { + case REST -> { + HttpGet request = getRestHttpGet(clientId, targetClientId, securityServerId); + + try (CloseableHttpResponse response = proxyHttpClient.execute(request)) { + if (response.getStatusLine().getStatusCode() != HTTP_200) { + ObjectMapper mapper = new ObjectMapper(); + String body = EntityUtils.toString(response.getEntity()); + JsonNode json = mapper.readTree(body); + + String errorCode = json.has("type") ? json.get("type").asText() : "Error"; + String details = json.has("message") ? json.get("message").asText() : body; + + return ConnectionStatus.error(errorCode, List.of(details)); + } + } + + } + case SOAP -> { + try (HttpSender sender = createSender(proxyHttpClient)) { + SoapMessageImpl soapMessage = buildListMethodsSoapMessage( + clientId, targetClientId, securityServerId); + + send(sender, new URI(SystemProperties.getProxyUiSecurityServerUrl()), soapMessage); + } + } + default -> throw new IllegalStateException("should not get here"); + } + + } catch (Exception e) { + XrdRuntimeException result = XrdRuntimeException.systemException(e); + return ConnectionStatus.error(result.getErrorCode(), List.of(result.getDetails())); + } + + return ConnectionStatus.ok(); + } + + private CloseableHttpClient createProxyHttpClient() { + try { + return createProxyHttpClientWithInternalKey(); + } catch (Exception e) { + throw XrdRuntimeException.systemException(ErrorCode.INTERNAL_ERROR) + .cause(e) + .details("Unable to initialize request client") + .build(); + } + } + + @SuppressWarnings("java:S4830") // Won't fix: Works as designed ("Server certificates should be verified") + private CloseableHttpClient createProxyHttpClientWithInternalKey() throws NoSuchAlgorithmException, KeyManagementException { + TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // never called as this is trust manager of a client + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + // localhost called so server is trusted + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + }; + + return createHttpClient(new KeyManager[] {new ClientSslKeyManager(serverConfProvider)}, new TrustManager[] {trustManager}); + } + + private static CloseableHttpClient createHttpClient(KeyManager[] keyManagers, TrustManager[] trustManagers) + throws NoSuchAlgorithmException, KeyManagementException { + + SSLContext sslContext = SSLContext.getInstance(CryptoUtils.SSL_PROTOCOL); + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + + int timeout = SystemProperties.getClientProxyTimeout(); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setConnectionRequestTimeout(timeout) + .setSocketTimeout(SystemProperties.getClientProxyHttpClientTimeout()) + .build(); + + return HttpClients.custom() + .setSSLSocketFactory(sslSocketFactory) + .setDefaultRequestConfig(requestConfig) + .disableAutomaticRetries() + .build(); + } + + private static HttpSender createSender(CloseableHttpClient client) { + HttpSender httpSender = new HttpSender(client); + httpSender.setConnectionTimeout(SystemProperties.getClientProxyTimeout()); + httpSender.setSocketTimeout(SystemProperties.getClientProxyHttpClientTimeout()); + return httpSender; + } + + private static void send(HttpSender sender, URI address, SoapMessageImpl soapMessage) throws XrdRuntimeException { + try { + sender.doPost(address, new ByteArrayInputStream(soapMessage.getBytes()), CHUNKED_LENGTH, TEXT_XML_UTF8); + + Soap response = new SoapParserImpl().parse(getBaseContentType(sender.getResponseContentType()), sender.getResponseContent()); + if (response instanceof SoapFault soapFault) { + throw soapFault.toCodedException(); + } + + } catch (Exception e) { + throw XrdRuntimeException.systemException(e); + } + } + + private HttpGet getRestHttpGet(ClientId clientId, ClientId targetClientId, SecurityServerId securityServerId) { + HttpGet request = new HttpGet(URI.create(SystemProperties.getProxyUiSecurityServerUrl() + getRestPath(targetClientId))); + request.setProtocolVersion(org.apache.http.HttpVersion.HTTP_1_1); + request.addHeader("accept", "application/json"); + request.addHeader(HEADER_SECURITY_SERVER, String.format("%s/%s/%s/%s", + securityServerId.getXRoadInstance(), + securityServerId.getMemberClass(), + securityServerId.getMemberCode(), + securityServerId.getServerCode())); + request.addHeader(HEADER_CLIENT_ID, clientId.getSubsystemCode() != null + ? String.format("%s/%s/%s/%s", + clientId.getXRoadInstance(), + clientId.getMemberClass(), + clientId.getMemberCode(), + clientId.getSubsystemCode()) + : String.format("%s/%s/%s", + clientId.getXRoadInstance(), + clientId.getMemberClass(), + clientId.getMemberCode())); + return request; + } + + private static SoapMessageImpl buildListMethodsSoapMessage(ClientId clientId, ClientId targetClientId, + SecurityServerId securityServerId) + throws IllegalAccessException, SOAPException, JAXBException, IOException { + + SoapHeader header = new SoapHeader(); + header.setClient(clientId); + header.setService(ServiceId.Conf.create(targetClientId, "listMethods")); + header.setSecurityServer(SecurityServerId.Conf.create( + securityServerId.getXRoadInstance(), + securityServerId.getMemberClass(), + securityServerId.getMemberCode(), + securityServerId.getServerCode() + )); + header.setQueryId(UUID.randomUUID().toString()); + header.setProtocolVersion(new ProtocolVersion()); + + SoapBuilder builder = new SoapBuilder(); + builder.setHeader(header); + builder.setRpcEncoded(false); + + return builder.build(); + } + + private static String getRestPath(ClientId clientId) { + return String.format("/r%d/%s/%s/%s/%s/listMethods", + RestMessage.PROTOCOL_VERSION, + clientId.getXRoadInstance(), + clientId.getMemberClass(), + clientId.getMemberCode(), + clientId.getSubsystemCode()); + } + private boolean isExpectedInvalidRequest(CodedException e) { return X_INVALID_REQUEST.equals(e.getFaultCode()) || "InvalidRequest".equals(e.getFaultCode()); } diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ManagementRequestSenderService.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ManagementRequestSenderService.java index ba858d4426..bfd94d8726 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ManagementRequestSenderService.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/service/ManagementRequestSenderService.java @@ -71,7 +71,9 @@ public Integer sendAuthCertRegisterRequest(String address, byte[] authCert, bool try { return sender.sendAuthCertRegRequest(currentSecurityServerId.getServerId(), address, authCert, dryRun); } catch (Exception e) { - log.error(MANAGEMENT_REQUEST_SENDING_FAILED_ERROR, e); + if (!dryRun) { + log.error(MANAGEMENT_REQUEST_SENDING_FAILED_ERROR, e); + } if (e instanceof CodedException) { throw (CodedException) e; } diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/HttpUrlConnectionConfig.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/HttpUrlConnectionConfig.java index 7fdba24503..11eacb38b7 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/HttpUrlConnectionConfig.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/HttpUrlConnectionConfig.java @@ -27,6 +27,7 @@ package org.niis.xroad.securityserver.restapi.wsdl; import lombok.RequiredArgsConstructor; +import org.niis.xroad.securityserver.restapi.config.ClientSslKeyManager; import org.niis.xroad.securityserver.restapi.config.CustomClientTlsSSLSocketFactory; import org.niis.xroad.serverconf.ServerConfProvider; import org.springframework.beans.factory.InitializingBean; diff --git a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/WsdlParser.java b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/WsdlParser.java index 3fa695c9f6..f0c5359f8d 100644 --- a/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/WsdlParser.java +++ b/src/security-server/admin-service/application/src/main/java/org/niis/xroad/securityserver/restapi/wsdl/WsdlParser.java @@ -33,6 +33,7 @@ import org.niis.xroad.common.core.exception.ErrorCode; import org.niis.xroad.common.core.exception.XrdRuntimeException; import org.niis.xroad.common.exception.BadRequestException; +import org.niis.xroad.securityserver.restapi.config.ClientSslKeyManager; import org.niis.xroad.serverconf.ServerConfProvider; import org.springframework.stereotype.Component; import org.w3c.dom.Element; diff --git a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/ClientsApiControllerIntegrationTest.java b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/ClientsApiControllerIntegrationTest.java index ffbc0d4520..a79ae6b56f 100644 --- a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/ClientsApiControllerIntegrationTest.java +++ b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/ClientsApiControllerIntegrationTest.java @@ -168,7 +168,7 @@ public void setup() throws Exception { @WithMockUser(authorities = "VIEW_CLIENTS") public void getAllClients() { ResponseEntity> response = clientsApiController.findClients(null, null, null, null, null, true, - false, null, false); + false, null, false, false); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(12, response.getBody().size()); // Test sorting order @@ -180,7 +180,7 @@ public void getAllClients() { public void ownerMemberFlag() { ResponseEntity> response = clientsApiController.findClients(null, null, null, null, null, true, - false, null, false); + false, null, false, false); assertEquals(12, response.getBody().size()); List owners = response.getBody().stream() .filter(ClientDto::getOwner) @@ -193,7 +193,7 @@ public void ownerMemberFlag() { @WithMockUser(authorities = "VIEW_CLIENTS") public void getAllLocalClients() { ResponseEntity> response = clientsApiController.findClients(null, null, null, null, null, true, - true, null, false); + true, null, false, false); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(8, response.getBody().size()); ClientDto client = response @@ -297,7 +297,7 @@ public void getClientSignCertificates() { public void forbidden() { try { clientsApiController.findClients(null, null, null, null, null, null, - null, null, false); + null, null, false, false); fail("should throw AccessDeniedException"); } catch (AccessDeniedException expected) { } @@ -429,7 +429,7 @@ public void findAllClientsByAllSearchTermsExcludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients( TestUtils.NAME_FOR + TestUtils.SUBSYSTEM1, TestUtils.INSTANCE_FI, TestUtils.MEMBER_CLASS_GOV, TestUtils.MEMBER_CODE_M1, TestUtils.SUBSYSTEM1, - false, false, null, false); + false, false, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(1, clientsResponse.getBody().size()); Set clients = clientsResponse.getBody(); @@ -449,7 +449,7 @@ public void findAllClientsByAllSearchTermsExcludeMembers() { @WithMockUser(authorities = "VIEW_CLIENTS") public void findAllClients() { ResponseEntity> clientsResponse = clientsApiController.findClients(null, null, null, null, null, - true, false, null, false); + true, false, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(12, clientsResponse.getBody().size()); } @@ -464,19 +464,19 @@ public void findAllClientsByLocalValidSignCert() { int clientsWithValidSignCert = 3; // search all ResponseEntity> clientsResponse = clientsApiController.findClients(null, null, null, null, null, - true, false, null, false); + true, false, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(clientsTotal, clientsResponse.getBody().size()); // search ones with valid sign cert clientsResponse = clientsApiController.findClients(null, null, null, null, null, - true, false, true, false); + true, false, true, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(clientsWithValidSignCert, clientsResponse.getBody().size()); // search ones without valid sign cert clientsResponse = clientsApiController.findClients(null, null, null, null, null, - true, false, false, false); + true, false, false, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals((clientsTotal - clientsWithValidSignCert), clientsResponse.getBody().size()); } @@ -498,7 +498,7 @@ private List createSimpleSignCertList() { @WithMockUser(authorities = "VIEW_CLIENTS") public void findAllClientsByMemberCodeIncludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients(null, null, null, - TestUtils.MEMBER_CODE_M1, null, true, false, null, false); + TestUtils.MEMBER_CODE_M1, null, true, false, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(5, clientsResponse.getBody().size()); } @@ -508,7 +508,7 @@ public void findAllClientsByMemberCodeIncludeMembers() { public void findAllClientsByMemberClassIncludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients(null, null, TestUtils.MEMBER_CLASS_PRO, - null, null, true, false, null, false); + null, null, true, false, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(3, clientsResponse.getBody().size()); } @@ -518,12 +518,12 @@ public void findAllClientsByMemberClassIncludeMembers() { public void findAllClientsByNameIncludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients( TestUtils.NAME_FOR + TestUtils.SUBSYSTEM2, - null, null, null, null, false, true, null, false); + null, null, null, null, false, true, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(1, clientsResponse.getBody().size()); // not found clientsResponse = clientsApiController.findClients("DOES_NOT_EXIST", null, null, null, null, true, false, - null, false); + null, false, false); assertEquals(0, clientsResponse.getBody().size()); } @@ -533,7 +533,7 @@ public void findInternalClientsByAllSearchTermsExcludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients( TestUtils.NAME_FOR + TestUtils.SUBSYSTEM1, TestUtils.INSTANCE_FI, TestUtils.MEMBER_CLASS_GOV, TestUtils.MEMBER_CODE_M1, TestUtils.SUBSYSTEM1, - false, true, null, false); + false, true, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(1, clientsResponse.getBody().size()); } @@ -542,12 +542,12 @@ public void findInternalClientsByAllSearchTermsExcludeMembers() { @WithMockUser(authorities = "VIEW_CLIENTS") public void findInternalClientsBySubsystemExcludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients(null, null, null, null, - TestUtils.SUBSYSTEM2, false, true, null, false); + TestUtils.SUBSYSTEM2, false, true, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(1, clientsResponse.getBody().size()); // not found clientsResponse = clientsApiController.findClients(null, null, null, null, TestUtils.SUBSYSTEM3, false, true, - null, false); + null, false, false); assertEquals(0, clientsResponse.getBody().size()); } @@ -607,7 +607,7 @@ private Optional getDescription(Set> clientsResponse = clientsApiController.findClients(TestUtils.SUBSYSTEM3, null, - null, null, null, false, false, null, false); + null, null, null, false, false, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(1, clientsResponse.getBody().size()); } @@ -616,7 +616,7 @@ public void findAllClientsByPartialNameIncludeMembers() { @WithMockUser(authorities = {"VIEW_CLIENTS"}) public void findAllClientsByPartialSearchTermsIncludeMembers() { ResponseEntity> clientsResponse = clientsApiController.findClients(null, "FI", - "OV", "1", "1", false, true, null, false); + "OV", "1", "1", false, true, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(1, clientsResponse.getBody().size()); } @@ -625,7 +625,7 @@ public void findAllClientsByPartialSearchTermsIncludeMembers() { @WithMockUser(authorities = {"VIEW_CLIENTS"}) public void findAllClientsShouldNotFindByPartialInstance() { ResponseEntity> clientsResponse = clientsApiController.findClients(null, "F", - "OV", "1", "1", false, true, null, false); + "OV", "1", "1", false, true, null, false, false); assertEquals(HttpStatus.OK, clientsResponse.getStatusCode()); assertEquals(0, clientsResponse.getBody().size()); } diff --git a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiControllerTest.java b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiControllerTest.java index 2d00923d12..5be128fcaa 100644 --- a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiControllerTest.java +++ b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/DiagnosticsApiControllerTest.java @@ -524,7 +524,7 @@ public void getAuthCertReqStatus() { @Test @WithMockUser(authorities = {"DIAGNOSTICS"}) public void getGlobalConfStatus() { - when(globalConfProvider.findSourceAddresses()).thenReturn(Set.of("one-host")); + when(globalConfProvider.getSourceAddresses(globalConfProvider.getInstanceIdentifier())).thenReturn(Set.of("one-host")); ResponseEntity> response = diagnosticsApiController.getGlobalConfStatus(); @@ -549,6 +549,22 @@ public void getGlobalConfStatus() { }); } + @Test + @WithMockUser(authorities = {"DIAGNOSTICS"}) + public void getOtherSecurityServerStatus() { + ResponseEntity response = diagnosticsApiController.getOtherSecurityServerStatus("REST", + "DEV:COM:4321", "DEV:COM:1234:MANAGEMENT", "DEV:COM:1234:SS0"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + ConnectionStatusDto connectionStatusDto = response.getBody(); + assertNotNull(connectionStatusDto); + assertEquals(DiagnosticStatusClassDto.FAIL, connectionStatusDto.getStatusClass()); + assertEquals("network_error", connectionStatusDto.getError().getCode()); + assertThat(connectionStatusDto.getError().getMetadata().getFirst()) + .contains("Connect to localhost:8443") + .contains("Connection refused"); + } + private void stubForDiagnosticsRequest(String requestPath, String responseBody) { stubFor(get(urlEqualTo(requestPath)) .willReturn(aResponse().withBody(responseBody))); diff --git a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/XroadInstancesApiControllerIntegrationTest.java b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/XRoadInstancesApiControllerIntegrationTest.java similarity index 75% rename from src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/XroadInstancesApiControllerIntegrationTest.java rename to src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/XRoadInstancesApiControllerIntegrationTest.java index edb2cba5f8..3d9955d080 100644 --- a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/XroadInstancesApiControllerIntegrationTest.java +++ b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/openapi/XRoadInstancesApiControllerIntegrationTest.java @@ -27,6 +27,7 @@ import org.junit.Before; import org.junit.Test; +import org.niis.xroad.securityserver.restapi.openapi.model.XRoadInstanceDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -42,26 +43,30 @@ /** * test xroad instances api controller */ -public class XroadInstancesApiControllerIntegrationTest extends AbstractApiControllerTestContext { +public class XRoadInstancesApiControllerIntegrationTest extends AbstractApiControllerTestContext { @Autowired - XroadInstancesApiController xroadInstancesApiController; + XRoadInstancesApiController xRoadInstancesApiController; private static final String INSTANCE_A = "instance_a"; private static final String INSTANCE_B = "instance_b"; private static final String INSTANCE_C = "instance_c"; + private static final XRoadInstanceDto INSTANCE_A_DTO = new XRoadInstanceDto(INSTANCE_A, true); + private static final XRoadInstanceDto INSTANCE_B_DTO = new XRoadInstanceDto(INSTANCE_B, false); + private static final XRoadInstanceDto INSTANCE_C_DTO = new XRoadInstanceDto(INSTANCE_C, false); private static final Set INSTANCE_IDS = new HashSet<>(Arrays.asList(INSTANCE_A, INSTANCE_B, INSTANCE_C)); @Before public void setup() { when(globalConfProvider.getInstanceIdentifiers()).thenReturn(INSTANCE_IDS); + when(globalConfProvider.getInstanceIdentifier()).thenReturn(INSTANCE_A); } @Test @WithMockUser(authorities = {"VIEW_XROAD_INSTANCES"}) - public void getMemberClassesForInstance() { - ResponseEntity> response = xroadInstancesApiController.getXroadInstances(); + public void getXRoadInstances() { + ResponseEntity> response = xRoadInstancesApiController.getXRoadInstances(); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(INSTANCE_IDS, response.getBody()); + assertEquals(Set.of(INSTANCE_A_DTO, INSTANCE_B_DTO, INSTANCE_C_DTO), response.getBody()); } } diff --git a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionServiceTest.java b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionServiceTest.java index a6aeb00798..b483fee294 100644 --- a/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionServiceTest.java +++ b/src/security-server/admin-service/application/src/test/java/org/niis/xroad/securityserver/restapi/service/DiagnosticConnectionServiceTest.java @@ -27,11 +27,26 @@ import ee.ria.xroad.common.CodedException; import ee.ria.xroad.common.DiagnosticStatus; - +import ee.ria.xroad.common.identifier.ClientId; +import ee.ria.xroad.common.identifier.SecurityServerId; + +import org.apache.http.Header; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.niis.xroad.common.core.dto.DownloadUrlConnectionStatus; import org.niis.xroad.common.core.exception.ErrorCode; @@ -39,7 +54,9 @@ import org.niis.xroad.common.core.exception.ExceptionCategory; import org.niis.xroad.common.core.exception.XrdRuntimeExceptionBuilder; import org.niis.xroad.globalconf.GlobalConfProvider; +import org.niis.xroad.securityserver.restapi.dto.ServiceProtocolType; import org.niis.xroad.securityserver.restapi.util.AuthCertVerifier; +import org.niis.xroad.serverconf.ServerConfProvider; import org.niis.xroad.signer.api.dto.CertificateInfo; import org.niis.xroad.signer.api.dto.KeyInfo; import org.niis.xroad.signer.api.dto.TokenInfo; @@ -57,15 +74,21 @@ import static ee.ria.xroad.common.ErrorCodes.X_INTERNAL_ERROR; import static ee.ria.xroad.common.ErrorCodes.X_INVALID_REQUEST; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class DiagnosticConnectionServiceTest { + private static final ClientId CLIENT_ID = ClientId.Conf.create("DEV", "COM", "4321"); + private static final ClientId TARGET_CLIENT_ID = ClientId.Conf.create("DEV", "COM", "1234", "MANAGEMENT"); + private static final SecurityServerId SECURITY_SERVER_ID = SecurityServerId.Conf.create("DEV", "COM", "1234", "SS0"); + @Mock GlobalConfProvider globalConfProvider; @Mock @@ -74,12 +97,15 @@ class DiagnosticConnectionServiceTest { AuthCertVerifier authCertVerifier; @Mock ManagementRequestSenderService managementRequestSenderService; + @Mock + ServerConfProvider serverConfProvider; DiagnosticConnectionService service; @BeforeEach void setUp() { - service = new DiagnosticConnectionService(globalConfProvider, tokenService, authCertVerifier, managementRequestSenderService); + service = new DiagnosticConnectionService(globalConfProvider, tokenService, authCertVerifier, managementRequestSenderService, + serverConfProvider); } @Test @@ -109,13 +135,15 @@ protected URLConnection openConnection(URL u) { @Test void getGlobalConfStatusThenReturnUnknownHostErrors() { - when(globalConfProvider.findSourceAddresses()) - .thenReturn(Set.of("unknown-host")); + when(globalConfProvider.getSourceAddresses(globalConfProvider.getInstanceIdentifier())).thenReturn(Set.of("unknown-host")); + when(globalConfProvider.getAllowedFederationInstances()).thenReturn(Set.of("FED")); + when(globalConfProvider.getSourceAddresses("FED")).thenReturn(Set.of("fed-unknown-host")); + when(globalConfProvider.getConfigurationDirectoryPath("FED")).thenReturn("FED/conf"); var statuses = service.getGlobalConfStatus(); assertThat(statuses) - .hasSize(2) + .hasSize(4) .extracting( DownloadUrlConnectionStatus::getDownloadUrl, s -> s.getConnectionStatus().getStatus(), @@ -123,7 +151,9 @@ void getGlobalConfStatusThenReturnUnknownHostErrors() { ) .containsExactlyInAnyOrder( tuple("http://unknown-host:80/internalconf", DiagnosticStatus.ERROR, "unknown_host"), - tuple("https://unknown-host:443/internalconf", DiagnosticStatus.ERROR, "unknown_host") + tuple("https://unknown-host:443/internalconf", DiagnosticStatus.ERROR, "unknown_host"), + tuple("http://fed-unknown-host:80/FED/conf", DiagnosticStatus.ERROR, "unknown_host"), + tuple("https://fed-unknown-host:443/FED/conf", DiagnosticStatus.ERROR, "unknown_host") ); } @@ -292,4 +322,108 @@ void getAuthCertReqStatusThenReturnCertificateNotFoundError() { assertThat(status.getErrorMetadata()).isEqualTo(List.of("No auth cert found")); assertThat(status.getValidationErrors()).isEmpty(); } + + @Test + void getOtherSecurityServerStatusWithRestThenReturnHttp200() throws Exception { + CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + + when(statusLine.getStatusCode()).thenReturn(200); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(httpClient.execute(any(HttpGet.class))).thenReturn(httpResponse); + + try (MockedStatic httpClientsMock = org.mockito.Mockito.mockStatic(HttpClients.class)) { + HttpClientBuilder builder = mock(HttpClientBuilder.class); + httpClientsMock.when(HttpClients::custom).thenReturn(builder); + + doReturn(builder).when(builder).setSSLSocketFactory(any()); + doReturn(builder).when(builder).setDefaultRequestConfig(any()); + doReturn(builder).when(builder).disableAutomaticRetries(); + doReturn(httpClient).when(builder).build(); + + var status = service.getOtherSecurityServerStatus(ServiceProtocolType.REST, CLIENT_ID, TARGET_CLIENT_ID, SECURITY_SERVER_ID); + + assertThat(status.getStatus()).isEqualTo(DiagnosticStatus.OK); + assertThat(status.getErrorCode()).isNull(); + assertThat(status.getErrorMetadata()).isEmpty(); + } + } + + @Test + void getOtherSecurityServerStatusWithSoapThenReturnHttp200() throws Exception { + CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse closeableHttpResponse = mock(CloseableHttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + + when(statusLine.getStatusCode()).thenReturn(200); + when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine); + when(closeableHttpResponse.getEntity()).thenReturn(new StringEntity(getMockSoapResponse(), ContentType.TEXT_XML)); + + Header contentTypeHeader = new BasicHeader("Content-Type", "text/xml; charset=UTF-8"); + when(closeableHttpResponse.getAllHeaders()).thenReturn(new Header[] {contentTypeHeader}); + + when(httpClient.execute(any(HttpUriRequest.class), any(HttpContext.class))).thenReturn(closeableHttpResponse); + + try (MockedStatic httpClientsMock = org.mockito.Mockito.mockStatic(HttpClients.class)) { + HttpClientBuilder builder = mock(HttpClientBuilder.class); + httpClientsMock.when(HttpClients::custom).thenReturn(builder); + + doReturn(builder).when(builder).setSSLSocketFactory(any()); + doReturn(builder).when(builder).setDefaultRequestConfig(any()); + doReturn(builder).when(builder).disableAutomaticRetries(); + doReturn(httpClient).when(builder).build(); + + var status = service.getOtherSecurityServerStatus(ServiceProtocolType.SOAP, CLIENT_ID, TARGET_CLIENT_ID, SECURITY_SERVER_ID); + + assertThat(status.getStatus()).isEqualTo(DiagnosticStatus.OK); + assertThat(status.getErrorCode()).isNull(); + assertThat(status.getErrorMetadata()).isEmpty(); + } + } + + @Test + void getOtherSecurityServerStatusWithWrongTypeThenReturnIllegalStateException() { + assertThatThrownBy(() -> + service.getOtherSecurityServerStatus(null, CLIENT_ID, TARGET_CLIENT_ID, SECURITY_SERVER_ID) + ) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unsupported protocol type: null"); + } + + private static String getMockSoapResponse() { + return """ + + + + DEV + COM + 4321 + SUBSYSTEM + + + DEV + COM + 1234 + MANAGEMENT + listMethods + + + DEV + COM + 1234 + SS0 + + 12345 + 4.0 + + + + + + """; + } } diff --git a/src/security-server/admin-service/ui/src/locales/en.json b/src/security-server/admin-service/ui/src/locales/en.json index 12864527ca..f3ca75f75f 100644 --- a/src/security-server/admin-service/ui/src/locales/en.json +++ b/src/security-server/admin-service/ui/src/locales/en.json @@ -336,11 +336,24 @@ "ok": "Everything ok", "test": "Test", "centralServer": { - "title": "Central Server", + "authCertRequest": "Authentication certificate registration service", "globalConf": "Global configuration download", "globalConfHttp": "http", "globalConfHttps": "https", - "authCertRequest": "Authentication certificate registration service" + "title": "Central Server" + }, + "management": { + "title": "Management Security Server" + }, + "securityServer": { + "client": "Client", + "sourceClient": "Source Client", + "rest": "REST", + "securityServer": "Security Server", + "soap": "SOAP", + "title": "Other Security Server", + "target": "Target", + "targetClient": "Target Client" } } }, diff --git a/src/security-server/admin-service/ui/src/locales/es.json b/src/security-server/admin-service/ui/src/locales/es.json index e6c4208739..326e16ac54 100644 --- a/src/security-server/admin-service/ui/src/locales/es.json +++ b/src/security-server/admin-service/ui/src/locales/es.json @@ -285,11 +285,24 @@ "ok": "Ok", "test": "Prueba", "centralServer": { - "title": "Servidor Central", + "authCertRequest": "Servicio de registro de certificados de autenticación", "globalConf": "Descarga de configuración global", "globalConfHttp": "http", "globalConfHttps": "https", - "authCertRequest": "Servicio de registro de certificados de autenticación" + "title": "Servidor Central" + }, + "management": { + "title": "Servidor de Seguridad de Gestión" + }, + "securityServer": { + "client": "Cliente", + "sourceClient": "Cliente de origen", + "rest": "REST", + "securityServer": "Servidor de Seguridad", + "soap": "SOAP", + "title": "Otro Servidor de Seguridad", + "target": "Destino", + "targetClient": "Cliente destino" } } }, diff --git a/src/security-server/admin-service/ui/src/locales/et.json b/src/security-server/admin-service/ui/src/locales/et.json index b86e3d519f..ca0d2fe601 100644 --- a/src/security-server/admin-service/ui/src/locales/et.json +++ b/src/security-server/admin-service/ui/src/locales/et.json @@ -331,11 +331,24 @@ "ok": "Kõik on korras", "test": "Test", "centralServer": { - "title": "Keskserver", + "authCertRequest": "Autentimise sertifikaadi registreerimise teenus", "globalConf": "Globaalse konfiguratsiooni allalaadimine", "globalConfHttp": "http", "globalConfHttps": "https", - "authCertRequest": "Autentimise sertifikaadid registreerimise teenus" + "title": "Keskserver" + }, + "management": { + "title": "Haldusteenuse pakkuja" + }, + "securityServer": { + "client": "Klient", + "sourceClient": "Lähteklient", + "rest": "REST", + "securityServer": "Turvaserver", + "soap": "SOAP", + "title": "Teine turvaserver", + "target": "Teine klient", + "targetClient": "Klient" } } }, diff --git a/src/security-server/admin-service/ui/src/locales/pt-BR.json b/src/security-server/admin-service/ui/src/locales/pt-BR.json index 4e71557ce0..ec4447646a 100644 --- a/src/security-server/admin-service/ui/src/locales/pt-BR.json +++ b/src/security-server/admin-service/ui/src/locales/pt-BR.json @@ -331,11 +331,24 @@ "ok": "Tudo certo", "test": "Teste", "centralServer": { - "title": "Servidor Central", + "authCertRequest": "Serviço de registro de certificado de autenticação", "globalConf": "Download da configuração global", "globalConfHttp": "http", "globalConfHttps": "https", - "authCertRequest": "Serviço de registro de certificado de autenticação" + "title": "Servidor Central" + }, + "management": { + "title": "Servidor de Segurança de Gerenciamento" + }, + "securityServer": { + "client": "Cliente", + "sourceClient": "Cliente de origem", + "rest": "REST", + "securityServer": "Servidor de Segurança", + "soap": "SOAP", + "title": "Outro Servidor de Segurança", + "target": "Destino", + "targetClient": "Cliente de destino" } } }, diff --git a/src/security-server/admin-service/ui/src/locales/ru.json b/src/security-server/admin-service/ui/src/locales/ru.json index 1c9c821c1d..48b6ef5aa9 100644 --- a/src/security-server/admin-service/ui/src/locales/ru.json +++ b/src/security-server/admin-service/ui/src/locales/ru.json @@ -260,11 +260,24 @@ "ok": "Все в порядке", "test": "Тест", "centralServer": { - "title": "Центральный сервер", + "authCertRequest": "Сервис регистрации сертификатов аутентификации", "globalConf": "Загрузка глобальной конфигурации", "globalConfHttp": "http", "globalConfHttps": "https", - "authCertRequest": "Сервис регистрации сертификатов аутентификации" + "title": "Центральный сервер" + }, + "management": { + "title": "Управляющий сервер безопасности" + }, + "securityServer": { + "client": "Клиент", + "sourceClient": "Исходный клиент", + "rest": "REST", + "securityServer": "Сервер безопасности", + "soap": "SOAP", + "title": "Другой сервер безопасности", + "target": "Цель", + "targetClient": "Целевой клиент" } } }, diff --git a/src/security-server/admin-service/ui/src/locales/tk.json b/src/security-server/admin-service/ui/src/locales/tk.json index 7d09357d08..830dc3c3b7 100644 --- a/src/security-server/admin-service/ui/src/locales/tk.json +++ b/src/security-server/admin-service/ui/src/locales/tk.json @@ -260,11 +260,24 @@ "ok": "Her şey yolunda", "test": "Test", "centralServer": { - "title": "Merkezi Sunucu", + "authCertRequest": "Kimlik doğrulama sertifikası kayıt servisi", "globalConf": "Küresel yapılandırma indirme", "globalConfHttp": "http", "globalConfHttps": "https", - "authCertRequest": "Kimlik doğrulama sertifikası kayıt servisi" + "title": "Merkezi Sunucu" + }, + "management": { + "title": "Yönetim Güvenlik Sunucusu" + }, + "securityServer": { + "client": "İstemci", + "sourceClient": "Kaynak İstemci", + "rest": "REST", + "securityServer": "Güvenlik Sunucusu", + "soap": "SOAP", + "title": "Diğer Güvenlik Sunucusu", + "target": "Hedef", + "targetClient": "Hedef İstemci" } } }, diff --git a/src/security-server/admin-service/ui/src/store/modules/client.ts b/src/security-server/admin-service/ui/src/store/modules/client.ts index 396b10938b..538ece5b09 100644 --- a/src/security-server/admin-service/ui/src/store/modules/client.ts +++ b/src/security-server/admin-service/ui/src/store/modules/client.ts @@ -27,13 +27,14 @@ import { defineStore } from 'pinia'; import * as api from '@/util/api'; import { encodePathParameter } from '@/util/api'; -import { CertificateDetails, Client, TokenCertificate } from '@/openapi-types'; +import { CertificateDetails, Client, SecurityServer, TokenCertificate } from '@/openapi-types'; export interface ClientState { client: Client | null; signCertificates: TokenCertificate[]; connection_type: string | null; tlsCertificates: CertificateDetails[]; + securityServers: SecurityServer[]; ssCertificate: CertificateDetails | null; clientLoading: boolean; } @@ -45,6 +46,7 @@ export const useClient = defineStore('client', { signCertificates: [], connection_type: null, tlsCertificates: [], + securityServers: [], ssCertificate: null, clientLoading: false, }; @@ -125,6 +127,23 @@ export const useClient = defineStore('client', { }); }, + async fetchSecurityServers(id: string) { + if (!id) { + throw new Error('Missing id'); + } + + return api + .get( + `/clients/${encodePathParameter(id)}/security-servers`, + ) + .then((res) => { + this.securityServers = res.data; + }) + .catch((error) => { + throw error; + }); + }, + async saveConnectionType(params: { clientId: string; connType: string }) { return api .patch(`/clients/${encodePathParameter(params.clientId)}`, { diff --git a/src/security-server/admin-service/ui/src/store/modules/clients.ts b/src/security-server/admin-service/ui/src/store/modules/clients.ts index 7a572b62f1..d8a0172c08 100644 --- a/src/security-server/admin-service/ui/src/store/modules/clients.ts +++ b/src/security-server/admin-service/ui/src/store/modules/clients.ts @@ -41,6 +41,7 @@ export interface ClientsState { members: ExtendedClient[]; // all local members, virtual and real realMembers: ExtendedClient[]; // local actual real members, owner +1 subsystems: ExtendedClient[]; + allSubsystems: Client[]; } export const useClients = defineStore('clients', { @@ -53,6 +54,7 @@ export const useClients = defineStore('clients', { members: [], subsystems: [], realMembers: [], + allSubsystems: [] as Client[], }; }, getters: { @@ -60,7 +62,7 @@ export const useClients = defineStore('clients', { }, actions: { - fetchClients() { + async fetchClients() { this.clientsLoading = true; return api @@ -75,6 +77,23 @@ export const useClients = defineStore('clients', { this.clientsLoading = false; }); }, + async fetchAllSubsystems(instance: string) { + return api + .get('/clients', { + params: { + instance: instance, + show_members: false, + internal_search: false, + include_management_service_check: true, + } + }) + .then((res) => { + this.allSubsystems = res.data; + }) + .catch((error) => { + throw error; + }); + }, storeClients(clients: Client[]) { this.clients = clients; diff --git a/src/security-server/admin-service/ui/src/store/modules/diagnostics.ts b/src/security-server/admin-service/ui/src/store/modules/diagnostics.ts index 3eb8fcd696..234a79de58 100644 --- a/src/security-server/admin-service/ui/src/store/modules/diagnostics.ts +++ b/src/security-server/admin-service/ui/src/store/modules/diagnostics.ts @@ -48,6 +48,7 @@ export interface DiagnosticsState { proxyMemoryUsageStatus?: ProxyMemoryUsageStatus; authCertReqStatus?: ConnectionStatus; globalConfStatuses: GlobalConfConnectionStatus[]; + otherSecurityServerStatus?: ConnectionStatus; } export const useDiagnostics = defineStore('diagnostics', { @@ -62,6 +63,7 @@ export const useDiagnostics = defineStore('diagnostics', { proxyMemoryUsageStatus: undefined, authCertReqStatus: undefined, globalConfStatuses: [], + otherSecurityServerStatus: undefined, }; }, persist: { @@ -134,6 +136,21 @@ export const useDiagnostics = defineStore('diagnostics', { this.authCertReqStatus = res.data; }); }, + async fetchOtherSecurityServerStatus(protocolType: string, clientId: string, targetClientId: string, + securityServerId: string) { + return api + .get('/diagnostics/other-security-server-status', { + params: { + protocol_type: protocolType, + client_id: clientId, + target_client_id: targetClientId, + security_server_id: securityServerId + } + }) + .then((res) => { + this.otherSecurityServerStatus = res.data; + }); + }, async fetchGlobalConfStatuses() { return api .get('/diagnostics/global-conf-status') diff --git a/src/security-server/admin-service/ui/src/store/modules/general.ts b/src/security-server/admin-service/ui/src/store/modules/general.ts index 9134e3d4f7..065138eae3 100644 --- a/src/security-server/admin-service/ui/src/store/modules/general.ts +++ b/src/security-server/admin-service/ui/src/store/modules/general.ts @@ -26,12 +26,13 @@ import { defineStore } from 'pinia'; import * as api from '@/util/api'; -import { MemberName } from '@/openapi-types'; +import { MemberName, XRoadInstance } from '@/openapi-types'; export const useGeneral = defineStore('general', { state: () => { return { - xroadInstances: [] as string[], + xRoadInstances: [] as XRoadInstance[], + xRoadInstanceIdentifiers: [] as string[], memberClasses: [] as string[], memberClassesCurrentInstance: [] as string[], memberName: '' as string, @@ -75,11 +76,14 @@ export const useGeneral = defineStore('general', { }); }, - fetchXroadInstances() { + fetchXRoadInstances() { return api .get('/xroad-instances') .then((res) => { - this.xroadInstances = res.data as string[]; + this.xRoadInstances = res.data as XRoadInstance[]; + this.xRoadInstanceIdentifiers = this.xRoadInstances.map( + (instance) => instance.identifier + ); }) .catch((error) => { throw error; diff --git a/src/security-server/admin-service/ui/src/util/formatting.ts b/src/security-server/admin-service/ui/src/util/formatting.ts new file mode 100644 index 0000000000..f3fae3fd88 --- /dev/null +++ b/src/security-server/admin-service/ui/src/util/formatting.ts @@ -0,0 +1,76 @@ +/* + * The MIT License + * Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS) + * Copyright (c) 2018 Estonian Information System Authority (RIA), + * Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK) + * Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +import { + type CodeWithDetails, +} from '@/openapi-types'; +import { i18n } from "@niis/shared-ui"; + + +export function formatErrorForUi(err?: CodeWithDetails): string { + if (!err) return ''; + + const { code, metadata = [], validation_errors = {} } = err; + + const buildKey = (rawKey?: string): string => { + if (!rawKey) return ''; + return rawKey.includes('.') ? rawKey : `error_code.${rawKey}`; + }; + + const t = (key: string) => i18n.global.t(key) as string; + + const codeKey = buildKey(code); + const codeText = codeKey ? t(codeKey) : ''; + const metaText = metadata.length ? metadata.join(', ') : ''; + const header = [codeText, metaText].filter(Boolean).join(' - '); + + const veEntries = Object.entries(validation_errors); + const veText = veEntries.length + ? veEntries + .map(([field, msgs]) => { + const labelKey = buildKey(field); + const translated = labelKey ? t(labelKey) : ''; + const label = translated || field; + return `${label}: ${msgs.join(', ')}`; + }) + .join(' | ') + : ''; + + return [header, veText].filter(Boolean).join(' | '); +} + +export function statusIconType(status: string | undefined): string { + if (!status) { + return 'progress-register'; + } + switch (status) { + case 'OK': + return 'ok'; + case 'FAIL': + return 'error'; + default: + return 'progress-register'; + } +} diff --git a/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/CentralServerConnectionTestingView.vue b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionCentralServerView.vue similarity index 76% rename from src/security-server/admin-service/ui/src/views/Diagnostics/Connection/CentralServerConnectionTestingView.vue rename to src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionCentralServerView.vue index 34486bb0c2..348cc7f8a6 100644 --- a/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/CentralServerConnectionTestingView.vue +++ b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionCentralServerView.vue @@ -45,7 +45,7 @@ v-for="(item, index) in globalConfStatuses" :key="item.download_url" > - + {{ $t('diagnostics.connection.centralServer.globalConf') }} @@ -53,12 +53,12 @@ {{ item.download_url }} - + - + @@ -73,6 +73,7 @@ large variant="text" @click="testGlobalConfDownload()" + data-test="central-server-global-conf-test-button" > {{ $t('diagnostics.connection.test') }} @@ -82,18 +83,18 @@ :colspan="5" :loading="globalConfLoading" :data="globalConfStatuses" - :no-items-text="$t('noData.noTimestampingServices')" + :no-items-text="$t('noData.noData')" /> - + {{ $t('diagnostics.connection.centralServer.authCertRequest') }} - + - + @@ -108,6 +109,7 @@ large variant="text" @click="testAuthCertRequest()" + data-test="central-server-auth-cert-test-button" > {{ $t('diagnostics.connection.test') }} @@ -117,7 +119,7 @@ :colspan="5" :loading="authCertLoading" :data="authCertReqStatus" - :no-items-text="$t('noData.noTimestampingServices')" + :no-items-text="$t('noData.noData')" /> @@ -125,11 +127,12 @@ @@ -268,10 +229,4 @@ export default defineComponent({ max-width: 50%; word-break: break-word; } - -.level-column { - @media only screen and (min-width: 1200px) { - width: 20%; - } -} diff --git a/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionContainer.vue b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionContainer.vue index f958e3f202..c019a908d5 100644 --- a/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionContainer.vue +++ b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionContainer.vue @@ -28,18 +28,69 @@ + + + + diff --git a/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionManagementView.vue b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionManagementView.vue new file mode 100644 index 0000000000..03d3de6f94 --- /dev/null +++ b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionManagementView.vue @@ -0,0 +1,278 @@ + + + + + + diff --git a/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionSecurityServerView.vue b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionSecurityServerView.vue new file mode 100644 index 0000000000..c4a43d1007 --- /dev/null +++ b/src/security-server/admin-service/ui/src/views/Diagnostics/Connection/ConnectionSecurityServerView.vue @@ -0,0 +1,280 @@ + + + + + + diff --git a/src/security-server/admin-service/ui/src/views/Diagnostics/DiagnosticsTabs.vue b/src/security-server/admin-service/ui/src/views/Diagnostics/DiagnosticsTabs.vue index 0271057cf7..b4246e82a1 100644 --- a/src/security-server/admin-service/ui/src/views/Diagnostics/DiagnosticsTabs.vue +++ b/src/security-server/admin-service/ui/src/views/Diagnostics/DiagnosticsTabs.vue @@ -59,7 +59,7 @@ function getAllowedTabs(): Tab[] { permissions: [Permissions.DIAGNOSTICS], }, { - key: 'diagnostics-connection-tab-button', + key: 'diagnostics-connection-testing-tab-button', name: 'tab.diagnostics.connectionTesting', to: { name: RouteName.DiagnosticsConnection, diff --git a/src/security-server/admin-service/ui/src/views/LocalGroup/AddMembersDialog.vue b/src/security-server/admin-service/ui/src/views/LocalGroup/AddMembersDialog.vue index 4c36b95202..800f6273bd 100644 --- a/src/security-server/admin-service/ui/src/views/LocalGroup/AddMembersDialog.vue +++ b/src/security-server/admin-service/ui/src/views/LocalGroup/AddMembersDialog.vue @@ -67,7 +67,7 @@ data-test="select-member-instance" hide-details clearable - :items="xroadInstances" + :items="xRoadInstanceIdentifiers" :label="$t('general.instance')" > @@ -219,19 +219,19 @@ export default defineComponent({ return { ...initialState() }; }, computed: { - ...mapState(useGeneral, ['xroadInstances', 'memberClasses']), + ...mapState(useGeneral, ['xRoadInstanceIdentifiers', 'memberClasses']), canSave(): boolean { return this.selectedIds.length > 0; }, }, created() { - this.fetchXroadInstances(); + this.fetchXRoadInstances(); this.fetchMemberClasses(); }, methods: { ...mapActions(useNotifications, ['showError']), - ...mapActions(useGeneral, ['fetchMemberClasses', 'fetchXroadInstances']), + ...mapActions(useGeneral, ['fetchMemberClasses', 'fetchXRoadInstances']), checkboxChange(id: string, event: boolean): void { if (event) { this.selectedIds.push(id); diff --git a/src/security-server/admin-service/ui/src/views/Service/AccessRightsDialog.vue b/src/security-server/admin-service/ui/src/views/Service/AccessRightsDialog.vue index 0e1cdd3094..6e8775a0cc 100644 --- a/src/security-server/admin-service/ui/src/views/Service/AccessRightsDialog.vue +++ b/src/security-server/admin-service/ui/src/views/Service/AccessRightsDialog.vue @@ -65,7 +65,7 @@ 0; }, @@ -275,12 +275,12 @@ export default defineComponent({ }, }, created() { - this.fetchXroadInstances(); + this.fetchXRoadInstances(); this.fetchMemberClasses(); }, methods: { ...mapActions(useNotifications, ['showError']), - ...mapActions(useGeneral, ['fetchMemberClasses', 'fetchXroadInstances']), + ...mapActions(useGeneral, ['fetchMemberClasses', 'fetchXRoadInstances']), checkboxChange(subject: ServiceClient, event: boolean): void { if (event) { this.selectedIds.push(subject); diff --git a/src/security-server/openapi-model/src/main/resources/META-INF/openapi-definition.yaml b/src/security-server/openapi-model/src/main/resources/META-INF/openapi-definition.yaml index e9ba5d274f..246b91a6d3 100644 --- a/src/security-server/openapi-model/src/main/resources/META-INF/openapi-definition.yaml +++ b/src/security-server/openapi-model/src/main/resources/META-INF/openapi-definition.yaml @@ -51,8 +51,8 @@ tags: description: token certificates endpoints - name: tokens description: tokens endpoints - - name: xroad-instances - description: xroad-instances endpoints + - name: x-road-instances + description: x-road-instances endpoints paths: /backups: get: @@ -843,6 +843,13 @@ paths: schema: type: boolean default: false + - in: query + name: include_management_service_check + description: add info, is this subsystem management service provider or not + required: false + schema: + type: boolean + default: false responses: '200': description: list of clients @@ -1236,6 +1243,46 @@ paths: examples: error_metadata_response: $ref: '#/components/examples/ErrorWithMetadataExample' + /clients/{id}/security-servers: + get: + tags: + - clients + summary: get all clients security servers + operationId: getClientSecurityServers + description:

Administrator views the details of clients security servers.

+ parameters: + - in: path + name: id + description: id of the client + required: true + schema: + type: string + format: text + minLength: 1 + maxLength: 1023 + responses: + '200': + description: list of SecurityServer objects + content: + application/json: + schema: + type: array + uniqueItems: true + description: array of SecurityServer objects + items: + $ref: '#/components/schemas/SecurityServer' + '400': + description: request was invalid + '401': + description: authentication credentials are missing + '403': + description: request has been refused + '404': + description: resource requested does not exists + '406': + description: request specified an invalid format + '500': + description: internal server error /clients/{id}/service-clients: get: tags: @@ -2445,6 +2492,67 @@ paths: description: request specified an invalid format '500': description: internal server error + /diagnostics/other-security-server-status: + get: + tags: + - diagnostics + summary: view other security server connection status + operationId: getOtherSecurityServerStatus + description:

Administrator views the other security server connection status.

+ parameters: + - in: query + name: protocol_type + description: service protocol type, must be REST or SOAP + required: true + schema: + type: string + enum: + - REST + - SOAP + example: REST + - in: query + name: client_id + description: clientId of the current security server. :::. Subsystem code is optional. + example: FI:GOV:123:Subsystem + required: true + schema: + type: string + format: text + - in: query + name: target_client_id + description: targetClientId of the other security server. :::. + example: FI:GOV:123:Subsystem + required: true + schema: + type: string + format: text + - in: query + name: security_server_id + description: securityServerId of the other security server. :::. + example: FI:GOV:123:DEV + required: true + schema: + type: string + format: text + responses: + '200': + description: other security server connection status + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionStatus' + '400': + description: request was invalid + '401': + description: authentication credentials are missing + '403': + description: request has been refused + '404': + description: resource requested does not exists + '406': + description: request specified an invalid format + '500': + description: internal server error /diagnostics/operational-monitoring: get: tags: @@ -5702,24 +5810,21 @@ paths: /xroad-instances: get: tags: - - xroad-instances - summary: get list of known xroad instance identifiers - operationId: getXroadInstances - description:

Administrator lists xroad instance identifiers

+ - x-road-instances + summary: get list of known x-road instance identifiers + operationId: getXRoadInstances + description:

Administrator lists x-road instance identifiers

responses: '200': - description: xroad instance identifiers + description: x-road instance identifiers content: application/json: schema: - type: array - description: array of xroad instance identifiers - uniqueItems: true - items: - type: string - format: text - minLength: 1 - maxLength: 255 + type: array + uniqueItems: true + description: array of x-road instance identifiers + items: + $ref: '#/components/schemas/XRoadInstance' '400': description: request was invalid '401': @@ -6111,6 +6216,24 @@ components: description: download url of the global configuration connection_status: $ref: '#/components/schemas/ConnectionStatus' + XRoadInstance: + type: object + description: x-road instance + required: + - identifier + - local + properties: + identifier: + type: string + readOnly: true + format: text + minLength: 1 + maxLength: 255 + description: x-road instance identifier + local: + type: boolean + readOnly: true + description: is this the local x-road instance MailNotificationStatus: type: object description: automatic e-mail notification status diff --git a/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/ConnectionTestingStepDefs.java b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/ConnectionTestingStepDefs.java new file mode 100644 index 0000000000..9304b35af1 --- /dev/null +++ b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/ConnectionTestingStepDefs.java @@ -0,0 +1,114 @@ +/* + * The MIT License + * + * Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS) + * Copyright (c) 2018 Estonian Information System Authority (RIA), + * Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK) + * Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.niis.xroad.ss.test.ui.glue; + +import com.codeborne.selenide.Condition; +import io.cucumber.java.en.Step; +import org.niis.xroad.ss.test.ui.page.ConnectionTestingPageObj; + +import static com.codeborne.selenide.Condition.enabled; +import static com.codeborne.selenide.Condition.visible; +import static org.niis.xroad.common.test.ui.utils.VuetifyHelper.vRadio; + +public class ConnectionTestingStepDefs extends BaseUiStepDefs { + private final ConnectionTestingPageObj page = new ConnectionTestingPageObj(); + + @Step("Global configuration download from {string} status should be failed") + public void centralServerGlobalConfMessage(String url) { + page.centralServerGlobalConfMessage(url).shouldNotHave(Condition.partialText("Everything ok")); + } + + @Step("Global configuration download Test button should be enabled") + public void centralServerGlobalConfTestButton() { + page.centralServerGlobalConfTestButton().shouldBe(enabled); + } + + @Step("Central Server authentication certificate registration service status should be failed") + public void centralServerAuthCertStatusFailed() { + page.centralServerAuthCertMessage().shouldHave(Condition.partialText("IO error")); + } + + @Step("Central Server authentication certificate registration service Test button should be enabled") + public void centralServerAuthCertTestButton() { + page.centralServerAuthCertTestButton().shouldBe(enabled); + } + + @Step("Run test for Management Security Server") + public void managementServerTest() { + page.managementServerTestButton().shouldBe(enabled).click(); + } + + @Step("Management Security Server error message should contain {}") + public void managementServerErrorContains(String message) { + page.managementServerStatusMessage().shouldHave(Condition.partialText(message)); + } + + @Step("Other Security Server Test button should be {}") + public void otherSecurityServerTestButton(String state) { + switch (state) { + case "enabled" -> page.otherSecurityServerTestButton().shouldBe(enabled); + case "disabled" -> page.otherSecurityServerTestButton().shouldBe(Condition.disabled); + default -> throw new IllegalArgumentException("State [" + state + "] is not supported"); + } + } + + @Step("Current client is set to {}") + public void selectedClient(String clientId) { + page.filter.selectedClient().click().selectCombobox(clientId); + } + + @Step("Service type is set to REST") + public void selectServiceType() { + page.radioRestPath().shouldBe(visible, enabled).click(); + vRadio(page.radioSoapPath()).shouldBeUnChecked(); + } + + @Step("Target instance is prefilled with {}") + public void targetInstance(String instance) { + page.selectedTargetInstanceInput().shouldHave(Condition.value(instance)); + } + + @Step("Target client is set to {}") + public void selectedTargetClient(String clientId) { + page.filter.selectedTargetClient().click().selectCombobox(clientId); + } + + @Step("Target security server is prefilled with {}") + public void securityServerId(String server) { + page.selectedSecurityServerIdInput().shouldHave(Condition.value(server)); + } + + @Step("Run test for Other Security Server") + public void otherSecurityServerTest() { + page.otherSecurityServerTestButton().shouldBe(enabled).click(); + } + + @Step("Other Security Server error message should contain {}") + public void otherServerStatusVisible(String message) { + page.otherSecurityServerStatusMessage().shouldHave(Condition.partialText(message)); + } +} diff --git a/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/NavigationStepDefs.java b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/NavigationStepDefs.java index a4083a8407..05aa0895f7 100644 --- a/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/NavigationStepDefs.java +++ b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/glue/NavigationStepDefs.java @@ -74,4 +74,9 @@ public void apiKeysSubTabIsSelected() { public void trafficSubTabIsSelected() { commonPageObj.subMenu.trafficTab().click(); } + + @Step("Connection Testing sub-tab is selected") + public void connectionTestingSubTabIsSelected() { + commonPageObj.subMenu.connectionTestingTab().click(); + } } diff --git a/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/CommonPageObj.java b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/CommonPageObj.java index 781a2862eb..070a6fd50a 100644 --- a/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/CommonPageObj.java +++ b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/CommonPageObj.java @@ -97,6 +97,10 @@ public SelenideElement trafficTab() { return $x("//*[@data-test='diagnostics-traffic-tab-button']"); } + public SelenideElement connectionTestingTab() { + return $x("//*[@data-test='diagnostics-connection-testing-tab-button']"); + } + } public class Form { diff --git a/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/ConnectionTestingPageObj.java b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/ConnectionTestingPageObj.java new file mode 100644 index 0000000000..492906a0fd --- /dev/null +++ b/src/security-server/system-test/src/intTest/java/org/niis/xroad/ss/test/ui/page/ConnectionTestingPageObj.java @@ -0,0 +1,96 @@ +/* + * The MIT License + * + * Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS) + * Copyright (c) 2018 Estonian Information System Authority (RIA), + * Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK) + * Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.niis.xroad.ss.test.ui.page; + +import com.codeborne.selenide.SelenideElement; +import org.niis.xroad.common.test.ui.utils.VuetifyHelper; + +import static com.codeborne.selenide.Selenide.$x; +import static org.niis.xroad.common.test.ui.utils.VuetifyHelper.vSelect; + +public class ConnectionTestingPageObj { + public final Filter filter = new Filter(); + + public SelenideElement centralServerGlobalConfMessage(String url) { + var xpath = "//tr[td/span[contains(text(),'%s')]]/td[@data-test='central-server-global-conf-message']"; + return $x(xpath.formatted(url)); + } + + public SelenideElement centralServerGlobalConfTestButton() { + return $x("//button[@data-test='central-server-global-conf-test-button']"); + } + + public SelenideElement centralServerAuthCertMessage() { + return $x("//td[@data-test='central-server-auth-cert-message']"); + } + + public SelenideElement centralServerAuthCertTestButton() { + return $x("//button[@data-test='central-server-auth-cert-test-button']"); + } + + public SelenideElement managementServerTestButton() { + return $x("//button[@data-test='management-server-test-button']"); + } + + public SelenideElement managementServerStatusMessage() { + return $x("//div[@data-test='management-server-status-message']"); + } + + public SelenideElement otherSecurityServerTestButton() { + return $x("//button[@data-test='other-security-server-test-button']"); + } + + public SelenideElement otherSecurityServerStatusMessage() { + return $x("//div[@data-test='other-security-server-status-message']"); + } + + public SelenideElement radioRestPath() { + return $x("//div[@data-test='other-security-server-rest-radio-button']"); + } + + public SelenideElement radioSoapPath() { + return $x("//div[@data-test='other-security-server-soap-radio-button']"); + } + + public SelenideElement selectedTargetInstanceInput() { + return $x("//div[@data-test='other-security-server-target-instance']//input"); + } + + public SelenideElement selectedSecurityServerIdInput() { + return $x("//div[@data-test='other-security-server-id']//input"); + } + + public static class Filter { + public VuetifyHelper.Select selectedClient() { + return vSelect($x("//div[@data-test='other-security-server-client-id']")); + } + + public VuetifyHelper.Select selectedTargetClient() { + return vSelect($x("//div[@data-test='other-security-server-target-client-id']")); + } + } +} diff --git a/src/security-server/system-test/src/intTest/resources/behavior/01-ui/0920-ss-diagnostics-connection-testing.feature b/src/security-server/system-test/src/intTest/resources/behavior/01-ui/0920-ss-diagnostics-connection-testing.feature new file mode 100644 index 0000000000..094254f5cd --- /dev/null +++ b/src/security-server/system-test/src/intTest/resources/behavior/01-ui/0920-ss-diagnostics-connection-testing.feature @@ -0,0 +1,32 @@ +@SecurityServer +@Diagnostics +Feature: 0920 - SS:Diagnostics - Connection Testing + + Background: + Given SecurityServer login page is open + And Page is prepared to be tested + And User xrd logs in to SecurityServer with password secret + And Diagnostics tab is selected + And Connection Testing sub-tab is selected + + Scenario: Central Server connection check tests should run + Given Global configuration download from "http://cs:80/internalconf" status should be failed + And Global configuration download from "https://cs:443/internalconf" status should be failed + And Global configuration download Test button should be enabled + And Central Server authentication certificate registration service status should be failed + And Central Server authentication certificate registration service Test button should be enabled + + Scenario: Other Security Server connection test can be run + Given Other Security Server Test button should be disabled + When Current client is set to DEV:COM:1234 + And Service type is set to REST + Then Target instance is prefilled with DEV + When Target client is set to DEV:COM:1234:MANAGEMENT + Then Target security server is prefilled with SS0 + And Other Security Server Test button should be enabled + When Run test for Other Security Server + Then Other Security Server error message should contain server.clientproxy.io_error + + Scenario: Management Security Server test fails because member is unknown + When Run test for Management Security Server + Then Management Security Server error message should contain server.serverproxy.service_failed.unknown_member diff --git a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClient.java b/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClient.java index ffe5b208d3..28339f6a9f 100644 --- a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClient.java +++ b/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClient.java @@ -40,6 +40,7 @@ import org.niis.xroad.globalconf.model.ConfigurationUtils; import org.niis.xroad.globalconf.model.ParametersProviderFactory; import org.niis.xroad.globalconf.model.PrivateParameters; +import org.niis.xroad.globalconf.util.FederationConfigurationSourceFilter; import java.nio.file.Files; import java.nio.file.Path; @@ -99,7 +100,7 @@ public synchronized DownloadResult execute() throws Exception { var configurationSources = getAdditionalConfigurationSources(); FederationConfigurationSourceFilter sourceFilter = - new FederationConfigurationSourceFilterImpl(configurationAnchor.getInstanceIdentifier()); + new FederationConfigurationSourceFilter(configurationAnchor.getInstanceIdentifier()); deleteExtraConfigurationDirectories(configurationSources, sourceFilter); diff --git a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClientCLI.java b/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClientCLI.java index aa564461b6..35ccea3649 100644 --- a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClientCLI.java +++ b/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationClientCLI.java @@ -33,6 +33,7 @@ import org.apache.commons.lang3.StringUtils; import org.niis.xroad.globalconf.model.ConfigurationAnchor; import org.niis.xroad.globalconf.model.ConfigurationSource; +import org.niis.xroad.globalconf.util.FederationConfigurationSourceFilter; import java.nio.file.Path; import java.util.Collections; diff --git a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtils.java b/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtils.java index 0fe30a5e07..5ab2478d66 100644 --- a/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtils.java +++ b/src/service/configuration-client/configuration-client-core/src/main/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtils.java @@ -26,6 +26,7 @@ package org.niis.xroad.confclient.core; import org.niis.xroad.globalconf.model.ConfigurationLocation; +import org.niis.xroad.globalconf.util.GlobalConfUtils; import java.util.ArrayList; import java.util.Collections; @@ -43,7 +44,7 @@ private ConfigurationDownloadUtils() { public static List shuffleLocationsPreferHttps(List locations) { List urls = new ArrayList<>(getLocationUrls(locations)); List httpsUrls = urls.stream() - .filter(location -> startWithHttpAndNotWithHttps(location.getDownloadURL())) + .filter(location -> GlobalConfUtils.startWithHttpAndNotWithHttps(location.getDownloadURL())) .map(location -> new ConfigurationLocation( location.getInstanceIdentifier(), location.getDownloadURL().replaceFirst(HTTP, HTTPS), @@ -66,8 +67,4 @@ private static List getLocationUrls(List get(ConfigurationSource source) { List locations = new ArrayList<>(); try { - String configurationDirectory = getConfigurationDirectory(source); + String configurationDirectory = GlobalConfUtils.getConfigurationDirectory(source); List sharedParametersConfigurationSources = getSharedParametersConfigurationSources(source.getInstanceIdentifier()); locations = sharedParametersConfigurationSources.stream() @@ -89,19 +86,6 @@ private String getDownloadUrl(String domainAddress, String configurationDirector return String.format("%s://%s/%s", HTTPS, domainAddress, configurationDirectory); } - private String getConfigurationDirectory(ConfigurationSource source) { - var firstHttpDownloadUrl = source.getLocations().stream() - .map(ConfigurationLocation::getDownloadURL) - .filter(ConfigurationDownloadUtils::startWithHttpAndNotWithHttps).findFirst(); - if (firstHttpDownloadUrl.isPresent()) { - Matcher matcher = CONF_PATTERN.matcher(firstHttpDownloadUrl.get()); - if (matcher.find()) { - return firstHttpDownloadUrl.get().substring(matcher.end()); - } - } - throw new ConflictException(INVALID_DOWNLOAD_URL_FORMAT.build()); - } - private List getVerificationCerts(String confLocation, SharedParameters.ConfigurationSource confSource) { if (isInternalConfiguration(confLocation)) { return confSource.getInternalVerificationCerts(); diff --git a/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtilsTest.java b/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtilsTest.java index 87b91d6060..5d2357deff 100644 --- a/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtilsTest.java +++ b/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/ConfigurationDownloadUtilsTest.java @@ -26,6 +26,7 @@ package org.niis.xroad.confclient.core; import org.junit.jupiter.api.Test; +import org.niis.xroad.globalconf.util.GlobalConfUtils; import java.util.ArrayList; import java.util.List; @@ -68,6 +69,6 @@ void shuffleLocationsPreferHttps() { @Test void withHttpsReturnStartWithHttpAndNotWithHttpsFalse() { - assertFalse(ConfigurationDownloadUtils.startWithHttpAndNotWithHttps("https://")); + assertFalse(GlobalConfUtils.startWithHttpAndNotWithHttps("https://")); } } diff --git a/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterImplTest.java b/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterTest.java similarity index 92% rename from src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterImplTest.java rename to src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterTest.java index a26c71127b..c5fcf739cf 100644 --- a/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterImplTest.java +++ b/src/service/configuration-client/configuration-client-core/src/test/java/org/niis/xroad/confclient/core/FederationConfigurationSourceFilterTest.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.niis.xroad.globalconf.util.FederationConfigurationSourceFilter; import static ee.ria.xroad.common.SystemProperties.AllowedFederationMode.ALL; import static ee.ria.xroad.common.SystemProperties.AllowedFederationMode.NONE; @@ -38,7 +39,7 @@ /** * Tests for {@link FederationConfigurationSourceFilter} */ -class FederationConfigurationSourceFilterImplTest { +class FederationConfigurationSourceFilterTest { private static final String FILTER_SEPARATOR = ","; @@ -63,7 +64,7 @@ static void restoreAllowedFederations() { @Test void shouldNotAllowAnyWhenNotConfigured() { System.clearProperty(SystemProperties.CONFIGURATION_CLIENT_ALLOWED_FEDERATIONS); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("EE")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); } @@ -72,7 +73,7 @@ void shouldNotAllowAnyWhenNotConfigured() { @Test void shouldNotAllowAnyWhenEmptyFilterList() { setFilter(" "); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("EE")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor("fi-prod")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); @@ -81,7 +82,7 @@ void shouldNotAllowAnyWhenEmptyFilterList() { @Test void shouldNotAllowAnyWhenConfigured() { setFilter(NONE.name()); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("JE")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor("fi-test")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); @@ -90,7 +91,7 @@ void shouldNotAllowAnyWhenConfigured() { @Test void shouldNotAllowAnyWhenConfiguredWithMixedCase() { setFilter("nOnE"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("test")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor("EE-test")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); @@ -99,7 +100,7 @@ void shouldNotAllowAnyWhenConfiguredWithMixedCase() { @Test void shouldNotAllowAnyWhenConfiguredWithNoneAndAllAndCustomInstances() { buildAndSetFilter("fi-prod", "aLL", "EE", "nOne"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("any")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor("does not matter")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor("EE")).isFalse(); @@ -110,7 +111,7 @@ void shouldNotAllowAnyWhenConfiguredWithNoneAndAllAndCustomInstances() { @Test void shouldAllowAllWhenConfigured() { setFilter(ALL.name()); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("EE")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("fi-prod")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); @@ -119,7 +120,7 @@ void shouldAllowAllWhenConfigured() { @Test void shouldAllowAllWhenConfiguredWithMixedCase() { setFilter("aLl"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("FI")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("fi-TEST")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); @@ -128,7 +129,7 @@ void shouldAllowAllWhenConfiguredWithMixedCase() { @Test void shouldAllowAllWhenConfiguredWithAllAndCustomInstances() { buildAndSetFilter("fi-prod", "aLL", "EE"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("any")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("does not matter")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor(DEFAULT_OWN_INSTANCE)).isTrue(); @@ -138,7 +139,7 @@ void shouldAllowAllWhenConfiguredWithAllAndCustomInstances() { @Test void shouldOnlyAllowCustomInstancesWhenConfiguredWithMixedCase() { buildAndSetFilter("fi-prod", "EE"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("FI-PROd")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("ee")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("EE-TEST")).isFalse(); @@ -149,7 +150,7 @@ void shouldOnlyAllowCustomInstancesWhenConfiguredWithMixedCase() { @Test void shouldAllowCustomInstances() { setFilter("fi-test,ee,some"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("FI-test")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("EE")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("some")).isTrue(); @@ -160,7 +161,7 @@ void shouldAllowCustomInstances() { @Test void shouldParseFilterWithExtraSpaces() { setFilter("fi-prOD , ee , fi-test, "); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("FI-PROd")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("EE")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("fi-TEST")).isTrue(); @@ -175,7 +176,7 @@ void shouldParseFilterWithExtraSpaces() { void shouldAlwaysAllowOwnInstance() { final String own = "fi-dev"; setFilter(NONE.name()); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(own); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(own); assertThat(filter.shouldDownloadConfigurationFor(" ")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor(" ")).isFalse(); assertThat(filter.shouldDownloadConfigurationFor("dev-fi")).isFalse(); @@ -186,7 +187,7 @@ void shouldAlwaysAllowOwnInstance() { @Test void shouldWorkWithSomeSpecialCharacters() { buildAndSetFilter("ää-ÖÖÖ", "èé-ãâ"); - FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilterImpl(DEFAULT_OWN_INSTANCE); + FederationConfigurationSourceFilter filter = new FederationConfigurationSourceFilter(DEFAULT_OWN_INSTANCE); assertThat(filter.shouldDownloadConfigurationFor("ÄÄ-ööö")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("ÈÉ-ÃÂ")).isTrue(); assertThat(filter.shouldDownloadConfigurationFor("dev-fi")).isFalse(); diff --git a/src/service/proxy/proxy-core/src/main/java/org/niis/xroad/proxy/core/util/MessageProcessorBase.java b/src/service/proxy/proxy-core/src/main/java/org/niis/xroad/proxy/core/util/MessageProcessorBase.java index 4e44736eb0..c4bcf10a14 100644 --- a/src/service/proxy/proxy-core/src/main/java/org/niis/xroad/proxy/core/util/MessageProcessorBase.java +++ b/src/service/proxy/proxy-core/src/main/java/org/niis/xroad/proxy/core/util/MessageProcessorBase.java @@ -172,7 +172,8 @@ protected void updateOpMonitoringDataByRestRequest(OpMonitoringData opMonitoring } private String getNormalizedServicePath(String servicePath) { - return Optional.of(UriUtils.uriPathPercentDecode(URI.create(servicePath).normalize().getRawPath(), true)) + return Optional.ofNullable(servicePath) + .map(UriUtils::decodeAndNormalize) .orElse(servicePath); } diff --git a/src/shared-ui/src/components/XrdEmptyPlaceholder.vue b/src/shared-ui/src/components/XrdEmptyPlaceholder.vue index 79b9152c11..4a37b83f99 100644 --- a/src/shared-ui/src/components/XrdEmptyPlaceholder.vue +++ b/src/shared-ui/src/components/XrdEmptyPlaceholder.vue @@ -27,9 +27,12 @@