diff options
Diffstat (limited to 'gui-server/src')
26 files changed, 1610 insertions, 0 deletions
diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/GuiServerApplication.java b/gui-server/src/main/java/org/onap/policy/gui/server/GuiServerApplication.java new file mode 100644 index 0000000..7da2dd6 --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/GuiServerApplication.java @@ -0,0 +1,33 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan(basePackages = "org.onap.policy.gui") +public class GuiServerApplication { + public static void main(String[] args) { + SpringApplication.run(GuiServerApplication.class, args); + } +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig.java b/gui-server/src/main/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig.java new file mode 100644 index 0000000..8d501d2 --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig.java @@ -0,0 +1,94 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import javax.annotation.PostConstruct; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustAllStrategy; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ClampRestTemplateConfig { + private static final Logger LOG = LoggerFactory.getLogger(ClampRestTemplateConfig.class); + + @Value("${clamp.disable-ssl-validation:false}") + private boolean disableSslValidation; + + @Value("${clamp.disable-ssl-hostname-check:false}") + private boolean disableSslHostnameCheck; + + @Value("${server.ssl.trust-store:#{null}}") + private Resource trustStore; + + @Value("${server.ssl.trust-store-password:#{null}}") + private char[] trustStorePassword; + + @PostConstruct + private void validateProperties() { + if (trustStore == null && !disableSslValidation) { + throw new IllegalArgumentException("server.ssl.trust-store must be set if SSL validation is enabled"); + } + if (disableSslValidation && !disableSslHostnameCheck) { + LOG.info("Disabling SSL hostname check as SSL validation is disabled"); + disableSslHostnameCheck = true; + } + } + + /** + * Returns a RestTemplate, optionally disabling SSL hostname check or disabling SSL validation entirely. + */ + @Bean + public RestTemplate clampRestTemplate() throws GeneralSecurityException, IOException { + SSLContext sslContext; + if (disableSslValidation) { + sslContext = new SSLContextBuilder().loadTrustMaterial(new TrustAllStrategy()).build(); + } else { + sslContext = new SSLContextBuilder().loadTrustMaterial(trustStore.getURL(), trustStorePassword).build(); + } + + HostnameVerifier hostnameVerifier; + if (disableSslHostnameCheck) { + hostnameVerifier = new NoopHostnameVerifier(); + } else { + hostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier(); + } + + var csf = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); + var httpClient = HttpClients.custom().setSSLSocketFactory(csf).build(); + var requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + return new RestTemplate(requestFactory); + } +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/config/FilterRegistrationConfig.java b/gui-server/src/main/java/org/onap/policy/gui/server/config/FilterRegistrationConfig.java new file mode 100644 index 0000000..3e62237 --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/config/FilterRegistrationConfig.java @@ -0,0 +1,42 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import org.onap.policy.gui.server.filters.ClientSslHeaderFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FilterRegistrationConfig { + + /** + * Registers ClientSslToHeaderFilter for /clamp/restservices/*. + */ + @Bean + public FilterRegistrationBean<ClientSslHeaderFilter> clientSslHeaderFilter() { + FilterRegistrationBean<ClientSslHeaderFilter> registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new ClientSslHeaderFilter()); + registrationBean.addUrlPatterns("/clamp/restservices/*"); + return registrationBean; + } + +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/config/StaticContentConfig.java b/gui-server/src/main/java/org/onap/policy/gui/server/config/StaticContentConfig.java new file mode 100644 index 0000000..479202d --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/config/StaticContentConfig.java @@ -0,0 +1,38 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class StaticContentConfig implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/clamp").setViewName("redirect:/clamp/"); + registry.addViewController("/clamp/").setViewName("forward:/clamp/index.html"); + registry.addViewController("/apex-editor").setViewName("redirect:/apex-editor/"); + registry.addViewController("/apex-editor/").setViewName("forward:/apex-editor/index.html"); + } + +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/filters/ClientSslHeaderFilter.java b/gui-server/src/main/java/org/onap/policy/gui/server/filters/ClientSslHeaderFilter.java new file mode 100644 index 0000000..db8f593 --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/filters/ClientSslHeaderFilter.java @@ -0,0 +1,146 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.filters; + +import static org.onap.policy.gui.server.util.X509CertificateEncoder.urlEncodeCert; + +import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Set; +import java.util.TreeSet; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Filter which encodes a client SSL certificate into X-SSL-Cert HTTP header. + * CLAMP has a corresponding filter called ClampCadiFilter which decodes the + * header. This is needed as CLAMP runtime uses AAF for auth, and AAF uses + * client cert authentication. Since REST requests from CLAMP GUI to CLAMP + * runtime are proxied in gui-server, the proxy needs to attach a copy of the + * client SSL cert, as the proxy could not know the client's private key. + */ +@Order(1) +public class ClientSslHeaderFilter extends OncePerRequestFilter { + private static final Logger LOG = LoggerFactory.getLogger(ClientSslHeaderFilter.class); + + // Name of attribute containing request SSL cert. + public static final String X509_ATTRIBUTE_NAME = "javax.servlet.request.X509Certificate"; + + // Name of header containing encoded SSL cert - also used in clamp's ClampCadiFilter. + public static final String SSL_CERT_HEADER_NAME = "X-SSL-Cert"; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + var wrappedRequest = new ClientSslHeaderRequestWrapper(request); + var certs = (X509Certificate[]) request.getAttribute(X509_ATTRIBUTE_NAME); + if (certs != null && certs.length > 0) { + try { + certs[0].checkValidity(); + wrappedRequest.setSslCertHeader(urlEncodeCert(certs[0])); + } catch (CertificateEncodingException e) { + LOG.error("Error encoding client SSL cert", e); + } catch (CertificateExpiredException | CertificateNotYetValidException e) { + LOG.info("Client SSL cert expired", e); + } + } + filterChain.doFilter(wrappedRequest, response); + } + + /* + * This class wraps a HttpServletRequest so that X-SSL-Cert header can be added. + */ + private static class ClientSslHeaderRequestWrapper extends HttpServletRequestWrapper { + private String encodedSslCert = null; + + public ClientSslHeaderRequestWrapper(HttpServletRequest request) { + super(request); + } + + public void setSslCertHeader(String encodedSslCert) { + this.encodedSslCert = encodedSslCert; + } + + /** + * Returns the value of the specified request header as a String. + * The header name is case insensitive. + */ + @Override + public String getHeader(String name) { + if (SSL_CERT_HEADER_NAME.equalsIgnoreCase(name)) { + return encodedSslCert; + } else { + return super.getHeader(name); + } + } + + /** + * Returns all the values of the specified request header as an Enumeration + * of String objects. + * Some headers, such as Accept-Language can be sent by clients as several + * headers each with a different value rather than sending the header as a + * comma separated list. The header name is case insensitive. + */ + @Override + public Enumeration<String> getHeaders(String name) { + if (SSL_CERT_HEADER_NAME.equalsIgnoreCase(name)) { + if (encodedSslCert != null) { + return Collections.enumeration(Collections.singletonList(encodedSslCert)); + } else { + return Collections.emptyEnumeration(); + } + } else { + return super.getHeaders(name); + } + } + + /** + * Returns an enumeration of all the header names this request contains. + * If the request has no headers, this method returns an empty enumeration. + */ + @Override + public Enumeration<String> getHeaderNames() { + Set<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + names.addAll(Collections.list(super.getHeaderNames())); + if (encodedSslCert != null) { + names.add(SSL_CERT_HEADER_NAME); + } else { + // This is needed to prevent an exploit where a user passes their own + // X-SSL-Cert header, possibly bypassing client cert verification. + names.remove(SSL_CERT_HEADER_NAME); + } + return Collections.enumeration(names); + } + } +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/rest/ApexEditorRestController.java b/gui-server/src/main/java/org/onap/policy/gui/server/rest/ApexEditorRestController.java new file mode 100644 index 0000000..a4b92ef --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/rest/ApexEditorRestController.java @@ -0,0 +1,41 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.rest; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +@RestController +@RequestMapping("/apex-editor/policy/gui/v*/apex/editor") +public class ApexEditorRestController { + + /** + * Strip /apex-editor prefix from Apex Editor rest calls. + */ + @RequestMapping("/**") + public ModelAndView forwardApexEditorRest(ModelMap model, HttpServletRequest request) { + String targetUrl = request.getRequestURI().replaceFirst("^/apex-editor", ""); + return new ModelAndView("forward:" + targetUrl, model); + } +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/rest/ClampRestController.java b/gui-server/src/main/java/org/onap/policy/gui/server/rest/ClampRestController.java new file mode 100644 index 0000000..1975f37 --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/rest/ClampRestController.java @@ -0,0 +1,78 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.rest; + +import java.net.URI; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +@RequestMapping("/clamp/restservices") +public class ClampRestController { + + @Value("${clamp.url}") + private URI clampUrl; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + /** + * Proxy rest calls to clamp backend. + */ + @RequestMapping("/**") + public ResponseEntity<String> mirrorRest(@RequestBody(required = false) String body, + @RequestHeader HttpHeaders headers, + HttpMethod method, + HttpServletRequest request) { + // Strip /clamp/ prefix from request URI. + String requestUri = request.getRequestURI().replaceFirst("^/clamp/", ""); + URI uri = UriComponentsBuilder.fromUri(clampUrl) + .path(requestUri) + .query(request.getQueryString()) + .build(true).toUri(); + + HttpEntity<String> httpEntity = new HttpEntity<>(body, headers); + try { + return restTemplate.exchange(uri, method, httpEntity, String.class); + + } catch (HttpStatusCodeException e) { + // On error, return the backend error code instead of 500. + return ResponseEntity.status(e.getRawStatusCode()) + .headers(e.getResponseHeaders()) + .body(e.getResponseBodyAsString()); + } + } + +} diff --git a/gui-server/src/main/java/org/onap/policy/gui/server/util/X509CertificateEncoder.java b/gui-server/src/main/java/org/onap/policy/gui/server/util/X509CertificateEncoder.java new file mode 100644 index 0000000..67da719 --- /dev/null +++ b/gui-server/src/main/java/org/onap/policy/gui/server/util/X509CertificateEncoder.java @@ -0,0 +1,70 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.util; + +import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; + +/** + * Helper methods for encoding/decoding X509Certificates from PEM strings and URL-encoded PEM strings. + */ +public class X509CertificateEncoder { + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n"; + private static final String END_CERT = "\n-----END CERTIFICATE-----"; + + private X509CertificateEncoder() {} + + /** + * Returns a PEM string from an X509Certificate. + */ + public static String getPemFromCert(X509Certificate cert) throws CertificateEncodingException { + return BEGIN_CERT + Base64.getEncoder().encodeToString(cert.getEncoded()) + END_CERT; + } + + /** + * Returns an X509Certificate from a PEM string. + */ + public static X509Certificate getCertFromPem(String pem) throws CertificateException { + return (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(pem.getBytes())); + } + + /** + * Returns URL-encoded PEM string from an X509Certificate, suitable as a HTTP header. + */ + public static String urlEncodeCert(X509Certificate cert) throws CertificateEncodingException { + return URLEncoder.encode(getPemFromCert(cert), StandardCharsets.UTF_8); + } + + /** + * Returns an X509Certificate from a URL-encoded PEM string. + */ + public static X509Certificate urlDecodeCert(String encodedPem) throws CertificateException { + return getCertFromPem(URLDecoder.decode(encodedPem, StandardCharsets.UTF_8)); + } +} diff --git a/gui-server/src/main/resources/static/index.html b/gui-server/src/main/resources/static/index.html new file mode 100644 index 0000000..3b079a8 --- /dev/null +++ b/gui-server/src/main/resources/static/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>ONAP Policy GUI</title> +</head> +<body> +<ul> + <li><a href="/apex-editor/">Apex Policy Editor</a></li> + <li><a href="/clamp/">CLAMP Designer UI</a></li> +</ul> +</body> +</html> diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/SpringContextTest.java b/gui-server/src/test/java/org/onap/policy/gui/server/SpringContextTest.java new file mode 100644 index 0000000..7be7694 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/SpringContextTest.java @@ -0,0 +1,37 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + properties = { + "clamp.url=https://clamp-backend:8443/", + "clamp.disable-ssl-validation=true" + }) +class SpringContextTest { + + @Test + @SuppressWarnings("java:S2699") + void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig1Test.java b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig1Test.java new file mode 100644 index 0000000..44e4c46 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig1Test.java @@ -0,0 +1,69 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.net.ssl.SSLPeerUnverifiedException; +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.hello.HelloWorldApplication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +/** + * In this test, SSL validation and hostname check are enabled. + * Since our keystore cert has a hostname 'helloworld' and our test request is + * to localhost, the request will fail with an SSLPeerUnverifiedException, as + * the SSL cert name does not match the server name 'localhost'. + */ +@SpringBootTest( + classes = { HelloWorldApplication.class, ClampRestTemplateConfig.class }, + properties = { + "server.ssl.key-store=file:src/test/resources/helloworld-keystore.jks", + "server.ssl.key-store-password=changeit", + "server.ssl.trust-store=file:src/test/resources/helloworld-truststore.jks", + "server.ssl.trust-store-password=changeit", + "clamp.disable-ssl-validation=false", + "clamp.disable-ssl-hostname-check=false" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ClampRestTemplateConfig1Test { + + @LocalServerPort + private int port; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + @Test + void testRequestFailsWhenSslHostnameCheckIsEnabled() { + var helloUrl = "https://localhost:" + port + "/"; + Exception e = assertThrows(RestClientException.class, + () -> restTemplate.getForEntity(helloUrl, String.class)); + assertTrue(e.getCause() instanceof SSLPeerUnverifiedException); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig2Test.java b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig2Test.java new file mode 100644 index 0000000..b8e744c --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig2Test.java @@ -0,0 +1,61 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.onap.policy.gui.server.test.util.hello.HelloWorldRestController.HELLO_WORLD_STRING; + +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.hello.HelloWorldApplication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.client.RestTemplate; + +/** + * In this test, SSL validation is disabled. + * The test request should succeed. A trust store has not been supplied in this case. + */ +@SpringBootTest( + classes = { HelloWorldApplication.class, ClampRestTemplateConfig.class }, + properties = { + "server.ssl.key-store=file:src/test/resources/helloworld-keystore.jks", + "server.ssl.key-store-password=changeit", + "clamp.disable-ssl-validation=true" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ClampRestTemplateConfig2Test { + + @LocalServerPort + private int port; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + @Test + void testRequestSucceedsWhenSslValidationIsDisabled() { + var helloUrl = "https://localhost:" + port + "/"; + String response = restTemplate.getForObject(helloUrl, String.class); + assertEquals(HELLO_WORLD_STRING, response); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig3Test.java b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig3Test.java new file mode 100644 index 0000000..4636982 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig3Test.java @@ -0,0 +1,70 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.onap.policy.gui.server.test.util.hello.HelloWorldRestController.HELLO_WORLD_STRING; + +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.hello.HelloWorldApplication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.client.RestTemplate; + +/** + * In this test, SSL validation is enabled but hostname check is disabled. + * Even though our keystore cert has a hostname 'helloworld' and our test + * request is to localhost, the request will succeed as the SSL hostname check + * is disabled. + */ +@SpringBootTest( + classes = { HelloWorldApplication.class, ClampRestTemplateConfig.class }, + properties = { + "server.ssl.key-store=file:src/test/resources/helloworld-keystore.jks", + "server.ssl.key-store-password=changeit", + "server.ssl.trust-store=file:src/test/resources/helloworld-truststore.jks", + "server.ssl.trust-store-password=changeit", + "clamp.disable-ssl-validation=false", + "clamp.disable-ssl-hostname-check=true" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ClampRestTemplateConfig3Test { + + @LocalServerPort + private int port; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + /* + * In this test, the request will succeed even though the SSL cert name + * does not match 'localhost', as SSL hostname verification is disabled. + */ + @Test + void testRequestSucceedsWhenSslHostnameCheckIsDisabled() { + var helloUrl = "https://localhost:" + port + "/"; + String response = restTemplate.getForObject(helloUrl, String.class); + assertEquals(HELLO_WORLD_STRING, response); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig4Test.java b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig4Test.java new file mode 100644 index 0000000..f0f222f --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig4Test.java @@ -0,0 +1,67 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.onap.policy.gui.server.test.util.hello.HelloWorldRestController.HELLO_WORLD_STRING; + +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.hello.HelloWorldApplication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.client.RestTemplate; + +/** + * In this test, SSL validation is disabled but hostname check is explicitly + * enabled. The expected behaviour is to disable the hostname check if SSL + * validation is disabled. We expect the request to succeed even though the + * SSL cert name does not match 'localhost', as SSL hostname verification is + * implicitly disabled. + */ +@SpringBootTest( + classes = { HelloWorldApplication.class, ClampRestTemplateConfig.class }, + properties = { + "server.ssl.key-store=file:src/test/resources/helloworld-keystore.jks", + "server.ssl.key-store-password=changeit", + "server.ssl.trust-store=file:src/test/resources/helloworld-truststore.jks", + "server.ssl.trust-store-password=changeit", + "clamp.disable-ssl-validation=true", + "clamp.disable-ssl-hostname-check=false" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ClampRestTemplateConfig4Test { + + @LocalServerPort + private int port; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + @Test + void testHostnameCheckIsDisabledWhenSslValidationIsDisabled() { + var helloUrl = "https://localhost:" + port + "/"; + String response = restTemplate.getForObject(helloUrl, String.class); + assertEquals(HELLO_WORLD_STRING, response); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig5Test.java b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig5Test.java new file mode 100644 index 0000000..cc23de5 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/config/ClampRestTemplateConfig5Test.java @@ -0,0 +1,69 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.config; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.net.ssl.SSLPeerUnverifiedException; +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.hello.HelloWorldApplication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +/** + * In this test, we verify that SSL validation and hostname check are enabled + * by default. Thus we do not explicitly set the Spring properties + * clamp.disable-ssl-validation and clamp.disable-ssl-hostname-check. + * Since our keystore cert has a hostname 'helloworld' and our test request is + * to localhost, the request will fail with an SSLPeerUnverifiedException, as + * the SSL cert name does not match the server name 'localhost'. + */ +@SpringBootTest( + classes = { HelloWorldApplication.class, ClampRestTemplateConfig.class }, + properties = { + "server.ssl.key-store=file:src/test/resources/helloworld-keystore.jks", + "server.ssl.key-store-password=changeit", + "server.ssl.trust-store=file:src/test/resources/helloworld-truststore.jks", + "server.ssl.trust-store-password=changeit", + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ClampRestTemplateConfig5Test { + + @LocalServerPort + private int port; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + @Test + void testSslValidationIsEnabledByDefault() { + var helloUrl = "https://localhost:" + port + "/"; + Exception e = assertThrows(RestClientException.class, + () -> restTemplate.getForEntity(helloUrl, String.class)); + assertTrue(e.getCause() instanceof SSLPeerUnverifiedException); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/filters/ClientSslHeaderFilterTest.java b/gui-server/src/test/java/org/onap/policy/gui/server/filters/ClientSslHeaderFilterTest.java new file mode 100644 index 0000000..5fc026d --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/filters/ClientSslHeaderFilterTest.java @@ -0,0 +1,211 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.filters; + +import static org.apache.commons.collections4.CollectionUtils.isEqualCollection; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.onap.policy.gui.server.filters.ClientSslHeaderFilter.SSL_CERT_HEADER_NAME; +import static org.onap.policy.gui.server.filters.ClientSslHeaderFilter.X509_ATTRIBUTE_NAME; +import static org.onap.policy.gui.server.util.X509CertificateEncoder.urlDecodeCert; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.onap.policy.gui.server.test.util.KeyStoreHelper; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +class ClientSslHeaderFilterTest { + + /* + * If the client does not supply an SSL cert, the filter should not set + * the X-SSL-Cert header. + */ + @Test + void testNoClientCert_noHeader() throws ServletException, IOException { + // Create a request without client SSL cert. + HttpServletRequest inRequest = new MockHttpServletRequest(); + + // Apply the filter. + HttpServletRequest outRequest = applyRequestFilter(inRequest); + + // The modified request should not contain cert header. + assertFalse(containsCertHeader(outRequest.getHeaderNames())); + assertNull(outRequest.getHeader(SSL_CERT_HEADER_NAME)); + assertEquals(Collections.emptyEnumeration(), outRequest.getHeaders(SSL_CERT_HEADER_NAME)); + } + + /* + * If the client does supply an SSL cert, the filter should set the + * X-SSL-Cert header with the encoded SSL cert. + */ + @Test + void testValidClientCert_hasHeader() throws Exception { + // Load valid cert from key store. + X509Certificate validCert = KeyStoreHelper.loadValidCert(); + + // Create a request with a valid client SSL cert. + MockHttpServletRequest inRequest = new MockHttpServletRequest(); + inRequest.setAttribute(X509_ATTRIBUTE_NAME, new X509Certificate[] { validCert }); + + // Apply the filter. + HttpServletRequest outRequest = applyRequestFilter(inRequest); + + // The modified request should contain a cert header. + assertTrue(containsCertHeader(outRequest.getHeaderNames())); + + // Check if the cert header parses back to the original cert. + String headerValue = outRequest.getHeader(SSL_CERT_HEADER_NAME); + assertEquals(validCert, urlDecodeCert(headerValue)); + + // Verify the getHeaders method also returns cert. + assertEquals(headerValue, outRequest.getHeaders(SSL_CERT_HEADER_NAME).nextElement()); + } + + /* + * If the client supplies an expired SSL cert, the filter should not set + * the X-SSL-Cert header. + */ + @Test + void testExpiredClientCert_noHeader() throws Exception { + // Load expired cert from key store. + X509Certificate expiredCert = KeyStoreHelper.loadExpiredCert(); + + // Create a request with an expired client SSL cert. + MockHttpServletRequest inRequest = new MockHttpServletRequest(); + inRequest.setAttribute(X509_ATTRIBUTE_NAME, new X509Certificate[] { expiredCert }); + + // Apply the filter. + HttpServletRequest outRequest = applyRequestFilter(inRequest); + + // The modified request should not contain a cert header. + assertFalse(containsCertHeader(outRequest.getHeaderNames())); + assertNull(outRequest.getHeader(SSL_CERT_HEADER_NAME)); + assertEquals(Collections.emptyEnumeration(), outRequest.getHeaders(SSL_CERT_HEADER_NAME)); + } + + /* + * This test is needed to prevent a security vulnerability where a + * malicious user does not authenticate using client cert, but defines the + * X-SSL-Cert header themselves, thus gaining access without having the + * corresponding private key. + * We thus test that an incoming X-SSL-Cert header is sanitized. + */ + @Test + void existingCertHeaderIsSanitized() throws Exception { + // Create a request with X-SSL-Cert header predefined. + MockHttpServletRequest inRequest = new MockHttpServletRequest(); + inRequest.addHeader(SSL_CERT_HEADER_NAME, "somevalue"); + + // Apply the filter. + HttpServletRequest outRequest = applyRequestFilter(inRequest); + + // The modified request should not contain a cert header. + assertFalse(containsCertHeader(outRequest.getHeaderNames())); + assertNull(outRequest.getHeader(SSL_CERT_HEADER_NAME)); + assertEquals(Collections.emptyEnumeration(), outRequest.getHeaders(SSL_CERT_HEADER_NAME)); + } + + /* + * This test verifies that existing HTTP headers are preserved + * (including multi-value headers). + */ + @Test + void otherHeadersAreStillAccessible() throws Exception { + // Load valid cert from key store. + X509Certificate validCert = KeyStoreHelper.loadValidCert(); + + // Create a request with a valid client SSL cert and some existing headers. + MockHttpServletRequest inRequest = new MockHttpServletRequest(); + inRequest.setAttribute(X509_ATTRIBUTE_NAME, new X509Certificate[] { validCert }); + inRequest.addHeader("User-Agent", "Jupiter"); + inRequest.addHeader("Accept-Language", "en-US"); + inRequest.addHeader("Accept-Language", "en-IE"); + + // Apply the filter. + HttpServletRequest outRequest = applyRequestFilter(inRequest); + + // The modified request contains the new cert header and the existing headers. + assertTrue( + isEqualCollection( + List.of("Accept-Language", "User-Agent", SSL_CERT_HEADER_NAME), + Collections.list(outRequest.getHeaderNames()))); + + // Verify getHeader method returns correct value. + String userAgent = outRequest.getHeader("User-Agent"); + assertEquals("Jupiter", userAgent); + + // Verify getHeaders method returns correct values. + Enumeration<String> acceptLanguages = outRequest.getHeaders("Accept-Language"); + assertEquals("en-US", acceptLanguages.nextElement()); + assertEquals("en-IE", acceptLanguages.nextElement()); + assertFalse(acceptLanguages.hasMoreElements()); + } + + /** + * Apply the ClientSslToHeaderFilter to the input request, + * and return the modified request. + */ + private HttpServletRequest applyRequestFilter(HttpServletRequest request) throws ServletException, IOException { + HttpServletResponse response = new MockHttpServletResponse(); + + // The filter calls filterChain::doFilter after processing the request, + // so capture the HttpServletRequest argument from filterChain::doFilter. + FilterChain filterChain = mock(FilterChain.class); + ArgumentCaptor<HttpServletRequest> requestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class); + doNothing().when(filterChain).doFilter(requestCaptor.capture(), eq(response)); + + // Apply the filter. + Filter filter = new ClientSslHeaderFilter(); + filter.doFilter(request, response, filterChain); + + // Return the modified HttpServletRequest. + return requestCaptor.getValue(); + } + + /** + * Check if an Enumeration of header names contains the certificate header. + * Note HTTP header names are case insensitive. + */ + private boolean containsCertHeader(Enumeration<String> headers) { + while (headers.hasMoreElements()) { + if (headers.nextElement().equalsIgnoreCase(SSL_CERT_HEADER_NAME)) { + return true; + } + } + return false; + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/rest/ApexEditorRestControllerTest.java b/gui-server/src/test/java/org/onap/policy/gui/server/rest/ApexEditorRestControllerTest.java new file mode 100644 index 0000000..4cfd994 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/rest/ApexEditorRestControllerTest.java @@ -0,0 +1,61 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.rest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest( + properties = { + "clamp.url=https://clamp-backend:8443/", + "clamp.disable-ssl-validation=true" + }) +@AutoConfigureMockMvc +class ApexEditorRestControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + void testStaticContentUrls() throws Exception { + mvc.perform(get("/apex-editor/")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/apex-editor/index.html")); + + mvc.perform(get("/apex-editor")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/apex-editor/")); + } + + @Test + void testApexEditorRestForwarding() throws Exception { + mvc.perform(get("/apex-editor/policy/gui/v1/apex/editor/-1/Session/Create")) + .andExpect(forwardedUrl("/policy/gui/v1/apex/editor/-1/Session/Create")); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/rest/ClampRestControllerTest.java b/gui-server/src/test/java/org/onap/policy/gui/server/rest/ClampRestControllerTest.java new file mode 100644 index 0000000..fb3e843 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/rest/ClampRestControllerTest.java @@ -0,0 +1,161 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.rest; + +import static org.onap.policy.gui.server.filters.ClientSslHeaderFilter.SSL_CERT_HEADER_NAME; +import static org.onap.policy.gui.server.test.util.X509RequestPostProcessor.x509; +import static org.onap.policy.gui.server.util.X509CertificateEncoder.urlEncodeCert; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.security.cert.X509Certificate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.KeyStoreHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest( + properties = { + "clamp.url=https://clamp-backend:8443/", + "clamp.disable-ssl-validation=true" + }) +@AutoConfigureMockMvc +class ClampRestControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + @Qualifier("clampRestTemplate") + private RestTemplate restTemplate; + + private MockRestServiceServer mockServer; + + @BeforeEach + public void init() { + mockServer = MockRestServiceServer.createServer(restTemplate); + } + + @Test + void testStaticContentUrls() throws Exception { + mvc.perform(get("/clamp/")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/clamp/index.html")); + + mvc.perform(get("/clamp")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/clamp/")); + } + + /* + * This is a happy path test to verify that calls to /clamp/restservices/** + * are relayed to the clamp backend, and that the backend receives the + * client certificate encoded in a header. More extensive tests of the + * certificate cert filter are in ClientSslHeaderFilterTest. + */ + @Test + void testClampProxyWithClientCert() throws Exception { + X509Certificate cert = KeyStoreHelper.loadValidCert(); + + mockServer.expect( + requestTo("https://clamp-backend:8443/restservices/junit/test")) + .andExpect(header(SSL_CERT_HEADER_NAME, urlEncodeCert(cert))) + .andRespond(withStatus(HttpStatus.OK).body("admin")); + + mvc.perform( + get("/clamp/restservices/junit/test") + .with(x509(cert))) + .andExpect(status().isOk()) + .andExpect(content().string("admin")); + + mockServer.verify(); + } + + /* + * This test verifies that HTTP headers are preserved for requests to the + * clamp backend (including multi-value headers). + */ + @Test + void verifyClampProxyPassesHeaders() throws Exception { + // Single value header + final String userAgent = "User-Agent"; + final String userAgentValue = "JUnit"; + // Multi value header + final String acceptLanguage = "Accept-Language"; + final String enUs = "en-US"; + final String enIe = "en-IE"; + + mockServer.expect( + requestTo("https://clamp-backend:8443/restservices/junit/test")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header(userAgent, userAgentValue)) + .andExpect(header(acceptLanguage, enUs, enIe)) + .andRespond(withStatus(HttpStatus.OK)); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set(userAgent, userAgentValue); + requestHeaders.add(acceptLanguage, enUs); + requestHeaders.add(acceptLanguage, enIe); + mvc.perform( + get("/clamp/restservices/junit/test") + .headers(requestHeaders)) + .andExpect(status().isOk()); + + mockServer.verify(); + } + + /* + * This test verifies that error messages from the clamp backend are + * delivered to the client (as opposed to 500 "Internal Server Error"). + */ + @Test + void verifyClampProxyReturnsBackendErrorCode() throws Exception { + final String errorMessage = "This appliance cannot brew coffee"; + + mockServer.expect( + requestTo("https://clamp-backend:8443/restservices/coffee")) + .andRespond(withStatus(HttpStatus.I_AM_A_TEAPOT).body(errorMessage)); + + mvc.perform( + post("/clamp/restservices/coffee")) + .andExpect(status().is(HttpStatus.I_AM_A_TEAPOT.value())) + .andExpect(content().string(errorMessage)); + + mockServer.verify(); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/test/util/KeyStoreHelper.java b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/KeyStoreHelper.java new file mode 100644 index 0000000..a4aabb8 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/KeyStoreHelper.java @@ -0,0 +1,92 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.test.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.X509Certificate; +import org.junit.jupiter.api.function.Executable; + +public class KeyStoreHelper { + /* + * The proxy test keystore contains certs with: + * - alias "valid": self-signed cert which expires circa year 2050 + * - alias "expired": self-signed cert which expired in year 2000 + */ + private static final String KEY_STORE_PATH = "src/test/resources/keystore-proxytest.jks"; + private static final String KEY_STORE_PASSWORD = "changeit"; + private static final String KEY_STORE_TYPE = "JKS"; + private static final String CERT_ALIAS_VALID = "valid"; + private static final String CERT_ALIAS_EXPIRED = "expired"; + + /** + * Load a valid certificate from the test keystore. + */ + public static X509Certificate loadValidCert() throws CouldNotLoadCertificateException { + X509Certificate cert = loadCertFromKeyStore(CERT_ALIAS_VALID); + assertDoesNotThrow((Executable) cert::checkValidity); + return cert; + } + + /** + * Load an expired certificate from the test keystore. + */ + public static X509Certificate loadExpiredCert() throws CouldNotLoadCertificateException { + X509Certificate cert = loadCertFromKeyStore(CERT_ALIAS_EXPIRED); + assertThrows(CertificateExpiredException.class, cert::checkValidity); + return cert; + } + + /** + * Load a certificate with given alias from the test keystore. + */ + private static X509Certificate loadCertFromKeyStore(String certAlias) throws CouldNotLoadCertificateException { + try { + KeyStore ks = KeyStore.getInstance(KEY_STORE_TYPE); + ks.load(new FileInputStream(KEY_STORE_PATH), KEY_STORE_PASSWORD.toCharArray()); + X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias); + if (cert == null) { + throw new CouldNotLoadCertificateException("Alias does not exist or does not contain a certificate."); + } + return cert; + } catch (Exception e) { + throw new CouldNotLoadCertificateException( + "Could not load cert with alias '" + certAlias + "' from test keystore.", e); + } + } + + /** + * Exception class for KeyStoreHelper methods. + */ + public static class CouldNotLoadCertificateException extends java.lang.Exception { + protected CouldNotLoadCertificateException(String errorMessage) { + super(errorMessage); + } + + protected CouldNotLoadCertificateException(String errorMessage, Throwable err) { + super(errorMessage, err); + } + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/test/util/X509RequestPostProcessor.java b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/X509RequestPostProcessor.java new file mode 100644 index 0000000..b1ef4a1 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/X509RequestPostProcessor.java @@ -0,0 +1,46 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.test.util; + +import java.security.cert.X509Certificate; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +/** + * X509RequestPostProcessor is a test helper class for use with Spring MockMvc. + * It allows setting X509 certificates to a MockHttpServletRequest. + */ +public final class X509RequestPostProcessor implements RequestPostProcessor { + private final X509Certificate[] certificates; + + public X509RequestPostProcessor(X509Certificate... certificates) { + this.certificates = certificates; + } + + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setAttribute("javax.servlet.request.X509Certificate", this.certificates); + return request; + } + + public static RequestPostProcessor x509(X509Certificate... certificates) { + return new X509RequestPostProcessor(certificates); + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/test/util/hello/HelloWorldApplication.java b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/hello/HelloWorldApplication.java new file mode 100644 index 0000000..e584665 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/hello/HelloWorldApplication.java @@ -0,0 +1,29 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.test.util.hello; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +// This class is used in tests for ClampRestTemplateConfig. +@SpringBootApplication +public class HelloWorldApplication { + +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/test/util/hello/HelloWorldRestController.java b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/hello/HelloWorldRestController.java new file mode 100644 index 0000000..71bd483 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/test/util/hello/HelloWorldRestController.java @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.test.util.hello; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +// This class is used in tests for ClampRestTemplateConfig. +@RestController +public class HelloWorldRestController { + public static final String HELLO_WORLD_STRING = "Hello, World"; + + @RequestMapping("/") + public String greeting() { + return HELLO_WORLD_STRING; + } +} diff --git a/gui-server/src/test/java/org/onap/policy/gui/server/util/X509CertificateEncoderTest.java b/gui-server/src/test/java/org/onap/policy/gui/server/util/X509CertificateEncoderTest.java new file mode 100644 index 0000000..28b1217 --- /dev/null +++ b/gui-server/src/test/java/org/onap/policy/gui/server/util/X509CertificateEncoderTest.java @@ -0,0 +1,47 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.gui.server.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import org.junit.jupiter.api.Test; +import org.onap.policy.gui.server.test.util.KeyStoreHelper; + +class X509CertificateEncoderTest { + + @Test + void testPemEncoder() throws KeyStoreHelper.CouldNotLoadCertificateException, CertificateException { + X509Certificate loadedCert = KeyStoreHelper.loadValidCert(); + String pem = X509CertificateEncoder.getPemFromCert(loadedCert); + X509Certificate certFromPem = X509CertificateEncoder.getCertFromPem(pem); + assertEquals(loadedCert, certFromPem); + } + + @Test + void testUrlEncoder() throws KeyStoreHelper.CouldNotLoadCertificateException, CertificateException { + X509Certificate loadedCert = KeyStoreHelper.loadValidCert(); + String encodedCert = X509CertificateEncoder.urlEncodeCert(loadedCert); + X509Certificate decodedCert = X509CertificateEncoder.urlDecodeCert(encodedCert); + assertEquals(loadedCert, decodedCert); + } +} diff --git a/gui-server/src/test/resources/helloworld-keystore.jks b/gui-server/src/test/resources/helloworld-keystore.jks Binary files differnew file mode 100644 index 0000000..c2f1a0e --- /dev/null +++ b/gui-server/src/test/resources/helloworld-keystore.jks diff --git a/gui-server/src/test/resources/helloworld-truststore.jks b/gui-server/src/test/resources/helloworld-truststore.jks Binary files differnew file mode 100644 index 0000000..64c123e --- /dev/null +++ b/gui-server/src/test/resources/helloworld-truststore.jks diff --git a/gui-server/src/test/resources/keystore-proxytest.jks b/gui-server/src/test/resources/keystore-proxytest.jks Binary files differnew file mode 100644 index 0000000..c261084 --- /dev/null +++ b/gui-server/src/test/resources/keystore-proxytest.jks |