From 8cd71d5d8f76b50c4653559a98337fc4c2cd40d4 Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Sat, 14 Nov 2026 13:55:58 -0500 Subject: [PATCH] feat: Third Party About page; About pages show product identity, not framework versions - Third Party: new public /about (AboutController + about.html on the shared GBA layout), linked from the footer, permitted anonymously. - Data Custodian and Third Party About pages now show the same fields: implementation, version, build time, the standard (NAESB REQ.21 ESPI 4.1) and a release status derived from the version (a SNAPSHOT is "Pre-release: not for production use"), plus links to GBA, the API docs, the source and SECURITY.md. - The Java and Spring Boot versions are no longer shown to anonymous visitors: exact framework versions help attackers pick known CVEs, and they stay available to admins via /actuator/info (#240). - Version and build time come from BuildProperties (spring-boot-maven-plugin:build-info, added to both apps) instead of the jar manifest, which is absent under spring-boot:run ("(development build)"). - Removed the legacy VersionRESTController (/about-version) from both apps. It read a servlet-context MANIFEST.MF that a Spring Boot jar does not have, and it filled a model key the DC template never read (DC) or rendered a template that did not exist (TP). - Tests: AboutControllerTest in both apps. GbaLayoutPagesTest now renders under the real /ThirdParty context path and asserts that every internal href/src carries it. With MockMvc's empty context path, a correct th:href="@{/about}" and a broken href="/about" rendered identically, so the previous #235 guard could not catch the bug. Co-Authored-By: Claude Opus 5.5 --- openespi-datacustodian/pom.xml | 9 ++ .../datacustodian/web/AboutController.java | 48 ++++++++-- .../web/VersionRESTController.java | 77 ---------------- .../src/main/resources/templates/about.html | 46 +++++++--- .../web/AboutControllerTest.java | 78 ++++++++++++++++ openespi-thirdparty/pom.xml | 9 ++ .../config/SecurityConfiguration.java | 5 +- .../espi/thirdparty/web/AboutController.java | 82 +++++++++++++++++ .../thirdparty/web/VersionRESTController.java | 72 --------------- .../src/main/resources/templates/about.html | 54 ++++++++++++ .../resources/templates/fragments/layout.html | 1 + .../thirdparty/web/AboutControllerTest.java | 88 +++++++++++++++++++ .../thirdparty/web/GbaLayoutPagesTest.java | 18 ++-- 13 files changed, 409 insertions(+), 178 deletions(-) delete mode 100644 openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java create mode 100644 openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java create mode 100644 openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java delete mode 100644 openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java create mode 100644 openespi-thirdparty/src/main/resources/templates/about.html create mode 100644 openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/AboutControllerTest.java diff --git a/openespi-datacustodian/pom.xml b/openespi-datacustodian/pom.xml index 19b93994..97c2f6bc 100644 --- a/openespi-datacustodian/pom.xml +++ b/openespi-datacustodian/pom.xml @@ -360,6 +360,15 @@ + + + + + build-info + + + diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java index 7565fd9b..b43d9cfb 100644 --- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java +++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java @@ -19,31 +19,63 @@ package org.greenbuttonalliance.espi.datacustodian.web; -import org.springframework.boot.SpringBootVersion; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.info.BuildProperties; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; /** - * Public "About" page (#175). Replaces the legacy about.jsp build-info table with a small, - * non-sensitive summary of the running implementation. Public (footer-linked from every page, - * including the anonymous login page). + * Public "About" page (#175), footer-linked from every page including the anonymous login page. + * + *

Shows only what identifies the product: name, version, build time, the ESPI standard it + * implements and its release status. It deliberately does NOT show the Java or Spring Boot version: + * exact framework versions help attackers pick known CVEs, so they stay behind the admin-only + * {@code /actuator/info} (#240). The version and build time come from {@link BuildProperties} + * ({@code spring-boot-maven-plugin:build-info}), which, unlike the jar manifest, is also present + * under {@code spring-boot:run}.

*/ @Controller public class AboutController { + static final String IMPLEMENTATION = "OpenESPI Green Button Data Custodian"; + static final String ESPI_STANDARD = "NAESB REQ.21 ESPI, version 4.1"; + + private static final DateTimeFormatter BUILD_TIME = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'").withZone(ZoneOffset.UTC); + + private final ObjectProvider buildProperties; + + public AboutController(ObjectProvider buildProperties) { + this.buildProperties = buildProperties; + } + @GetMapping("/about") public String about(Model model) { + BuildProperties build = buildProperties.getIfAvailable(); + String version = build != null ? build.getVersion() : null; + Map info = new LinkedHashMap<>(); - String version = AboutController.class.getPackage().getImplementationVersion(); - info.put("Implementation", "OpenESPI Green Button Data Custodian"); + info.put("Implementation", IMPLEMENTATION); info.put("Version", version != null ? version : "(development build)"); - info.put("Java version", System.getProperty("java.version", "—")); - info.put("Spring Boot version", SpringBootVersion.getVersion()); + if (build != null && build.getTime() != null) { + info.put("Build time", BUILD_TIME.format(build.getTime())); + } + info.put("Standard", ESPI_STANDARD); + info.put("Status", releaseStatus(version)); model.addAttribute("info", info); return "about"; } + + /** A version without a release (null or a SNAPSHOT) is pre-release and not for production use. */ + static String releaseStatus(String version) { + return version == null || version.endsWith("-SNAPSHOT") + ? "Pre-release: not for production use" + : "Release"; + } } diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java deleted file mode 100644 index affff259..00000000 --- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * - * Copyright (c) 2025-2026 Green Button Alliance, Inc. - * - * - * 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. - * - */ - -package org.greenbuttonalliance.espi.datacustodian.web; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.ServletContext; -import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.jar.Manifest; - -@Controller -// @RequestMapping - DISABLED -public class VersionRESTController extends BaseController { - - /** - * Handling GET request to retrieve details from MANIFEST.MF file - * - * @param request - * HTTP Servlet Request - * @param model - * ModelMap data - * @throws IOException - * Exception thrown by failed or interrupted I/O operations. - * - * @return implementation details - */ - @GetMapping("/about-version") - public String getBuildNumber(HttpServletRequest request, ModelMap model) - throws IOException { - - ServletContext context = request.getSession().getServletContext(); - InputStream manifestStream = context - .getResourceAsStream("/META-INF/MANIFEST.MF"); - Manifest manifest = new Manifest(manifestStream); - - Map aboutInfo = new HashMap<>(); - aboutInfo.put("Implementation-Vendor", manifest.getMainAttributes() - .getValue("Implementation-Vendor")); - aboutInfo.put("Implementation-Title", manifest.getMainAttributes() - .getValue("Implementation-Title")); - aboutInfo.put("Implementation-Version", manifest.getMainAttributes() - .getValue("Implementation-Version")); - aboutInfo.put("Implementation-Jdk", manifest.getMainAttributes() - .getValue("Build-Jdk")); - aboutInfo.put("Implementation-Build", manifest.getMainAttributes() - .getValue("Implementation-Build")); - aboutInfo.put("Implementation-Build-Time", manifest.getMainAttributes() - .getValue("Implementation-Build-Time")); - - model.put("aboutInfo", aboutInfo); - return "/about"; - - } - -} \ No newline at end of file diff --git a/openespi-datacustodian/src/main/resources/templates/about.html b/openespi-datacustodian/src/main/resources/templates/about.html index d2449459..2696c45a 100644 --- a/openespi-datacustodian/src/main/resources/templates/about.html +++ b/openespi-datacustodian/src/main/resources/templates/about.html @@ -8,25 +8,43 @@
-

About

-

+

About

+

A reference implementation of the NAESB ESPI (Green Button) Data Custodian, maintained by the - Green Button Alliance. + Green Button Alliance.

-
-
- - - - - - - -
Keyvalue
+
+
+
+

This installation

+
+ + + + + + + +
Keyvalue
+
+
-
+ +
diff --git a/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java new file mode 100644 index 00000000..82730f69 --- /dev/null +++ b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Green Button Alliance, Inc. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenbuttonalliance.espi.datacustodian.web; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootVersion; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * The About page is public, identifies the product and its release status, and does not disclose + * the Java or Spring Boot version to anonymous visitors. + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class AboutControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectProvider buildProperties; + + @Test + void about_is_public_and_shows_product_identity_without_framework_versions() throws Exception { + String html = mockMvc.perform(get("/about")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html) + .contains(AboutController.IMPLEMENTATION, AboutController.ESPI_STANDARD, "Status", "SECURITY.md") + .doesNotContain("Java version", System.getProperty("java.version")) + .doesNotContain("Spring Boot version", SpringBootVersion.getVersion()); + } + + @Test + void about_shows_the_build_version_when_build_info_is_present() throws Exception { + BuildProperties build = buildProperties.getIfAvailable(); + String html = mockMvc.perform(get("/about")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + // build-info.properties is generated by spring-boot-maven-plugin:build-info in a Maven build + assertThat(build).as("BuildProperties (spring-boot-maven-plugin:build-info)").isNotNull(); + assertThat(html).contains(build.getVersion(), AboutController.releaseStatus(build.getVersion()), "Build time"); + } + + @Test + void release_status_follows_the_version() { + assertThat(AboutController.releaseStatus("4.0.0-SNAPSHOT")).startsWith("Pre-release"); + assertThat(AboutController.releaseStatus(null)).startsWith("Pre-release"); + assertThat(AboutController.releaseStatus("4.0.0")).isEqualTo("Release"); + } +} diff --git a/openespi-thirdparty/pom.xml b/openespi-thirdparty/pom.xml index 40ff3cc9..f7fb6d8f 100644 --- a/openespi-thirdparty/pom.xml +++ b/openespi-thirdparty/pom.xml @@ -357,6 +357,15 @@ + + + + + build-info + + + diff --git a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java index aba8ba6d..99ab0937 100644 --- a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java +++ b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java @@ -58,8 +58,9 @@ public class SecurityConfiguration { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz - // Public pages (the Terms of Service and Usage Policy are linked from the navbar) - .requestMatchers("/", "/home", "/login", "/TermsOfService", "/UsagePolicy", "/error", + // Public pages (Terms of Service and Usage Policy are linked from the navbar, About from + // the footer) + .requestMatchers("/", "/home", "/login", "/about", "/TermsOfService", "/UsagePolicy", "/error", "/css/**", "/js/**", "/images/**").permitAll() // ESPI notification receipt: the Data Custodian POSTs a BatchList here without an // OAuth token — this endpoint is secured at the transport layer (TLS), not with an diff --git a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java new file mode 100644 index 00000000..fed7eb65 --- /dev/null +++ b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java @@ -0,0 +1,82 @@ +/* + * + * Copyright (c) 2026 Green Button Alliance, Inc. + * + * + * 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. + * + */ + +package org.greenbuttonalliance.espi.thirdparty.web; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.info.BuildProperties; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Public "About" page, footer-linked from every page. Mirrors the Data Custodian's About page. + * + *

Shows only what identifies the product: name, version, build time, the ESPI standard it + * implements and its release status. It deliberately does NOT show the Java or Spring Boot version: + * exact framework versions help attackers pick known CVEs, so they stay behind the admin-only + * {@code /actuator/info}. The version and build time come from {@link BuildProperties} + * ({@code spring-boot-maven-plugin:build-info}). Replaces the legacy {@code /about-version} + * controller, which read a servlet-context manifest that does not exist in a Spring Boot jar and + * rendered a template that did not exist.

+ */ +@Controller +public class AboutController { + + static final String IMPLEMENTATION = "OpenESPI Green Button Third Party"; + static final String ESPI_STANDARD = "NAESB REQ.21 ESPI, version 4.1"; + + private static final DateTimeFormatter BUILD_TIME = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'").withZone(ZoneOffset.UTC); + + private final ObjectProvider buildProperties; + + public AboutController(ObjectProvider buildProperties) { + this.buildProperties = buildProperties; + } + + @GetMapping("/about") + public String about(Model model) { + BuildProperties build = buildProperties.getIfAvailable(); + String version = build != null ? build.getVersion() : null; + + Map info = new LinkedHashMap<>(); + info.put("Implementation", IMPLEMENTATION); + info.put("Version", version != null ? version : "(development build)"); + if (build != null && build.getTime() != null) { + info.put("Build time", BUILD_TIME.format(build.getTime())); + } + info.put("Standard", ESPI_STANDARD); + info.put("Status", releaseStatus(version)); + model.addAttribute("info", info); + return "about"; + } + + /** A version without a release (null or a SNAPSHOT) is pre-release and not for production use. */ + static String releaseStatus(String version) { + return version == null || version.endsWith("-SNAPSHOT") + ? "Pre-release: not for production use" + : "Release"; + } +} diff --git a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java deleted file mode 100644 index f42c62f7..00000000 --- a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * - * Copyright (c) 2025-2026 Green Button Alliance, Inc. - * - * - * 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. - * - */ - -package org.greenbuttonalliance.espi.thirdparty.web; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; - -import jakarta.servlet.ServletContext; -import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.jar.Manifest; - -@Controller -@RequestMapping -public class VersionRESTController extends BaseController { - - /** - * Handling GET request to retrieve details from MANIFEST.MF file - * - * @return implementation details - */ - @RequestMapping(value = "/about-version", method = RequestMethod.GET) - public String getBuildNumber(HttpServletRequest request, ModelMap model) - throws IOException { - - ServletContext context = request.getSession().getServletContext(); - InputStream manifestStream = context - .getResourceAsStream("/META-INF/MANIFEST.MF"); - Manifest manifest = new Manifest(manifestStream); - - Map aboutInfo = new HashMap<>(); - aboutInfo.put("Implementation-Vendor", manifest.getMainAttributes() - .getValue("Implementation-Vendor")); - aboutInfo.put("Implementation-Title", manifest.getMainAttributes() - .getValue("Implementation-Title")); - aboutInfo.put("Implementation-Version", manifest.getMainAttributes() - .getValue("Implementation-Version")); - aboutInfo.put("Implementation-Jdk", manifest.getMainAttributes() - .getValue("Build-Jdk")); - aboutInfo.put("Implementation-Build", manifest.getMainAttributes() - .getValue("Implementation-Build")); - aboutInfo.put("Implementation-Build-Time", manifest.getMainAttributes() - .getValue("Implementation-Build-Time")); - - model.put("aboutInfo", aboutInfo); - return "/about"; - - } - -} \ No newline at end of file diff --git a/openespi-thirdparty/src/main/resources/templates/about.html b/openespi-thirdparty/src/main/resources/templates/about.html new file mode 100644 index 00000000..06d7c035 --- /dev/null +++ b/openespi-thirdparty/src/main/resources/templates/about.html @@ -0,0 +1,54 @@ + + + + About - Third Party - Green Button Alliance + + + + + +
+

About

+

+ A reference implementation of the NAESB ESPI (Green Button) Third Party, maintained by the + Green Button Alliance. +

+ +
+
+
+

This installation

+
+ + + + + + + +
Keyvalue
+
+
+
+ + +
+
+ +
+ + + diff --git a/openespi-thirdparty/src/main/resources/templates/fragments/layout.html b/openespi-thirdparty/src/main/resources/templates/fragments/layout.html index 3358d102..c1369098 100644 --- a/openespi-thirdparty/src/main/resources/templates/fragments/layout.html +++ b/openespi-thirdparty/src/main/resources/templates/fragments/layout.html @@ -87,6 +87,7 @@