Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions openespi-datacustodian/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,15 @@
</exclude>
</excludes>
</configuration>
<executions>
<!-- META-INF/build-info.properties -> BuildProperties bean (About page version/build
time; also shown by the admin-only /actuator/info). -->
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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}.</p>
*/
@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> buildProperties;

public AboutController(ObjectProvider<BuildProperties> buildProperties) {
this.buildProperties = buildProperties;
}

@GetMapping("/about")
public String about(Model model) {
BuildProperties build = buildProperties.getIfAvailable();
String version = build != null ? build.getVersion() : null;

Map<String, String> 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";
}
}

This file was deleted.

46 changes: 32 additions & 14 deletions openespi-datacustodian/src/main/resources/templates/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,43 @@
<nav th:replace="~{fragments/layout :: header}"></nav>

<div class="container">
<h2 class="mt-4">About</h2>
<p class="text-muted">
<h1 class="h2 mt-4 gba-section-title">About</h1>
<p class="lead">
A reference implementation of the NAESB ESPI (Green Button) Data Custodian, maintained by the
<a href="https://www.greenbuttonalliance.org">Green Button Alliance</a>.
<a href="https://www.greenbuttonalliance.org" target="_blank" rel="noopener">Green Button Alliance</a>.
</p>

<div class="card mt-3">
<div class="table-responsive">
<table class="table table-striped mb-0">
<tbody>
<tr th:each="entry : ${info}">
<th class="w-25" th:text="${entry.key}">Key</th>
<td th:text="${entry.value}">value</td>
</tr>
</tbody>
</table>
<div class="row g-4 mt-1">
<div class="col-lg-7">
<section class="card h-100">
<div class="gba-card-header"><h2>This installation</h2></div>
<div class="table-responsive">
<table class="table mb-0">
<tbody>
<tr th:each="entry : ${info}">
<th scope="row" class="w-25" th:text="${entry.key}">Key</th>
<td th:text="${entry.value}">value</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</div>

<div class="col-lg-5">
<section class="card h-100">
<div class="gba-card-header"><h2>Resources</h2></div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2"><a href="https://www.greenbuttonalliance.org" target="_blank" rel="noopener">Green Button Alliance</a></li>
<li class="mb-2"><a href="https://greenbuttonalliance.github.io/OpenESPI-GreenButton-API-Documentation/" target="_blank" rel="noopener">Green Button API documentation</a></li>
<li class="mb-2"><a href="https://github.com/GreenButtonAlliance/OpenESPI-GreenButton-Java" target="_blank" rel="noopener">Source code</a> (Apache License 2.0)</li>
<li><a href="https://github.com/GreenButtonAlliance/OpenESPI-GreenButton-Java/blob/main/SECURITY.md" target="_blank" rel="noopener">Report a security vulnerability</a></li>
</ul>
</div>
</section>
</div>
</div>
</div>

<footer th:replace="~{fragments/layout :: footer}"></footer>
Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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");
}
}
9 changes: 9 additions & 0 deletions openespi-thirdparty/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,15 @@
</exclude>
</excludes>
</configuration>
<executions>
<!-- META-INF/build-info.properties -> BuildProperties bean (About page version/build
time; also shown by the admin-only /actuator/info). -->
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading