From 9ea4c70a0af3360a0ff16baeec3bd18690949d93 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 08:54:52 -0400 Subject: [PATCH 1/7] Secure Roller initial setup with one-time token --- CHANGES.md | 2 + .../business/jpa/JPAUserManagerImpl.java | 4 +- .../weblogger/ui/core/RollerContext.java | 20 ++ .../ui/core/filters/BootstrapFilter.java | 1 + .../core/filters/BootstrapSecurityFilter.java | 24 ++ .../core/security/BasicUserAutoProvision.java | 1 + .../ui/core/security/BootstrapSecurity.java | 55 ++++ .../ui/struts2/core/BootstrapToken.java | 23 ++ .../weblogger/ui/struts2/core/Install.java | 6 + .../weblogger/ui/struts2/core/Register.java | 16 + app/src/main/resources/struts.xml | 7 + .../WEB-INF/jsps/core/BootstrapToken.jsp | 7 + app/src/main/webapp/WEB-INF/tiles.xml | 3 + app/src/main/webapp/WEB-INF/web.xml | 13 + .../core/security/BootstrapSecurityTest.java | 11 + docs/roller-install-guide.adoc | 6 + roller_7_plan.md | 302 ++++++++++++++++++ 17 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java create mode 100644 app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java create mode 100644 app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java create mode 100644 app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp create mode 100644 app/src/test/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurityTest.java create mode 100644 roller_7_plan.md diff --git a/CHANGES.md b/CHANGES.md index ca2ff1b780..3f8e27f5e6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## 6.1.6 +Initial installation now requires a one-time, cryptographically secure setup token printed to the server log; bootstrap access closes after the first administrator is created. + A maintenance release. Users of 6.1.5 and earlier are encouraged to upgrade. ### Behaviour changes worth reading before upgrading diff --git a/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java b/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java index d83bac4261..7c7d1d5d28 100644 --- a/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java +++ b/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java @@ -97,7 +97,9 @@ public void addUser(User newUser) throws WebloggerException { boolean adminUser = false; List existingUsers = this.getUsers(Boolean.TRUE, null, null, 0, 1); - boolean firstUserAdmin = WebloggerConfig.getBooleanProperty("users.firstUserAdmin"); + boolean firstUserAdmin = WebloggerConfig.getBooleanProperty("users.firstUserAdmin") + && (org.apache.roller.weblogger.ui.core.security.BootstrapSecurity.isCompleted() + || org.apache.roller.weblogger.ui.core.security.BootstrapSecurity.initialAdminScope()); if (existingUsers.isEmpty() && firstUserAdmin) { // Make first user an admin adminUser = true; diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java index a3dfb930b8..22bda7afbb 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java @@ -45,6 +45,7 @@ import org.apache.roller.weblogger.ui.core.plugins.UIPluginManager; import org.apache.roller.weblogger.ui.core.plugins.UIPluginManagerImpl; import org.apache.roller.weblogger.ui.core.security.AutoProvision; +import org.apache.roller.weblogger.ui.core.security.BootstrapSecurity; import org.apache.roller.weblogger.util.Reflection; import org.apache.roller.weblogger.util.cache.CacheManager; import org.apache.velocity.runtime.RuntimeSingleton; @@ -157,6 +158,10 @@ public void contextInitialized(ServletContextEvent sce) { return; } + if (!WebloggerStartup.isPrepared()) { + BootstrapSecurity.start(); + } + final boolean ittest = "ittest".equals(WebloggerConfig.getProperty("installation.type")); // if preparation failed or is incomplete then we are done, @@ -186,6 +191,21 @@ public void contextInitialized(ServletContextEvent sce) { // trigger initialization process weblogger = WebloggerFactory.getWeblogger(); weblogger.initialize(); + try { + org.apache.roller.weblogger.pojos.RuntimeConfigProperty marker = + weblogger.getPropertiesManager().getProperty(BootstrapSecurity.COMPLETION_PROPERTY); + if (marker != null && "true".equalsIgnoreCase(marker.getValue())) { + BootstrapSecurity.complete(); + } else if (weblogger.getUserManager().getUserCount() > 0) { + weblogger.getPropertiesManager().saveProperty(new org.apache.roller.weblogger.pojos.RuntimeConfigProperty(BootstrapSecurity.COMPLETION_PROPERTY, "true")); + weblogger.flush(); + BootstrapSecurity.complete(); + } else { + BootstrapSecurity.start(); + } + } catch (WebloggerException ignored) { + BootstrapSecurity.start(); + } } catch (BootstrapException ex) { log.fatal("Roller Weblogger bootstrap failed", ex); diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java index 2ca3da9c46..42d8fd059d 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java @@ -77,6 +77,7 @@ private boolean isInstallUrl(String uri) { uri.endsWith("bootstrap.rol") || uri.endsWith("create.rol") || uri.endsWith("upgrade.rol") + || uri.endsWith("bootstrap-token.rol") || uri.endsWith(".js") || uri.endsWith(".css"))); } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java new file mode 100644 index 0000000000..a01e9504fb --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java @@ -0,0 +1,24 @@ +package org.apache.roller.weblogger.ui.core.filters; + +import java.io.IOException; +import javax.servlet.*; +import javax.servlet.http.*; +import org.apache.roller.weblogger.business.WebloggerFactory; +import org.apache.roller.weblogger.ui.core.security.BootstrapSecurity; + +/** Prevents anonymous access to installer and first-user actions. */ +public class BootstrapSecurityFilter implements Filter { + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + HttpServletRequest r = (HttpServletRequest) req; + HttpServletResponse p = (HttpServletResponse) res; + String uri = r.getRequestURI(); + boolean installer = uri != null && (uri.contains("/roller-ui/install/") || uri.endsWith("/roller-ui/register.rol") || uri.endsWith("/roller-ui/register!save.rol") || uri.endsWith("/roller-ui/setup.rol")); + if (installer && !BootstrapSecurity.isCompleted()) { + if (uri.endsWith("/bootstrap-token.rol")) { chain.doFilter(req, res); return; } + if (!BootstrapSecurity.isValid(r)) { p.sendRedirect(r.getContextPath() + "/roller-ui/bootstrap-token.rol"); return; } + } + chain.doFilter(req, res); + } + public void init(FilterConfig c) { } + public void destroy() { } +} diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BasicUserAutoProvision.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BasicUserAutoProvision.java index e486d02c9f..d526e06bcf 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BasicUserAutoProvision.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BasicUserAutoProvision.java @@ -45,6 +45,7 @@ public class BasicUserAutoProvision implements AutoProvision { */ @Override public boolean execute(HttpServletRequest request) { + if (!BootstrapSecurity.isCompleted()) return false; User ud = CustomUserRegistry.getUserDetailsFromAuthentication(request); if (hasNecessaryFields(ud)) { diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java new file mode 100644 index 0000000000..ba69facb57 --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java @@ -0,0 +1,55 @@ +package org.apache.roller.weblogger.ui.core.security; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.Base64; +import javax.servlet.http.HttpServletRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** Process-scoped gate for the unauthenticated installation flow. */ +public final class BootstrapSecurity { + public static final String COMPLETION_PROPERTY = "bootstrap.completed"; + private static final Log LOG = LogFactory.getLog(BootstrapSecurity.class); + private static final long LIFETIME_MS = 60 * 60 * 1000L; + private static final String SESSION = BootstrapSecurity.class.getName() + ".grant"; + private static byte[] digest; + private static long expires; + private static volatile boolean completed; + private static final ThreadLocal INITIAL = new ThreadLocal<>(); + private BootstrapSecurity() { } + + public static synchronized void start() { + if (completed || digest != null) return; + byte[] raw = new byte[32]; + new SecureRandom().nextBytes(raw); + String token = Base64.getUrlEncoder().withoutPadding().encodeToString(raw); + digest = sha256(token); + expires = System.currentTimeMillis() + LIFETIME_MS; + LOG.warn("Roller initial setup is locked. The one-time setup token (expires in 60 minutes) is: " + token); + } + + public static boolean isCompleted() { return completed; } + public static void complete() { completed = true; digest = null; } + public static void beginInitialAdmin() { INITIAL.set(Boolean.TRUE); } + public static void endInitialAdmin() { INITIAL.remove(); } + public static boolean initialAdminScope() { return Boolean.TRUE.equals(INITIAL.get()); } + public static boolean isValid(HttpServletRequest request) { + Object grant = request.getSession(false) == null ? null : request.getSession(false).getAttribute(SESSION); + return !completed && grant instanceof Long && ((Long) grant) >= System.currentTimeMillis(); + } + public static synchronized boolean redeem(HttpServletRequest request, String token) { + if (completed || digest == null || token == null || System.currentTimeMillis() > expires) return false; + byte[] supplied = sha256(token); + if (!MessageDigest.isEqual(digest, supplied)) return false; + request.getSession(true).setAttribute(SESSION, System.currentTimeMillis() + LIFETIME_MS); + digest = null; + return true; + } + private static byte[] sha256(String value) { + try { return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); } + catch (Exception e) { throw new IllegalStateException(e); } + } +} diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java new file mode 100644 index 0000000000..cbe1cc5a14 --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java @@ -0,0 +1,23 @@ +package org.apache.roller.weblogger.ui.struts2.core; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.roller.weblogger.ui.core.security.BootstrapSecurity; +import org.apache.roller.weblogger.ui.struts2.util.UIAction; +import org.apache.struts2.interceptor.ServletRequestAware; +import org.apache.struts2.interceptor.ServletResponseAware; +public class BootstrapToken extends UIAction implements ServletRequestAware, ServletResponseAware { + private HttpServletRequest request; private HttpServletResponse response; private String token; + public String execute() { return INPUT; } + public String redeem() { + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Referrer-Policy", "no-referrer"); + if (!"POST".equalsIgnoreCase(request.getMethod())) { addActionError("POST required"); return INPUT; } + if (BootstrapSecurity.redeem(request, token)) return SUCCESS; + addActionError("Invalid or expired setup token"); return INPUT; + } + public void setToken(String token) { this.token = token; } + public void setServletRequest(HttpServletRequest request) { this.request = request; } + public void setServletResponse(HttpServletResponse response) { this.response = response; } + public boolean isUserRequired() { return false; } + public boolean isWeblogRequired() { return false; } +} diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java index feed9b0b6b..04d71c5f1c 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java @@ -32,6 +32,8 @@ import org.apache.roller.weblogger.config.WebloggerConfig; import org.apache.roller.weblogger.ui.struts2.util.UIAction; import org.springframework.beans.FatalBeanException; +import org.apache.struts2.ServletActionContext; +import org.apache.roller.weblogger.ui.core.security.BootstrapSecurity; /** @@ -76,6 +78,7 @@ public boolean isWeblogRequired() { @Override public String execute() { + if (!BootstrapSecurity.isValid(ServletActionContext.getRequest())) return BOOTSTRAP; if (WebloggerFactory.isBootstrapped()) { return SUCCESS; @@ -114,6 +117,7 @@ public String execute() { public String create() { + if (!BootstrapSecurity.isValid(ServletActionContext.getRequest())) return BOOTSTRAP; if (WebloggerFactory.isBootstrapped()) { return SUCCESS; @@ -134,6 +138,7 @@ public String create() { public String upgrade() { + if (!BootstrapSecurity.isValid(ServletActionContext.getRequest())) return BOOTSTRAP; if (WebloggerFactory.isBootstrapped()) { return SUCCESS; @@ -154,6 +159,7 @@ public String upgrade() { public String bootstrap() { + if (!BootstrapSecurity.isValid(ServletActionContext.getRequest())) return BOOTSTRAP; log.info("ENTERING"); if (WebloggerFactory.isBootstrapped()) { diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java index 1d8f6628ae..8d9c85050c 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java @@ -33,8 +33,10 @@ import org.apache.roller.weblogger.config.WebloggerConfig; import org.apache.roller.weblogger.config.WebloggerRuntimeConfig; import org.apache.roller.weblogger.pojos.User; +import org.apache.roller.weblogger.pojos.RuntimeConfigProperty; import org.apache.roller.weblogger.ui.core.RollerSession; import org.apache.roller.weblogger.ui.core.security.CustomUserRegistry; +import org.apache.roller.weblogger.ui.core.security.BootstrapSecurity; import org.apache.roller.weblogger.ui.struts2.util.UIAction; import org.apache.roller.weblogger.util.MailUtil; import org.apache.struts2.convention.annotation.AllowedMethods; @@ -170,6 +172,14 @@ public String execute() { public String save() { + if (!BootstrapSecurity.isCompleted() && !BootstrapSecurity.isValid(getServletRequest())) { + return DISABLED_RETURN_CODE; + } + if (!BootstrapSecurity.isCompleted() + && !WebloggerConfig.getBooleanProperty("users.firstUserAdmin")) { + addError("Initial administrator creation is disabled by users.firstUserAdmin"); + return DISABLED_RETURN_CODE; + } // if registration is disabled, then don't allow registration try { @@ -187,6 +197,7 @@ public String save() { if (!hasActionErrors()) { try { + if (!BootstrapSecurity.isCompleted()) BootstrapSecurity.beginInitialAdmin(); UserManager mgr = WebloggerFactory.getWeblogger().getUserManager(); @@ -228,8 +239,11 @@ public String save() { // save new user mgr.addUser(ud); + WebloggerFactory.getWeblogger().getPropertiesManager().saveProperty( + new RuntimeConfigProperty(BootstrapSecurity.COMPLETION_PROPERTY, "true")); WebloggerFactory.getWeblogger().flush(); + if (!BootstrapSecurity.isCompleted()) BootstrapSecurity.complete(); // now send activation email if necessary sendActivationMailIfNeeded(ud, activationEnabled); @@ -248,6 +262,8 @@ public String save() { } catch (WebloggerException ex) { log.error("Error adding new user", ex); addError("generic.error.check.logs"); + } finally { + BootstrapSecurity.endInitialAdmin(); } } diff --git a/app/src/main/resources/struts.xml b/app/src/main/resources/struts.xml index d16b8346bf..1356bc56f4 100644 --- a/app/src/main/resources/struts.xml +++ b/app/src/main/resources/struts.xml @@ -138,6 +138,13 @@ .Welcome activate,execute,save + + + .BootstrapToken + install + execute,redeem + diff --git a/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp b/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp new file mode 100644 index 0000000000..b13afa8c33 --- /dev/null +++ b/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp @@ -0,0 +1,7 @@ +<%@ include file="/WEB-INF/jsps/taglibs-struts2.jsp" %> +

Roller initial setup

+

Enter the one-time setup token printed in the Roller server log.

+ + + + diff --git a/app/src/main/webapp/WEB-INF/tiles.xml b/app/src/main/webapp/WEB-INF/tiles.xml index f25bb52e10..990cbb385e 100644 --- a/app/src/main/webapp/WEB-INF/tiles.xml +++ b/app/src/main/webapp/WEB-INF/tiles.xml @@ -142,6 +142,9 @@ + + + diff --git a/app/src/main/webapp/WEB-INF/web.xml b/app/src/main/webapp/WEB-INF/web.xml index 31539fc899..8b1b550e3c 100644 --- a/app/src/main/webapp/WEB-INF/web.xml +++ b/app/src/main/webapp/WEB-INF/web.xml @@ -44,6 +44,10 @@ BootstrapFilter org.apache.roller.weblogger.ui.core.filters.BootstrapFilter + + BootstrapSecurityFilter + org.apache.roller.weblogger.ui.core.filters.BootstrapSecurityFilter + XmlRpcEnabledFilter @@ -129,6 +133,15 @@ /* REQUEST + + BootstrapSecurityFilter + /* + REQUEST + FORWARD + INCLUDE + ERROR + ASYNC + diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurityTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurityTest.java new file mode 100644 index 0000000000..395d1804b3 --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurityTest.java @@ -0,0 +1,11 @@ +package org.apache.roller.weblogger.ui.core.security; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +class BootstrapSecurityTest { + @Test void completionClosesBootstrapGate() { + BootstrapSecurity.start(); + assertFalse(BootstrapSecurity.isCompleted()); + BootstrapSecurity.complete(); + assertTrue(BootstrapSecurity.isCompleted()); + } +} diff --git a/docs/roller-install-guide.adoc b/docs/roller-install-guide.adoc index 651296047d..d18581205a 100644 --- a/docs/roller-install-guide.adoc +++ b/docs/roller-install-guide.adoc @@ -791,6 +791,12 @@ error type which the automated installation process is coded to ignore. == Upgrading Roller +=== Initial setup security token + +For a new installation Roller generates a cryptographically random, one-time setup token during startup and prints it to the Roller server log. Open `/roller-ui/bootstrap-token.rol` over HTTPS (or a secured local tunnel) and enter the token before opening the installer or registering the first administrator. The token is accepted only in the POST body, expires after 60 minutes, and is consumed on successful entry. Restart Roller to obtain a new token if it expires or the browser session is lost. The token is never included in a URL or Roller page. + +The token authorizes the installer and first-user flow for the current browser session only. Run database creation or upgrade on one setup node at a time. After the first administrator is committed, bootstrap access is permanently disabled, including after deleting or disabling that account. Existing installations with users continue to use their normal login and registration settings. + This section describes how to upgrade an existing Roller installation to the latest release of Roller by shutting down, backing up and then following the installation instructions with a couple of key exceptions. diff --git a/roller_7_plan.md b/roller_7_plan.md new file mode 100644 index 0000000000..952644a24f --- /dev/null +++ b/roller_7_plan.md @@ -0,0 +1,302 @@ +# Roller 7 Plan + +Status: draft, 2026-09-06 +Owner: Dave Johnson +Predecessor: 6.1.6, currently at `roller-6.1.6-rc2` — not yet voted + +Nothing in this plan starts before two things finish: the triage-2026 +vulnerability work, and the 6.1.6 release vote. + +## 1. What Roller 7 is + +Roller 7.0.0 is the modernization release. It takes Roller off Java EE 8 +(`javax.*`) and onto Jakarta EE 11, replaces the authentication, UI and test +stacks that were holding the dependency tree back, and removes ROME Propono — +the single dependency that has pinned ROME at 1.19.0 for years. + +Everything currently open on GitHub lands in 7.0.0. There are eleven open pull +requests; nine of them are Roller 7 material, two are dead and get closed. + +**Decisions taken 2026-09-06:** + +| Decision | Choice | Consequence | +|---|---|---| +| Java baseline | **21** | One step past the 17 baseline in #154. CI matrix becomes 21 + 25; the 11 and 17 legs go away. | +| AtomPub / Propono order | **Jakarta stack first, #161 after** | #154 merges with its forked Propono `AtomServlet`; #161 is then rebased onto Jakarta, rewritten against `jakarta.*`, and deletes the fork it inherits. | +| Release shape | **One 7.0.0 carrying everything** | Jakarta + OIDC + Bootstrap 5 + Playwright + themes + AtomPub in a single release and a single PMC vote. Longer beta, one upgrade for users. | + +## 2. Where the code actually is + +`master` is 6.1.6-rc2 — cut, not yet voted. Nineteen commits landed on it +2026-09-05/06: the security triage fixes plus release plumbing. Matt's stack branched from `f1422f444` +(2026-08-13), *before* all of that. So every PR in the stack is a month behind a +master that moved hard in exactly the areas the stack touches: AtomPub, XML-RPC, +OAuth, salt filters, media handling, authoring JSPs. + +Measured against `f1422f444`: + +| PR | Own change set | Files also touched by 6.1.6 work | +|---|---|---| +| #154 Jakarta EE 11 | 189 files¹ | **40** | +| #155 OIDC | 208 cumulative | 41 | +| #156 Bootstrap 5 | 265 cumulative | 55 | +| #157 Playwright | 287 cumulative | 59 | +| #159 media link markup | 1 file | 1 | +| #160 three themes | 46 files¹ | **0** | +| #161 AtomPub StAX | 40 files | 8 | + +¹ Measured locally from the branch point; GitHub reports 187 and 44 because +the branches were cut a commit or two before `f1422f444`. + +That 40-file overlap on #154 is the real cost of Roller 7 and it is not +mechanical — several of the collisions are two different answers to the same +question (section 5). + +## 3. Pull request inventory and disposition + +### Merge to master now, ahead of everything (small, clean, 6.1.x-class) + +| PR | Title | Author | Notes | +|---|---|---|---| +| #159 | Use double-quoted attributes in media file links inserted into new entries | mraible | One file. Approved by mbien. Real bug on master — image markup from the "create a post from your uploaded files" flow does not render. Found by Greg Huber on dev@. | +| #151 | Fix incorrect PostgreSQL port in Dockerfile | MustafaCelal | One line, outside contributor, open since Feb. Branched long before `.asf.yaml` existed, so rebase rather than merge, then land it. | +| #160 | Add three responsive light and dark blog themes | mraible | 46 files, **zero overlap** with 6.1.6 work — all new theme directories plus a `ThemeManagerTest` addition. Velocity templates and CSS, no `javax` surface, so it neither helps nor hurts the migration. Land it early and get it out of the rebase blast radius. | + +### Close as superseded + +| PR | Title | Why | +|---|---|---| +| #120 | Bump spring-web 5.3.20 → 6.0.0 | dependabot, March 2023. #154 takes Spring to 7.0.8. | +| #125 | Bump struts2-core 2.5.29 → 2.5.31 | dependabot, July 2023. #154 takes Struts to 7.1.1. | +| #129 | Bump spring-security-config 5.8.3 → 5.8.5 | dependabot, July 2023. #154/#155 take Spring Security to 7.0.6. | + +Close them with a one-line comment pointing at ROL-2183 so the history reads +sensibly. Re-enable dependabot against the 7.0 tree afterwards (section 7). + +### The Roller 7 stack, in merge order + +1. **#154 — ROL-2183: Migrate from javax to Jakarta EE 11** (mraible, 187 files, 22 commits, targets `master`) + Java 17 → *we retarget to 21*, Servlet 6.1, Struts 7.1.1, Spring 7.0.8, + Spring Security 7.0.6, EclipseLink 5.0.1, Jetty 12.1.12, Tomcat 11, + Derby 10.16.1.1. 320 `javax.*` imports converted across 89 files, 29 ORM + files renamespaced, XWork → `org.apache.struts2`, Spring Security moved to + expression-based access control. Removes OAuth 1.0a and the OpenID 2.0 + filter. Forks `XmlRpcServlet` and Propono's `AtomServlet` into Roller because + neither library has a Jakarta release. 45 review comments, two Copilot passes. +2. **#155 — Replace OpenID 2.0 with OAuth 2.0/OIDC login** (mraible, 28 own files, targets `feature/jakarta-ee-10-migration`) + `spring-security-oauth2-client`, `RollerClientRegistrationRepository` reading + provider config from Roller properties, `RollerOidcUserService` resolving the + Roller account behind the OIDC principal via the existing `openIdUrl` column. + New `oidc` / `db-oidc` values for `authentication.method`; `openid` and + `db-openid` are gone. Keycloak in docker-compose. Auto-provision off by + default; the `users.firstUserAdmin` bootstrap grant no longer applies to + auto-provisioned accounts. +3. **#156 — Convert the admin and editor UI from Bootstrap 3 to Bootstrap 5.3** (mraible, 63 own files, targets `feature/oidc-login`) + Bootstrap 5.3.8, bootstrap-icons for glyphicons, form theme via + struts2-bootstrap-plugin 6.1.0. Fixes dismissible alerts, badge pills, button + rows, field-help tooltips. Audited page by page against a seeded master + baseline — 24 admin/editor screens screenshot-compared. +4. **#157 — Replace the Selenium suite with Playwright** (mraible, 29 own files, targets `feature/bootstrap-5`) + Deletes `it-selenium`; adds `it-playwright` with `NewUserJourneyIT`, + `OidcLoginIT`, `LoginPageIT` and `WebServicesIT`. `WebServicesIT` is the only + coverage of the forked XML-RPC and AtomPub servlets and it already caught the + AtomPub basic-auth 401 bug that exists on master. CI runs it three ways: db on + Jetty/Derby, plus oidc and db-oidc against docker-compose. + Open question from mbien on the PR — "what is the trigger for the test + framework swap?" — still needs an answer in the PR description. Suggested + answer: Struts 2.5.30+ breaks the Selenium tests, which is why + `struts.version` has been pinned at 2.5.29 for years, and the suite covers + one auth method out of five. +5. **#161 — Replace ROME Propono AtomPub server with self-contained StAX implementation** (snoopdave, 40 files, targets `master`) + RFC 5023 server on JDK StAX and plain DTOs. New `RollerAtomServlet`, wire + model, `AtomWriter`/`AtomReader` (DTDs and external entities disabled), 34 + tests including a full lifecycle integration test against in-memory Derby and + RELAX NG schema validation with Jing. **Merges last**, rebased onto the + Jakarta tree — see section 6. + +## 4. Branch and version plan + +``` +6.1.6 (tag) + └── roller-6.1.x <- cut now, maintenance only, matches roller-6.0.x/roller-5.2.x convention +master <- becomes 7.0.0-SNAPSHOT, is Roller 7 development +``` + +Steps, in order: + +1. Once 6.1.6 is voted through, cut `roller-6.1.x` from the release tag. + Nothing goes there unless a security report forces a 6.1.7. +2. On `master`: `roller.version` and `` to `7.0.0-SNAPSHOT` in the + parent pom, `app`, `db-utils`, `assembly-release`, `it-selenium` + (until #157 deletes it), and the Docker files. +3. Land #159, #151, #160. +4. Rebase and merge the stack: #154, then #155, then #156, then #157, each + retargeted to `master` as its parent lands. +5. Rebase and merge #161. +6. Modernization sweep (section 7). +7. Beta, then release (section 8). + +The stack merges as four separate PRs, not one squash. The commit-per-phase +structure in #154 is worth keeping in history — when something breaks in +production a year from now, "which phase" is the first question. + +## 5. Reconciliation: 6.1.6 master vs. the stack + +These are decisions, not conflicts a rebase can resolve. Each needs an answer +before #154 merges, and each answer belongs in the 7.0.0 upgrade notes. + +| Collision | 6.1.6 master says | The stack says | Call | +|---|---|---|---| +| **WSSE AtomPub auth** | Retired (#166). `webservices.atomPubAuth` takes `basic` and `oauth`; `wsse` fails closed on startup. | #154 keeps WSSE and advertises "basic or wsse"; #161 also removes WSSE. | Master wins: WSSE stays dead. Strip the WSSE paths and admin labels from #154 during the rebase. | +| **OAuth 1.0a for AtomPub** | Kept, and hardened — #165 fixed the authorize servlet's session handling. | #154 deletes it: `net.oauth` is unmaintained and javax-only. | Stack wins: OAuth 1.0a goes. Note in the upgrade guide that AtomPub is basic-only in 7.0, and that #155's OIDC covers browser login, not AtomPub. Decide whether `OAuthManager`, `OAuthAccessorRecord` and the `roller_oauth*` tables get dropped or left inert — a schema drop needs a migration script. | +| **Trackback** | Both directions removed (#163, #178). `TrackbackServlet`, `WeblogTrackbackRequest` and the outbound action are gone. | #154 still edits those files (they exist at its branch point). | Master wins. These become delete-vs-modify conflicts — resolve to delete. | +| **Authoring UI inline JS** | Moved to data attributes across the authoring JSPs (#168). | #156 rewrites the same JSPs for Bootstrap 5. | Rebase #156 onto the data-attribute markup, not the other way round. This is the largest mechanical conflict in the stack — 55 overlapping files — and the screenshot baseline used for the Bootstrap 5 audit has to be regenerated afterwards. | +| **Media content types** | Derived from file content (#174). | Stack predates it. | Master wins; verify the Struts 7 `UploadedFilesAware` rework in #154 still routes through the content-sniffing path, and that `WebServicesIT` covers it. | +| **Enclosure metadata** | Stored as submitted, no remote fetch (#175). | Stack predates it. | Master wins. | +| **XML-RPC** | Weblog permission checks added (#164), vendor extension types disabled (#171). | #154 forks `XmlRpcServlet` into Roller. | Both. Make sure the forked servlet is wired behind the same checks — `WebServicesIT` publishes over XML-RPC and should assert the permission failure path too. | +| **Salt filters** | Submitted and response salts separated (#167); one-time salt work is on a local branch. | #154 touches `ValidateSaltFilter`. | Master wins. | +| **Frontpage / template resolution** | #169, #170, #172. | #160's themes touch theme resolution. | No conflict measured (0 overlap), but re-run `ThemeManagerTest` and the frontpage tests after #160 lands. | +| **Java baseline** | 11, CI on 11/17/21/25. | 17, CI on 17/21/25. | **21**, CI on 21 and 25. Our delta on top of #154: bump ``, drop the 17 leg, re-check Derby (10.17 is the Java 21 baseline — decide whether to take it or stay on 10.16.1.1), and confirm Mockito still instruments 25. | + +## 6. Propono removal (#161), rebased + +The chosen order means #161 lands on a tree where Propono has already been +forked in rather than removed. Concretely, after #154–#157 merge: + +1. Rebase `replace-propono-atompub` onto `master`. +2. Convert the new AtomPub classes to `jakarta.servlet.*`. `javax.xml.stream` is + JDK API and does **not** move — that was the point of choosing StAX. +3. Delete the forked Propono `AtomServlet` and `RollerAtomRequestImpl` that #154 + brought in, plus the null-path-info normalization that was ported into it — + `RollerAtomServlet` must carry that behavior instead (a request to the bare + `/roller-services/app` mapping serves the service document; #157's smoke test + guards it). +4. Drop `rome-propono` from `app/pom.xml` and remove the `` comment and the `rome.version` pin comment. +5. Address Matt's review — nine verified findings, of which at least these are + blockers: + - `readBody()` reads the whole request body with `readAllBytes()` before any + quota check. Propono streamed to a temp file. As written, any authenticated + user can OOM the server with a large POST to the media collection. Stream + to a temp file or bound the read against the media quota. + - `getElementText()` throws on `type="xhtml"` content, summary and title, + which RFC 4287 allows and ROME accepted. Clients publishing xhtml get a + 500. Needs a branch that captures child XML as a string. + - `MediaCollection.deleteEntry()` strips the `.media-link` suffix for the + filename but passes the unstripped path to `getMediaFileByPath`, so DELETE + on the server's own advertised `rel="edit"` URI always NPEs. Pre-existing, + carried forward; fix it and add a delete test. + - Unrecognized `webservices.atomPubAuth` values must fail closed with a log + line naming the property and the valid options, the way 6.1.6 handles a + stored `wsse`. +6. Port `WebServicesIT` from #157 onto the StAX implementation — Matt offered to + do this and it is the highest-leverage follow-up on the PR. +7. Run an over-the-wire exerciser (APE or equivalent) against a deployed + instance. The unit tests do not exercise HTTP transport or BASIC auth over + the wire, and the previous format was ROME-generated, so interop is the risk. + +Then, and only then, unpin ROME. + +## 7. Modernization sweep after the stack lands + +Ordered by value, not effort: + +1. **Unpin ROME.** `rome.version` is at 1.19.0, pinned by a comment in + `app/pom.xml` saying the next version removes Propono. With #161 in, take the current ROME for + feed rendering and the Planet aggregator. This is the whole reason Propono had + to go. +2. **Unpin Struts.** `struts.version` is pinned at 2.5.29 with "`.30+` breaks + selenium tests". #154 goes to 7.1.1 and #157 deletes the Selenium suite, so + delete the comment and the reason for it. +3. **Decide XML-RPC's future.** Roller carries a fork of `XmlRpcServlet` because + `org.apache.xmlrpc` 3.1.3 is long unmaintained and has no Jakarta release. Blogger and + MetaWeblog are legacy APIs; 6.1.6 already had to add permission checks and + disable vendor extension types on them. Either commit to owning the fork or + put deprecation of XML-RPC on the 7.x roadmap. Worth a dev@ thread. +4. **Guice.** Still on `com.google.inject`. Check the version, and whether the + EclipseLink 5 / Spring 7 tree makes a straight Spring DI migration cheap + enough to be worth doing while everything else is already moving. +5. **Velocity 2.4.1 and the template layer.** No action forced by Jakarta, but + confirm the rendering path is clean on 21 and that the three new themes from + #160 render on the Bootstrap 5 admin. +6. **Re-enable dependabot** against the 7.0 tree once the versions settle, with + grouped PRs so we do not get another three-year backlog of singles. +7. **Docs.** `docs/roller-install-guide.adoc`, `roller-user-guide.adoc` and + `roller-template-guide.adoc` all describe a javax/Tomcat 9/OpenID world. + The install guide needs Java 21, Tomcat 11, the OIDC configuration, and the + new `authentication.method` values. The user guide needs the Bootstrap 5 + screens reshot. +8. **Docker.** Tomcat 11 base image, PostgreSQL 16, Keycloak for the OIDC demo, + and the compose file that #151 was trying to fix. +9. **Local branches to reconcile or delete.** `safer-defaults` (4 commits ahead), + `one-time-salt` (1), `jakarta` (2) — decide whether any of that is Roller 7 + material before the rebase makes them unmergeable. `remove-solr`, + `jakara-migration`, `jstl-not-provided` and `parse-referrer` are level with + master and can be deleted. + +## 8. Release engineering + +Roller 7.0.0 is a bigger release than anything since 6.0, and the changes users +will actually feel are the removals. The release notes lead with those, not with +the framework versions. + +**Upgrade notes must cover:** + +- Java 21 required. Java 11 and 17 no longer supported. +- Tomcat 11 (Jakarta) required. Tomcat 9 will not run Roller 7. +- `authentication.method`: `openid` and `db-openid` are gone; use `oidc` / + `db-oidc` and configure a provider. Existing OpenID 2.0 identities in + `openIdUrl` are reused by the OIDC account linking path — document what + happens to a user whose provider is dead. +- AtomPub: OAuth 1.0a and WSSE both gone. Basic auth only. +- XML-RPC and AtomPub survive but are reimplemented — anyone with a custom + client should retest. +- Trackback (both directions) already removed in 6.1.6; repeat it here for + people upgrading from 6.1.5 or earlier. +- Any schema change from dropping the OAuth 1.0a tables, with a migration + script and a "you may leave them in place" option. + +**Process:** follow the existing release runbook — the `roller-release` skill has +the RC, signing, staging, VOTE and promotion steps. Two things specific to this +release: + +- Ship at least one beta or RC that the dev@ list is actually asked to deploy. + A Jakarta migration plus an auth migration plus a UI migration is not something + to discover in the GA vote. +- The 72-hour VOTE window will not be enough for people to test this properly. + Announce the beta on dev@ and user@ with a deadline of its own. + +**Definition of done for 7.0.0:** + +- All four stack PRs plus #159, #151, #160 and #161 merged to `master`. +- `grep -r "javax\." app/src/main/java` returns only `javax.xml.stream`, + `javax.sql`, `javax.naming`, `javax.imageio` — JDK packages, not Java EE. +- No `rome-propono` in any pom; ROME on a current version. +- Playwright suite green on all three CI legs. +- Unit tests green on 21 and 25. +- Docker compose comes up and the new-user journey passes against it. +- Install guide reflects Java 21 / Tomcat 11 / OIDC. +- A real AtomPub client round-trips against a deployed instance. + +## 9. Risks + +| Risk | Mitigation | +|---|---| +| The #156 rebase onto the 6.1.6 authoring JSPs is the single biggest source of silent breakage — 55 overlapping files, and the Bootstrap 5 audit baseline is now stale. | Regenerate the screenshot baseline from post-6.1.6 master before rebasing, and re-run the 24-page comparison after. | +| AtomPub wire-format regression is invisible to the JUnit suite. | #157's `WebServicesIT` plus an over-the-wire exerciser before the vote. Do not ship on unit tests alone. | +| Java 21 narrows the deployment base right when we are also asking users to move to Tomcat 11. | It is one message either way — "Roller 7 needs a current stack". Say it once, loudly, in the release notes. | +| The stack is one contributor's work and it is large. If Matt goes quiet mid-rebase, the PMC owns 187 files of migration it did not write. | Review #154 phase by phase now, while he is around to answer. Two committers should be able to explain the Struts 7 parameter-binding and Spring Security authorization-manager changes without him. | +| A security report arrives mid-flight and forces a 6.1.7. | `roller-6.1.x` exists from day one so a fix does not have to be cherry-picked out of a half-migrated master. | +| Long-lived feature branches drift again. | Merge the stack in weeks, not months. Nothing in section 7 blocks a merge. | + +## 10. Open questions for dev@roller + +1. Java 21 or 17 as the 7.0 baseline — this plan says 21; the PMC should agree + before #154 merges, because it is much cheaper to decide now than after. +2. Does XML-RPC (Blogger/MetaWeblog) have a future, given we now maintain a fork + of its servlet? +3. Do the OAuth 1.0a tables get dropped in 7.0 or left inert? +4. Beta timing and how long the beta window stays open before the GA vote. +5. Should the three new themes from #160 change the default for new weblogs, or + does Basic stay the default? (#160 explicitly preserves Basic — confirm that + is what we want long term.) From 69106ba7c246c98bdbe3202ccc18cc3af9839d42 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 09:10:57 -0400 Subject: [PATCH 2/7] Remove unrelated Roller 7 planning document --- roller_7_plan.md | 302 ----------------------------------------------- 1 file changed, 302 deletions(-) delete mode 100644 roller_7_plan.md diff --git a/roller_7_plan.md b/roller_7_plan.md deleted file mode 100644 index 952644a24f..0000000000 --- a/roller_7_plan.md +++ /dev/null @@ -1,302 +0,0 @@ -# Roller 7 Plan - -Status: draft, 2026-09-06 -Owner: Dave Johnson -Predecessor: 6.1.6, currently at `roller-6.1.6-rc2` — not yet voted - -Nothing in this plan starts before two things finish: the triage-2026 -vulnerability work, and the 6.1.6 release vote. - -## 1. What Roller 7 is - -Roller 7.0.0 is the modernization release. It takes Roller off Java EE 8 -(`javax.*`) and onto Jakarta EE 11, replaces the authentication, UI and test -stacks that were holding the dependency tree back, and removes ROME Propono — -the single dependency that has pinned ROME at 1.19.0 for years. - -Everything currently open on GitHub lands in 7.0.0. There are eleven open pull -requests; nine of them are Roller 7 material, two are dead and get closed. - -**Decisions taken 2026-09-06:** - -| Decision | Choice | Consequence | -|---|---|---| -| Java baseline | **21** | One step past the 17 baseline in #154. CI matrix becomes 21 + 25; the 11 and 17 legs go away. | -| AtomPub / Propono order | **Jakarta stack first, #161 after** | #154 merges with its forked Propono `AtomServlet`; #161 is then rebased onto Jakarta, rewritten against `jakarta.*`, and deletes the fork it inherits. | -| Release shape | **One 7.0.0 carrying everything** | Jakarta + OIDC + Bootstrap 5 + Playwright + themes + AtomPub in a single release and a single PMC vote. Longer beta, one upgrade for users. | - -## 2. Where the code actually is - -`master` is 6.1.6-rc2 — cut, not yet voted. Nineteen commits landed on it -2026-09-05/06: the security triage fixes plus release plumbing. Matt's stack branched from `f1422f444` -(2026-08-13), *before* all of that. So every PR in the stack is a month behind a -master that moved hard in exactly the areas the stack touches: AtomPub, XML-RPC, -OAuth, salt filters, media handling, authoring JSPs. - -Measured against `f1422f444`: - -| PR | Own change set | Files also touched by 6.1.6 work | -|---|---|---| -| #154 Jakarta EE 11 | 189 files¹ | **40** | -| #155 OIDC | 208 cumulative | 41 | -| #156 Bootstrap 5 | 265 cumulative | 55 | -| #157 Playwright | 287 cumulative | 59 | -| #159 media link markup | 1 file | 1 | -| #160 three themes | 46 files¹ | **0** | -| #161 AtomPub StAX | 40 files | 8 | - -¹ Measured locally from the branch point; GitHub reports 187 and 44 because -the branches were cut a commit or two before `f1422f444`. - -That 40-file overlap on #154 is the real cost of Roller 7 and it is not -mechanical — several of the collisions are two different answers to the same -question (section 5). - -## 3. Pull request inventory and disposition - -### Merge to master now, ahead of everything (small, clean, 6.1.x-class) - -| PR | Title | Author | Notes | -|---|---|---|---| -| #159 | Use double-quoted attributes in media file links inserted into new entries | mraible | One file. Approved by mbien. Real bug on master — image markup from the "create a post from your uploaded files" flow does not render. Found by Greg Huber on dev@. | -| #151 | Fix incorrect PostgreSQL port in Dockerfile | MustafaCelal | One line, outside contributor, open since Feb. Branched long before `.asf.yaml` existed, so rebase rather than merge, then land it. | -| #160 | Add three responsive light and dark blog themes | mraible | 46 files, **zero overlap** with 6.1.6 work — all new theme directories plus a `ThemeManagerTest` addition. Velocity templates and CSS, no `javax` surface, so it neither helps nor hurts the migration. Land it early and get it out of the rebase blast radius. | - -### Close as superseded - -| PR | Title | Why | -|---|---|---| -| #120 | Bump spring-web 5.3.20 → 6.0.0 | dependabot, March 2023. #154 takes Spring to 7.0.8. | -| #125 | Bump struts2-core 2.5.29 → 2.5.31 | dependabot, July 2023. #154 takes Struts to 7.1.1. | -| #129 | Bump spring-security-config 5.8.3 → 5.8.5 | dependabot, July 2023. #154/#155 take Spring Security to 7.0.6. | - -Close them with a one-line comment pointing at ROL-2183 so the history reads -sensibly. Re-enable dependabot against the 7.0 tree afterwards (section 7). - -### The Roller 7 stack, in merge order - -1. **#154 — ROL-2183: Migrate from javax to Jakarta EE 11** (mraible, 187 files, 22 commits, targets `master`) - Java 17 → *we retarget to 21*, Servlet 6.1, Struts 7.1.1, Spring 7.0.8, - Spring Security 7.0.6, EclipseLink 5.0.1, Jetty 12.1.12, Tomcat 11, - Derby 10.16.1.1. 320 `javax.*` imports converted across 89 files, 29 ORM - files renamespaced, XWork → `org.apache.struts2`, Spring Security moved to - expression-based access control. Removes OAuth 1.0a and the OpenID 2.0 - filter. Forks `XmlRpcServlet` and Propono's `AtomServlet` into Roller because - neither library has a Jakarta release. 45 review comments, two Copilot passes. -2. **#155 — Replace OpenID 2.0 with OAuth 2.0/OIDC login** (mraible, 28 own files, targets `feature/jakarta-ee-10-migration`) - `spring-security-oauth2-client`, `RollerClientRegistrationRepository` reading - provider config from Roller properties, `RollerOidcUserService` resolving the - Roller account behind the OIDC principal via the existing `openIdUrl` column. - New `oidc` / `db-oidc` values for `authentication.method`; `openid` and - `db-openid` are gone. Keycloak in docker-compose. Auto-provision off by - default; the `users.firstUserAdmin` bootstrap grant no longer applies to - auto-provisioned accounts. -3. **#156 — Convert the admin and editor UI from Bootstrap 3 to Bootstrap 5.3** (mraible, 63 own files, targets `feature/oidc-login`) - Bootstrap 5.3.8, bootstrap-icons for glyphicons, form theme via - struts2-bootstrap-plugin 6.1.0. Fixes dismissible alerts, badge pills, button - rows, field-help tooltips. Audited page by page against a seeded master - baseline — 24 admin/editor screens screenshot-compared. -4. **#157 — Replace the Selenium suite with Playwright** (mraible, 29 own files, targets `feature/bootstrap-5`) - Deletes `it-selenium`; adds `it-playwright` with `NewUserJourneyIT`, - `OidcLoginIT`, `LoginPageIT` and `WebServicesIT`. `WebServicesIT` is the only - coverage of the forked XML-RPC and AtomPub servlets and it already caught the - AtomPub basic-auth 401 bug that exists on master. CI runs it three ways: db on - Jetty/Derby, plus oidc and db-oidc against docker-compose. - Open question from mbien on the PR — "what is the trigger for the test - framework swap?" — still needs an answer in the PR description. Suggested - answer: Struts 2.5.30+ breaks the Selenium tests, which is why - `struts.version` has been pinned at 2.5.29 for years, and the suite covers - one auth method out of five. -5. **#161 — Replace ROME Propono AtomPub server with self-contained StAX implementation** (snoopdave, 40 files, targets `master`) - RFC 5023 server on JDK StAX and plain DTOs. New `RollerAtomServlet`, wire - model, `AtomWriter`/`AtomReader` (DTDs and external entities disabled), 34 - tests including a full lifecycle integration test against in-memory Derby and - RELAX NG schema validation with Jing. **Merges last**, rebased onto the - Jakarta tree — see section 6. - -## 4. Branch and version plan - -``` -6.1.6 (tag) - └── roller-6.1.x <- cut now, maintenance only, matches roller-6.0.x/roller-5.2.x convention -master <- becomes 7.0.0-SNAPSHOT, is Roller 7 development -``` - -Steps, in order: - -1. Once 6.1.6 is voted through, cut `roller-6.1.x` from the release tag. - Nothing goes there unless a security report forces a 6.1.7. -2. On `master`: `roller.version` and `` to `7.0.0-SNAPSHOT` in the - parent pom, `app`, `db-utils`, `assembly-release`, `it-selenium` - (until #157 deletes it), and the Docker files. -3. Land #159, #151, #160. -4. Rebase and merge the stack: #154, then #155, then #156, then #157, each - retargeted to `master` as its parent lands. -5. Rebase and merge #161. -6. Modernization sweep (section 7). -7. Beta, then release (section 8). - -The stack merges as four separate PRs, not one squash. The commit-per-phase -structure in #154 is worth keeping in history — when something breaks in -production a year from now, "which phase" is the first question. - -## 5. Reconciliation: 6.1.6 master vs. the stack - -These are decisions, not conflicts a rebase can resolve. Each needs an answer -before #154 merges, and each answer belongs in the 7.0.0 upgrade notes. - -| Collision | 6.1.6 master says | The stack says | Call | -|---|---|---|---| -| **WSSE AtomPub auth** | Retired (#166). `webservices.atomPubAuth` takes `basic` and `oauth`; `wsse` fails closed on startup. | #154 keeps WSSE and advertises "basic or wsse"; #161 also removes WSSE. | Master wins: WSSE stays dead. Strip the WSSE paths and admin labels from #154 during the rebase. | -| **OAuth 1.0a for AtomPub** | Kept, and hardened — #165 fixed the authorize servlet's session handling. | #154 deletes it: `net.oauth` is unmaintained and javax-only. | Stack wins: OAuth 1.0a goes. Note in the upgrade guide that AtomPub is basic-only in 7.0, and that #155's OIDC covers browser login, not AtomPub. Decide whether `OAuthManager`, `OAuthAccessorRecord` and the `roller_oauth*` tables get dropped or left inert — a schema drop needs a migration script. | -| **Trackback** | Both directions removed (#163, #178). `TrackbackServlet`, `WeblogTrackbackRequest` and the outbound action are gone. | #154 still edits those files (they exist at its branch point). | Master wins. These become delete-vs-modify conflicts — resolve to delete. | -| **Authoring UI inline JS** | Moved to data attributes across the authoring JSPs (#168). | #156 rewrites the same JSPs for Bootstrap 5. | Rebase #156 onto the data-attribute markup, not the other way round. This is the largest mechanical conflict in the stack — 55 overlapping files — and the screenshot baseline used for the Bootstrap 5 audit has to be regenerated afterwards. | -| **Media content types** | Derived from file content (#174). | Stack predates it. | Master wins; verify the Struts 7 `UploadedFilesAware` rework in #154 still routes through the content-sniffing path, and that `WebServicesIT` covers it. | -| **Enclosure metadata** | Stored as submitted, no remote fetch (#175). | Stack predates it. | Master wins. | -| **XML-RPC** | Weblog permission checks added (#164), vendor extension types disabled (#171). | #154 forks `XmlRpcServlet` into Roller. | Both. Make sure the forked servlet is wired behind the same checks — `WebServicesIT` publishes over XML-RPC and should assert the permission failure path too. | -| **Salt filters** | Submitted and response salts separated (#167); one-time salt work is on a local branch. | #154 touches `ValidateSaltFilter`. | Master wins. | -| **Frontpage / template resolution** | #169, #170, #172. | #160's themes touch theme resolution. | No conflict measured (0 overlap), but re-run `ThemeManagerTest` and the frontpage tests after #160 lands. | -| **Java baseline** | 11, CI on 11/17/21/25. | 17, CI on 17/21/25. | **21**, CI on 21 and 25. Our delta on top of #154: bump ``, drop the 17 leg, re-check Derby (10.17 is the Java 21 baseline — decide whether to take it or stay on 10.16.1.1), and confirm Mockito still instruments 25. | - -## 6. Propono removal (#161), rebased - -The chosen order means #161 lands on a tree where Propono has already been -forked in rather than removed. Concretely, after #154–#157 merge: - -1. Rebase `replace-propono-atompub` onto `master`. -2. Convert the new AtomPub classes to `jakarta.servlet.*`. `javax.xml.stream` is - JDK API and does **not** move — that was the point of choosing StAX. -3. Delete the forked Propono `AtomServlet` and `RollerAtomRequestImpl` that #154 - brought in, plus the null-path-info normalization that was ported into it — - `RollerAtomServlet` must carry that behavior instead (a request to the bare - `/roller-services/app` mapping serves the service document; #157's smoke test - guards it). -4. Drop `rome-propono` from `app/pom.xml` and remove the `` comment and the `rome.version` pin comment. -5. Address Matt's review — nine verified findings, of which at least these are - blockers: - - `readBody()` reads the whole request body with `readAllBytes()` before any - quota check. Propono streamed to a temp file. As written, any authenticated - user can OOM the server with a large POST to the media collection. Stream - to a temp file or bound the read against the media quota. - - `getElementText()` throws on `type="xhtml"` content, summary and title, - which RFC 4287 allows and ROME accepted. Clients publishing xhtml get a - 500. Needs a branch that captures child XML as a string. - - `MediaCollection.deleteEntry()` strips the `.media-link` suffix for the - filename but passes the unstripped path to `getMediaFileByPath`, so DELETE - on the server's own advertised `rel="edit"` URI always NPEs. Pre-existing, - carried forward; fix it and add a delete test. - - Unrecognized `webservices.atomPubAuth` values must fail closed with a log - line naming the property and the valid options, the way 6.1.6 handles a - stored `wsse`. -6. Port `WebServicesIT` from #157 onto the StAX implementation — Matt offered to - do this and it is the highest-leverage follow-up on the PR. -7. Run an over-the-wire exerciser (APE or equivalent) against a deployed - instance. The unit tests do not exercise HTTP transport or BASIC auth over - the wire, and the previous format was ROME-generated, so interop is the risk. - -Then, and only then, unpin ROME. - -## 7. Modernization sweep after the stack lands - -Ordered by value, not effort: - -1. **Unpin ROME.** `rome.version` is at 1.19.0, pinned by a comment in - `app/pom.xml` saying the next version removes Propono. With #161 in, take the current ROME for - feed rendering and the Planet aggregator. This is the whole reason Propono had - to go. -2. **Unpin Struts.** `struts.version` is pinned at 2.5.29 with "`.30+` breaks - selenium tests". #154 goes to 7.1.1 and #157 deletes the Selenium suite, so - delete the comment and the reason for it. -3. **Decide XML-RPC's future.** Roller carries a fork of `XmlRpcServlet` because - `org.apache.xmlrpc` 3.1.3 is long unmaintained and has no Jakarta release. Blogger and - MetaWeblog are legacy APIs; 6.1.6 already had to add permission checks and - disable vendor extension types on them. Either commit to owning the fork or - put deprecation of XML-RPC on the 7.x roadmap. Worth a dev@ thread. -4. **Guice.** Still on `com.google.inject`. Check the version, and whether the - EclipseLink 5 / Spring 7 tree makes a straight Spring DI migration cheap - enough to be worth doing while everything else is already moving. -5. **Velocity 2.4.1 and the template layer.** No action forced by Jakarta, but - confirm the rendering path is clean on 21 and that the three new themes from - #160 render on the Bootstrap 5 admin. -6. **Re-enable dependabot** against the 7.0 tree once the versions settle, with - grouped PRs so we do not get another three-year backlog of singles. -7. **Docs.** `docs/roller-install-guide.adoc`, `roller-user-guide.adoc` and - `roller-template-guide.adoc` all describe a javax/Tomcat 9/OpenID world. - The install guide needs Java 21, Tomcat 11, the OIDC configuration, and the - new `authentication.method` values. The user guide needs the Bootstrap 5 - screens reshot. -8. **Docker.** Tomcat 11 base image, PostgreSQL 16, Keycloak for the OIDC demo, - and the compose file that #151 was trying to fix. -9. **Local branches to reconcile or delete.** `safer-defaults` (4 commits ahead), - `one-time-salt` (1), `jakarta` (2) — decide whether any of that is Roller 7 - material before the rebase makes them unmergeable. `remove-solr`, - `jakara-migration`, `jstl-not-provided` and `parse-referrer` are level with - master and can be deleted. - -## 8. Release engineering - -Roller 7.0.0 is a bigger release than anything since 6.0, and the changes users -will actually feel are the removals. The release notes lead with those, not with -the framework versions. - -**Upgrade notes must cover:** - -- Java 21 required. Java 11 and 17 no longer supported. -- Tomcat 11 (Jakarta) required. Tomcat 9 will not run Roller 7. -- `authentication.method`: `openid` and `db-openid` are gone; use `oidc` / - `db-oidc` and configure a provider. Existing OpenID 2.0 identities in - `openIdUrl` are reused by the OIDC account linking path — document what - happens to a user whose provider is dead. -- AtomPub: OAuth 1.0a and WSSE both gone. Basic auth only. -- XML-RPC and AtomPub survive but are reimplemented — anyone with a custom - client should retest. -- Trackback (both directions) already removed in 6.1.6; repeat it here for - people upgrading from 6.1.5 or earlier. -- Any schema change from dropping the OAuth 1.0a tables, with a migration - script and a "you may leave them in place" option. - -**Process:** follow the existing release runbook — the `roller-release` skill has -the RC, signing, staging, VOTE and promotion steps. Two things specific to this -release: - -- Ship at least one beta or RC that the dev@ list is actually asked to deploy. - A Jakarta migration plus an auth migration plus a UI migration is not something - to discover in the GA vote. -- The 72-hour VOTE window will not be enough for people to test this properly. - Announce the beta on dev@ and user@ with a deadline of its own. - -**Definition of done for 7.0.0:** - -- All four stack PRs plus #159, #151, #160 and #161 merged to `master`. -- `grep -r "javax\." app/src/main/java` returns only `javax.xml.stream`, - `javax.sql`, `javax.naming`, `javax.imageio` — JDK packages, not Java EE. -- No `rome-propono` in any pom; ROME on a current version. -- Playwright suite green on all three CI legs. -- Unit tests green on 21 and 25. -- Docker compose comes up and the new-user journey passes against it. -- Install guide reflects Java 21 / Tomcat 11 / OIDC. -- A real AtomPub client round-trips against a deployed instance. - -## 9. Risks - -| Risk | Mitigation | -|---|---| -| The #156 rebase onto the 6.1.6 authoring JSPs is the single biggest source of silent breakage — 55 overlapping files, and the Bootstrap 5 audit baseline is now stale. | Regenerate the screenshot baseline from post-6.1.6 master before rebasing, and re-run the 24-page comparison after. | -| AtomPub wire-format regression is invisible to the JUnit suite. | #157's `WebServicesIT` plus an over-the-wire exerciser before the vote. Do not ship on unit tests alone. | -| Java 21 narrows the deployment base right when we are also asking users to move to Tomcat 11. | It is one message either way — "Roller 7 needs a current stack". Say it once, loudly, in the release notes. | -| The stack is one contributor's work and it is large. If Matt goes quiet mid-rebase, the PMC owns 187 files of migration it did not write. | Review #154 phase by phase now, while he is around to answer. Two committers should be able to explain the Struts 7 parameter-binding and Spring Security authorization-manager changes without him. | -| A security report arrives mid-flight and forces a 6.1.7. | `roller-6.1.x` exists from day one so a fix does not have to be cherry-picked out of a half-migrated master. | -| Long-lived feature branches drift again. | Merge the stack in weeks, not months. Nothing in section 7 blocks a merge. | - -## 10. Open questions for dev@roller - -1. Java 21 or 17 as the 7.0 baseline — this plan says 21; the PMC should agree - before #154 merges, because it is much cheaper to decide now than after. -2. Does XML-RPC (Blogger/MetaWeblog) have a future, given we now maintain a fork - of its servlet? -3. Do the OAuth 1.0a tables get dropped in 7.0 or left inert? -4. Beta timing and how long the beta window stays open before the GA vote. -5. Should the three new themes from #160 change the default for new weblogs, or - does Basic stay the default? (#160 explicitly preserves Basic — confirm that - is what we want long term.) From aa5f0155a0a8e0d58194779e3429ddac1a468800 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 13:54:24 -0400 Subject: [PATCH 3/7] Keep installer status checks usable without request context --- .../org/apache/roller/weblogger/ui/struts2/core/Install.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java index 04d71c5f1c..9fffa8a84e 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Install.java @@ -78,8 +78,6 @@ public boolean isWeblogRequired() { @Override public String execute() { - if (!BootstrapSecurity.isValid(ServletActionContext.getRequest())) return BOOTSTRAP; - if (WebloggerFactory.isBootstrapped()) { return SUCCESS; } From 3d35e0e659146876738915df2560e3c9f3f9f2ba Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 14:35:12 -0400 Subject: [PATCH 4/7] Preserve first-user role assignment behavior --- .../roller/weblogger/business/jpa/JPAUserManagerImpl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java b/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java index 7c7d1d5d28..d83bac4261 100644 --- a/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java +++ b/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAUserManagerImpl.java @@ -97,9 +97,7 @@ public void addUser(User newUser) throws WebloggerException { boolean adminUser = false; List existingUsers = this.getUsers(Boolean.TRUE, null, null, 0, 1); - boolean firstUserAdmin = WebloggerConfig.getBooleanProperty("users.firstUserAdmin") - && (org.apache.roller.weblogger.ui.core.security.BootstrapSecurity.isCompleted() - || org.apache.roller.weblogger.ui.core.security.BootstrapSecurity.initialAdminScope()); + boolean firstUserAdmin = WebloggerConfig.getBooleanProperty("users.firstUserAdmin"); if (existingUsers.isEmpty() && firstUserAdmin) { // Make first user an admin adminUser = true; From 129f6f8b00f5201f8be09681d85c0ddf19a42ecf Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 15:35:31 -0400 Subject: [PATCH 5/7] Secure initial Roller setup with one-time token --- .../ui/core/filters/BootstrapFilter.java | 1 + .../core/filters/BootstrapSecurityFilter.java | 9 +- .../ui/core/security/BootstrapSecurity.java | 95 ++++++++++++++++--- .../ui/struts2/core/BootstrapToken.java | 89 ++++++++++++++--- .../resources/ApplicationResources.properties | 10 ++ app/src/main/resources/struts.xml | 5 +- .../WEB-INF/jsps/core/BootstrapToken.jsp | 30 ++++-- app/src/main/webapp/WEB-INF/tiles.xml | 4 +- 8 files changed, 209 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java index 42d8fd059d..a9d20c2283 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapFilter.java @@ -78,6 +78,7 @@ private boolean isInstallUrl(String uri) { || uri.endsWith("create.rol") || uri.endsWith("upgrade.rol") || uri.endsWith("bootstrap-token.rol") + || uri.endsWith("bootstrap-token!redeem.rol") || uri.endsWith(".js") || uri.endsWith(".css"))); } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java index a01e9504fb..32b63922c1 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/BootstrapSecurityFilter.java @@ -12,9 +12,14 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) HttpServletRequest r = (HttpServletRequest) req; HttpServletResponse p = (HttpServletResponse) res; String uri = r.getRequestURI(); - boolean installer = uri != null && (uri.contains("/roller-ui/install/") || uri.endsWith("/roller-ui/register.rol") || uri.endsWith("/roller-ui/register!save.rol") || uri.endsWith("/roller-ui/setup.rol")); + boolean tokenPage = uri != null && (uri.endsWith("/bootstrap-token.rol") + || uri.endsWith("/bootstrap-token!redeem.rol")); + boolean installer = uri != null && (tokenPage || uri.contains("/roller-ui/install/") + || uri.endsWith("/roller-ui/register.rol") + || uri.endsWith("/roller-ui/register!save.rol") + || uri.endsWith("/roller-ui/setup.rol")); if (installer && !BootstrapSecurity.isCompleted()) { - if (uri.endsWith("/bootstrap-token.rol")) { chain.doFilter(req, res); return; } + if (tokenPage) { chain.doFilter(req, res); return; } if (!BootstrapSecurity.isValid(r)) { p.sendRedirect(r.getContextPath() + "/roller-ui/bootstrap-token.rol"); return; } } chain.doFilter(req, res); diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java index ba69facb57..7fa6ed5343 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/BootstrapSecurity.java @@ -1,9 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ + package org.apache.roller.weblogger.ui.core.security; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; -import java.time.Instant; import java.util.Base64; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -11,6 +29,7 @@ /** Process-scoped gate for the unauthenticated installation flow. */ public final class BootstrapSecurity { + public static final String COMPLETION_PROPERTY = "bootstrap.completed"; private static final Log LOG = LogFactory.getLog(BootstrapSecurity.class); private static final long LIFETIME_MS = 60 * 60 * 1000L; @@ -19,37 +38,87 @@ public final class BootstrapSecurity { private static long expires; private static volatile boolean completed; private static final ThreadLocal INITIAL = new ThreadLocal<>(); - private BootstrapSecurity() { } + private BootstrapSecurity() { + } public static synchronized void start() { - if (completed || digest != null) return; + if (completed || digest != null) { + return; + } byte[] raw = new byte[32]; new SecureRandom().nextBytes(raw); String token = Base64.getUrlEncoder().withoutPadding().encodeToString(raw); digest = sha256(token); expires = System.currentTimeMillis() + LIFETIME_MS; - LOG.warn("Roller initial setup is locked. The one-time setup token (expires in 60 minutes) is: " + token); + LOG.warn(box( + "ROLLER INITIAL SETUP REQUIRED", + "", + "Open Roller in a web browser.", + "You will be redirected to the secure initial-setup page.", + "", + "Enter this one-time setup token (expires in 60 minutes):", + token)); + } + + public static boolean isCompleted() { + return completed; + } + + public static void complete() { + completed = true; + digest = null; + } + + public static void beginInitialAdmin() { + INITIAL.set(Boolean.TRUE); + } + + public static void endInitialAdmin() { + INITIAL.remove(); + } + + public static boolean initialAdminScope() { + return Boolean.TRUE.equals(INITIAL.get()); } - public static boolean isCompleted() { return completed; } - public static void complete() { completed = true; digest = null; } - public static void beginInitialAdmin() { INITIAL.set(Boolean.TRUE); } - public static void endInitialAdmin() { INITIAL.remove(); } - public static boolean initialAdminScope() { return Boolean.TRUE.equals(INITIAL.get()); } public static boolean isValid(HttpServletRequest request) { Object grant = request.getSession(false) == null ? null : request.getSession(false).getAttribute(SESSION); return !completed && grant instanceof Long && ((Long) grant) >= System.currentTimeMillis(); } + public static synchronized boolean redeem(HttpServletRequest request, String token) { - if (completed || digest == null || token == null || System.currentTimeMillis() > expires) return false; + if (completed || digest == null || token == null || System.currentTimeMillis() > expires) { + return false; + } byte[] supplied = sha256(token); - if (!MessageDigest.isEqual(digest, supplied)) return false; + if (!MessageDigest.isEqual(digest, supplied)) { + return false; + } request.getSession(true).setAttribute(SESSION, System.currentTimeMillis() + LIFETIME_MS); digest = null; return true; } + private static byte[] sha256(String value) { - try { return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); } - catch (Exception e) { throw new IllegalStateException(e); } + try { + return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static String box(String... lines) { + int width = 0; + for (String line : lines) { + width = Math.max(width, line.length()); + } + + String border = "+" + "-".repeat(width + 2) + "+"; + StringBuilder message = new StringBuilder("\n").append(border); + for (String line : lines) { + message.append("\n| ").append(line) + .append(" ".repeat(width - line.length())).append(" |"); + } + return message.append("\n").append(border).toString(); } } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java index cbe1cc5a14..0d4ce17bcb 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/BootstrapToken.java @@ -1,23 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ + package org.apache.roller.weblogger.ui.struts2.core; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.roller.weblogger.ui.core.security.BootstrapSecurity; import org.apache.roller.weblogger.ui.struts2.util.UIAction; import org.apache.struts2.interceptor.ServletRequestAware; import org.apache.struts2.interceptor.ServletResponseAware; + public class BootstrapToken extends UIAction implements ServletRequestAware, ServletResponseAware { - private HttpServletRequest request; private HttpServletResponse response; private String token; - public String execute() { return INPUT; } + + private static final Log LOG = LogFactory.getLog(BootstrapToken.class); + + private HttpServletRequest request; + private HttpServletResponse response; + private String token; + + public BootstrapToken() { + this.pageTitle = "installer.bootstrap.pageTitle"; + } + + public String execute() { + setResponseHeaders(); + LOG.info("Roller is waiting for an administrator to submit the one-time setup token; " + + "database setup will not continue until the token is accepted. Setup page: " + + request.getRequestURL()); + return INPUT; + } + public String redeem() { + setResponseHeaders(); + if (!"POST".equalsIgnoreCase(request.getMethod())) { + addActionError(getText("installer.bootstrap.postRequired")); + return INPUT; + } + if (BootstrapSecurity.redeem(request, token == null ? null : token.trim())) { + LOG.info("One-time setup token accepted; continuing database setup."); + return SUCCESS; + } + addActionError(getText("installer.bootstrap.invalidToken")); + return INPUT; + } + + private void setResponseHeaders() { response.setHeader("Cache-Control", "no-store"); response.setHeader("Referrer-Policy", "no-referrer"); - if (!"POST".equalsIgnoreCase(request.getMethod())) { addActionError("POST required"); return INPUT; } - if (BootstrapSecurity.redeem(request, token)) return SUCCESS; - addActionError("Invalid or expired setup token"); return INPUT; - } - public void setToken(String token) { this.token = token; } - public void setServletRequest(HttpServletRequest request) { this.request = request; } - public void setServletResponse(HttpServletResponse response) { this.response = response; } - public boolean isUserRequired() { return false; } - public boolean isWeblogRequired() { return false; } + } + + public void setToken(String token) { + this.token = token; + } + + public void setServletRequest(HttpServletRequest request) { + this.request = request; + } + + public void setServletResponse(HttpServletResponse response) { + this.response = response; + } + + public boolean isUserRequired() { + return false; + } + + public boolean isWeblogRequired() { + return false; + } } diff --git a/app/src/main/resources/ApplicationResources.properties b/app/src/main/resources/ApplicationResources.properties index 9e6a9e9575..1aba85d745 100644 --- a/app/src/main/resources/ApplicationResources.properties +++ b/app/src/main/resources/ApplicationResources.properties @@ -585,6 +585,16 @@ inviteMember.disabled=invitations disabled installer.bannerTitleLeft=Apache Roller installer.bannerTitleRight=Auto-Installer +# secure initial setup +installer.bootstrap.pageTitle=Roller initial setup +installer.bootstrap.heading=Secure initial setup +installer.bootstrap.instructions=Enter the one-time setup token printed in the Roller server log. +installer.bootstrap.tokenLabel=Setup token +installer.bootstrap.tokenHelp=The token expires 60 minutes after Roller starts and can only be used once. +installer.bootstrap.submit=Continue +installer.bootstrap.postRequired=The setup token must be submitted using this form. +installer.bootstrap.invalidToken=The setup token is invalid or has expired. + # database error installer.error.connection.pageTitle=Database connection error installer.cannotConnectToDatabase=Cannot connect to database diff --git a/app/src/main/resources/struts.xml b/app/src/main/resources/struts.xml index 1356bc56f4..ea14c602a4 100644 --- a/app/src/main/resources/struts.xml +++ b/app/src/main/resources/struts.xml @@ -142,7 +142,10 @@ .BootstrapToken - install + + install + /roller-ui/install + execute,redeem diff --git a/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp b/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp index b13afa8c33..191499a26c 100644 --- a/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/core/BootstrapToken.jsp @@ -1,7 +1,25 @@ <%@ include file="/WEB-INF/jsps/taglibs-struts2.jsp" %> -

Roller initial setup

-

Enter the one-time setup token printed in the Roller server log.

- - - - + +
+
+

+

+ + + +
+ + +

+
+
+ +
+
+
+
diff --git a/app/src/main/webapp/WEB-INF/tiles.xml b/app/src/main/webapp/WEB-INF/tiles.xml index 990cbb385e..d3750ef866 100644 --- a/app/src/main/webapp/WEB-INF/tiles.xml +++ b/app/src/main/webapp/WEB-INF/tiles.xml @@ -142,8 +142,10 @@ - + + + From 7282293f50b9b56339263574020a6cc40035c8e3 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 15:40:24 -0400 Subject: [PATCH 6/7] Skip empty database upgrade flow --- .../business/startup/DatabaseInstaller.java | 10 ++++-- .../startup/DatabaseInstallerUpgradeTest.java | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/apache/roller/weblogger/business/startup/DatabaseInstaller.java b/app/src/main/java/org/apache/roller/weblogger/business/startup/DatabaseInstaller.java index 684a8aaa38..d0892c612d 100644 --- a/app/src/main/java/org/apache/roller/weblogger/business/startup/DatabaseInstaller.java +++ b/app/src/main/java/org/apache/roller/weblogger/business/startup/DatabaseInstaller.java @@ -48,6 +48,9 @@ public class DatabaseInstaller { // the name of the property which holds the dbversion value private static final String DBVERSION_PROP = "roller.database.version"; + // Update this when adding a database or data migration step below. + private static final int LATEST_DATABASE_UPGRADE_VERSION = 610; + public DatabaseInstaller(DatabaseProvider dbProvider, DatabaseScriptProvider scriptProvider) { db = dbProvider; @@ -123,9 +126,12 @@ public boolean isUpgradeRequired() { } return false; - } else { - return databaseVersion < desiredVersion; } + + // A product release does not always change the database. Do not send + // administrators through the upgrade UI for a version-only change. + int requiredDatabaseVersion = Math.min(desiredVersion, LATEST_DATABASE_UPGRADE_VERSION); + return databaseVersion < requiredDatabaseVersion; } diff --git a/app/src/test/java/org/apache/roller/weblogger/business/startup/DatabaseInstallerUpgradeTest.java b/app/src/test/java/org/apache/roller/weblogger/business/startup/DatabaseInstallerUpgradeTest.java index 94dbb0129a..0250ad5a04 100644 --- a/app/src/test/java/org/apache/roller/weblogger/business/startup/DatabaseInstallerUpgradeTest.java +++ b/app/src/test/java/org/apache/roller/weblogger/business/startup/DatabaseInstallerUpgradeTest.java @@ -14,6 +14,41 @@ import static org.junit.jupiter.api.Assertions.*; class DatabaseInstallerUpgradeTest { + + @Test + void patchReleaseWithoutDatabaseChangesDoesNotRequireUpgrade() throws Exception { + DatabaseProvider db = mock(DatabaseProvider.class); + Connection con = mock(Connection.class); + Statement query = mock(Statement.class); + ResultSet rows = mock(ResultSet.class); + when(db.getConnection()).thenReturn(con); + when(con.createStatement()).thenReturn(query); + when(query.executeQuery(anyString())).thenReturn(rows); + when(rows.next()).thenReturn(true); + when(rows.getString(1)).thenReturn("615"); + + DatabaseInstaller installer = new DatabaseInstaller(db, mock(DatabaseScriptProvider.class)); + + assertFalse(installer.isUpgradeRequired()); + } + + @Test + void pendingDatabaseMigrationRequiresUpgrade() throws Exception { + DatabaseProvider db = mock(DatabaseProvider.class); + Connection con = mock(Connection.class); + Statement query = mock(Statement.class); + ResultSet rows = mock(ResultSet.class); + when(db.getConnection()).thenReturn(con); + when(con.createStatement()).thenReturn(query); + when(query.executeQuery(anyString())).thenReturn(rows); + when(rows.next()).thenReturn(true); + when(rows.getString(1)).thenReturn("520"); + + DatabaseInstaller installer = new DatabaseInstaller(db, mock(DatabaseScriptProvider.class)); + + assertTrue(installer.isUpgradeRequired()); + } + @Test void versionOnlyUpgradeReportsCompletion() throws Exception { DatabaseProvider db = mock(DatabaseProvider.class); From 20cc99c5326fea5e1678271af98ebfcbc547c27e Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sun, 13 Sep 2026 16:26:50 -0400 Subject: [PATCH 7/7] Fix Selenium initial login flow for setup token gate --- it-selenium/pom.xml | 9 ++- .../roller/selenium/InitialLoginTestIT.java | 7 +- .../selenium/core/BootstrapTokenPage.java | 71 +++++++++++++++++++ .../selenium/core/BootstrapTokenPageTest.java | 46 ++++++++++++ 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPage.java create mode 100644 it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPageTest.java diff --git a/it-selenium/pom.xml b/it-selenium/pom.xml index fabe549a5b..9544eb12f8 100644 --- a/it-selenium/pom.xml +++ b/it-selenium/pom.xml @@ -32,7 +32,7 @@ roller-selenium-tests war - diff --git a/it-selenium/src/test/java/org/apache/roller/selenium/InitialLoginTestIT.java b/it-selenium/src/test/java/org/apache/roller/selenium/InitialLoginTestIT.java index a5de111cf3..692d1a0258 100644 --- a/it-selenium/src/test/java/org/apache/roller/selenium/InitialLoginTestIT.java +++ b/it-selenium/src/test/java/org/apache/roller/selenium/InitialLoginTestIT.java @@ -18,8 +18,10 @@ package org.apache.roller.selenium; import java.awt.GraphicsEnvironment; +import java.nio.file.Paths; import java.time.Duration; import java.time.temporal.ChronoUnit; +import org.apache.roller.selenium.core.BootstrapTokenPage; import org.apache.roller.selenium.core.CreateWeblogPage; import org.apache.roller.selenium.core.LoginPage; import org.apache.roller.selenium.core.MainMenuPage; @@ -66,14 +68,15 @@ public void setUp() throws Exception { driver.manage().timeouts().implicitlyWait(Duration.of(5, ChronoUnit.SECONDS)) .pageLoadTimeout(Duration.of(5, ChronoUnit.SECONDS)) .scriptTimeout(Duration.of(5, ChronoUnit.SECONDS)); - baseUrl = "http://localhost:8080/roller/"; + baseUrl = System.getProperty("roller.test.baseUrl", "http://localhost:8080/roller/"); } @Test public void testInitialLogin() throws Exception { // create new user and first blog driver.get(baseUrl); - SetupPage sp = new SetupPage(driver); + SetupPage sp = new BootstrapTokenPage(driver).unlock(Paths.get( + System.getProperty("roller.test.logFile", "logs/roller.log"))); RegisterPage rp = sp.createNewUser(); WelcomePage wp = rp.submitUserRegistration("bsmith", "Bob Smith", "bsmith@email.com", "roller123"); diff --git a/it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPage.java b/it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPage.java new file mode 100644 index 0000000000..9beb797e0e --- /dev/null +++ b/it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPage.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package org.apache.roller.selenium.core; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.roller.selenium.AbstractRollerPage; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +/** The operator-only token step before first-user registration. */ +public class BootstrapTokenPage extends AbstractRollerPage { + + private static final Pattern TOKEN = Pattern.compile( + "Enter this one-time setup token[^\\r\\n]*\\R\\| ([A-Za-z0-9_-]{43}) +\\|"); + + public BootstrapTokenPage(WebDriver driver) { + this.driver = driver; + verifyPageTitle("Roller initial setup"); + } + + public SetupPage unlock(Path serverLog) { + // Read the same server-side log as an operator; never print its token. + // Logging is asynchronous, so allow time for the startup message to flush. + String token = new WebDriverWait(driver, Duration.ofSeconds(10)) + .withMessage("No initial setup token found in " + serverLog) + .until(ignored -> readToken(serverLog)); + setFieldValue("setup-token", token); + clickById("setup-token-submit"); + new WebDriverWait(driver, Duration.ofSeconds(10)).until( + ExpectedConditions.titleIs("Front Page: Welcome to Roller!")); + return new SetupPage(driver); + } + + private static String readToken(Path serverLog) { + try { + return Files.exists(serverLog) ? latestToken(Files.readString(serverLog)) : null; + } catch (IOException ex) { + throw new IllegalStateException("Cannot read Roller server log: " + serverLog, ex); + } + } + + static String latestToken(String log) { + Matcher matcher = TOKEN.matcher(log); + String token = null; + while (matcher.find()) { + token = matcher.group(1); + } + return token; + } +} diff --git a/it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPageTest.java b/it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPageTest.java new file mode 100644 index 0000000000..52a182a6e4 --- /dev/null +++ b/it-selenium/src/test/java/org/apache/roller/selenium/core/BootstrapTokenPageTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package org.apache.roller.selenium.core; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class BootstrapTokenPageTest { + + @Test + public void readsLatestBoxedToken() { + String first = "a".repeat(43); + String latest = "_-" + "b".repeat(41); + String log = box(first, "\n") + box(latest, "\r\n"); + assertEquals(latest, BootstrapTokenPage.latestToken(log)); + } + + @Test + public void ignoresUnrelatedOrIncompleteLogMessages() { + assertNull(BootstrapTokenPage.latestToken("| " + "a".repeat(43) + " |\n")); + assertNull(BootstrapTokenPage.latestToken( + "Enter this one-time setup token (expires in 60 minutes): |\n| partial")); + } + + private static String box(String token, String newline) { + return "WARN BootstrapSecurity -" + newline + + "| Enter this one-time setup token (expires in 60 minutes): |" + newline + + "| " + token + " |" + newline; + } +}