From ac83b2fd16e7642e5b05db8d964789d3a385cebe Mon Sep 17 00:00:00 2001 From: mbiuki Date: Mon, 4 May 2026 14:21:40 -0400 Subject: [PATCH 01/10] fix(publisher): parameterize getPublishAuditStatuses bundle-id query Replaces the manually quoted IN-clause with bound parameters so caller-supplied bundle ids cannot break out of the SQL literal. Also guards against null/empty input (which previously NPE'd or produced invalid `IN ()` SQL), and corrects the catch-block logger to use this class and ERROR level (matching the sibling deletePublishAuditStatus method). Refs: dotCMS/private-issues#581 --- .../business/PublishAuditAPIImpl.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java b/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java index b875079b65e9..27da9af58f14 100644 --- a/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -224,23 +225,29 @@ public PublishAuditStatus getPublishAuditStatus(String bundleId) @CloseDBIfOpened public List getPublishAuditStatuses(List bundleIds) throws DotPublisherException { + if (bundleIds == null || bundleIds.isEmpty()) { + return Collections.emptyList(); + } try { final List result = new ArrayList<>(); - DotConnect dc = new DotConnect(); - final List parameter = bundleIds.stream().map(id -> "'" + id + "'").collect(Collectors.toList()); + final DotConnect dc = new DotConnect(); + final String placeholders = bundleIds.stream() + .map(id -> "?") + .collect(Collectors.joining(",")); - dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, String.join(",", parameter))); - List> items = dc.loadObjectResults(); + dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders)); + bundleIds.forEach(dc::addParam); + final List> items = dc.loadObjectResults(); - for(Map item: items) { - result.add(turnIntoPublishAuditStatus(NO_LIMIT_ASSETS, item)); + for (final Map item : items) { + result.add(turnIntoPublishAuditStatus(NO_LIMIT_ASSETS, item)); } return result; - }catch(Exception e){ - Logger.debug(PublisherUtil.class,e.getMessage(),e); - throw new DotPublisherException("Unable to get list of elements with error:"+e.getMessage(), e); + } catch (Exception e) { + Logger.error(PublishAuditAPIImpl.class, e.getMessage(), e); + throw new DotPublisherException("Unable to get list of elements with error:" + e.getMessage(), e); } } From 8e0a64acb372fed37a85db8063887761dd237028 Mon Sep 17 00:00:00 2001 From: mbiuki Date: Mon, 4 May 2026 14:30:19 -0400 Subject: [PATCH 02/10] test(publisher): regression tests for getPublishAuditStatuses parameterization Adds four integration tests for getPublishAuditStatuses(List): - happy path returns the requested rows and excludes others - empty list returns an empty result without producing invalid `IN ()` SQL - null list returns an empty result without NPE - bundle ids containing SQL meta-characters (single quotes, OR 1=1, '; DROP TABLE; --, comment terminators, escaped quotes) are bound as parameters and produce no rows; the audit table remains intact Refs: dotCMS/private-issues#581 --- .../business/PublishAuditAPITest.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/dotcms-integration/src/test/java/com/dotcms/publisher/business/PublishAuditAPITest.java b/dotcms-integration/src/test/java/com/dotcms/publisher/business/PublishAuditAPITest.java index 543cd2facc38..52cce77af11a 100644 --- a/dotcms-integration/src/test/java/com/dotcms/publisher/business/PublishAuditAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/publisher/business/PublishAuditAPITest.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -386,6 +387,101 @@ public void allThePublishAuditHistoryWithOneAssets() throws DotPublisherExceptio }); } + /** + * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)} + * When: three audit statuses are inserted and two of them are requested by id + * Should: return exactly the two requested rows. + */ + @Test + public void test_getPublishAuditStatuses_returnsRequestedRows() throws DotPublisherException { + final String bundleId1 = UUIDGenerator.generateUuid(); + final String bundleId2 = UUIDGenerator.generateUuid(); + final String bundleId3 = UUIDGenerator.generateUuid(); + insertPublishAuditStatus(Status.SUCCESS, bundleId1); + insertPublishAuditStatus(Status.FAILED_TO_PUBLISH, bundleId2); + insertPublishAuditStatus(Status.SUCCESS, bundleId3); + + try { + final List result = publishAuditAPI.getPublishAuditStatuses( + Arrays.asList(bundleId1, bundleId2)); + + assertNotNull(result); + assertEquals(2, result.size()); + final Set ids = result.stream() + .map(PublishAuditStatus::getBundleId) + .collect(Collectors.toSet()); + assertTrue(ids.contains(bundleId1)); + assertTrue(ids.contains(bundleId2)); + assertFalse(ids.contains(bundleId3)); + } finally { + publishAuditAPI.deletePublishAuditStatus(bundleId1); + publishAuditAPI.deletePublishAuditStatus(bundleId2); + publishAuditAPI.deletePublishAuditStatus(bundleId3); + } + } + + /** + * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)} + * When: an empty list is passed + * Should: return an empty result, not throw or produce invalid SQL. + */ + @Test + public void test_getPublishAuditStatuses_emptyList_returnsEmpty() throws DotPublisherException { + final List result = publishAuditAPI.getPublishAuditStatuses(Collections.emptyList()); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + /** + * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)} + * When: null is passed + * Should: return an empty result, not throw NPE. + */ + @Test + public void test_getPublishAuditStatuses_nullList_returnsEmpty() throws DotPublisherException { + final List result = publishAuditAPI.getPublishAuditStatuses(null); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + /** + * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)} + * When: bundle ids contain SQL meta-characters (single quotes, OR 1=1, ; DROP TABLE) + * Should: bind the values as parameters, return an empty result, leave the + * publishing_queue_audit table intact. + * + * Regression test for the previous String.format-based IN-clause that + * concatenated quote-wrapped ids directly into the SQL. + */ + @Test + public void test_getPublishAuditStatuses_neutralizesSqlInjectionAttempts() throws DotPublisherException { + final String realBundleId = UUIDGenerator.generateUuid(); + insertPublishAuditStatus(Status.SUCCESS, realBundleId); + + try { + final List maliciousIds = Arrays.asList( + "x' OR '1'='1", + "x'; DROP TABLE publishing_queue_audit; --", + "x' UNION SELECT '" + realBundleId + "' --", + "x'/*", + "x\\' OR \\'1\\'=\\'1" + ); + + final List result = publishAuditAPI.getPublishAuditStatuses(maliciousIds); + + assertNotNull(result); + assertTrue("Malicious bundle ids should not match any rows; got " + + result.size() + " rows back, indicating values were not bound as parameters", + result.isEmpty()); + + final PublishAuditStatus stillThere = publishAuditAPI.getPublishAuditStatus(realBundleId); + assertNotNull("publishing_queue_audit row was lost after SQL injection attempt — " + + "DROP TABLE payload may have executed", stillThere); + } finally { + publishAuditAPI.deletePublishAuditStatus(realBundleId); + } + } + @Test public void test_getPublishAuditStatusByFilter() throws DotPublisherException, DotDataException { From 97009364f92d79b12819f4c37f3d9e0c8d2c9bd1 Mon Sep 17 00:00:00 2001 From: Mehdi <10160868+mbiuki@users.noreply.github.com> Date: Mon, 4 May 2026 14:36:51 -0400 Subject: [PATCH 03/10] Apply suggestions from code review Co-authored-by: semgrep-code-dotcms-test[bot] <183154938+semgrep-code-dotcms-test[bot]@users.noreply.github.com> --- .../dotcms/publisher/business/PublishAuditAPIImpl.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java b/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java index 27da9af58f14..b9c4320ca6cc 100644 --- a/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java @@ -236,7 +236,15 @@ public List getPublishAuditStatuses(List bundleIds) .map(id -> "?") .collect(Collectors.joining(",")); - dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders)); + // Build placeholders for the IN clause, e.g., "?, ?, ?" + String placeholders = bundleIds.stream().map(id -> "?").collect(Collectors.joining(",")); + String newQueryString = String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders); + dc.setSQL(newQueryString); + + // Add each bundleId as a parameter to safely bind it (prevents SQL injection) + for (String id : bundleIds) { + dc.addParam(id); + } bundleIds.forEach(dc::addParam); final List> items = dc.loadObjectResults(); From 0f3a942a398cbd9b70f65f4912f1aea98ce13413 Mon Sep 17 00:00:00 2001 From: mbiuki Date: Mon, 4 May 2026 14:58:30 -0400 Subject: [PATCH 04/10] fix: remove duplicate placeholders block from accidental suggestion apply The "Apply suggestion" action on the Semgrep bot's review (commit f68ff49) appended the bot's suggested code on top of the existing parameterized fix instead of replacing it, leaving two `placeholders` declarations in the same method (compile error: "variable placeholders is already defined"). The bot's suggestion was functionally identical to the committed code; the existing parameterized fix is correct. Removing the duplicate block. --- .../dotcms/publisher/business/PublishAuditAPIImpl.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java b/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java index b9c4320ca6cc..27da9af58f14 100644 --- a/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java @@ -236,15 +236,7 @@ public List getPublishAuditStatuses(List bundleIds) .map(id -> "?") .collect(Collectors.joining(",")); - // Build placeholders for the IN clause, e.g., "?, ?, ?" - String placeholders = bundleIds.stream().map(id -> "?").collect(Collectors.joining(",")); - String newQueryString = String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders); - dc.setSQL(newQueryString); - - // Add each bundleId as a parameter to safely bind it (prevents SQL injection) - for (String id : bundleIds) { - dc.addParam(id); - } + dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders)); bundleIds.forEach(dc::addParam); final List> items = dc.loadObjectResults(); From 3369c2bbc0e7ce610b9fcdf747205630c6145440 Mon Sep 17 00:00:00 2001 From: mbiuki Date: Mon, 4 May 2026 15:05:04 -0400 Subject: [PATCH 05/10] fix(rest): require backend user + publishing-queue portlet on AuditPublishingResource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds WebResource.InitBuilder authentication checks to GET /api/auditPublishing/get and POST /api/auditPublishing/getAll. Both endpoints previously skipped WebResource.init entirely, so an unauthenticated caller could enumerate publish audit status for any bundle id (or list of ids) — the SQL injection in the underlying DB method was the worst-case shape, but even after parameterization the data exposure remains until the resource is gated. Both endpoints now require: - a backend (CMS) user, not a frontend user - the `publishing-queue` portlet permission - rejection when no user is present (no anonymous access) Addresses review feedback from @wezell. --- .../dotcms/rest/AuditPublishingResource.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java b/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java index 0003c5e8eb97..f3dc1595c183 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java @@ -4,7 +4,10 @@ import com.dotcms.publisher.business.PublishAuditAPI; import com.dotcms.publisher.business.PublishAuditStatus; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.ws.rs.*; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -24,7 +27,18 @@ public class AuditPublishingResource { @GET @Path("/get/{bundleId:.*}") @Produces(MediaType.TEXT_XML) - public Response get(@PathParam("bundleId") String bundleId) { + public Response get(@PathParam("bundleId") final String bundleId, + @Context final HttpServletRequest request, + @Context final HttpServletResponse response) { + + new WebResource.InitBuilder() + .requiredBackendUser(true) + .requiredFrontendUser(false) + .requestAndResponse(request, response) + .rejectWhenNoUser(true) + .requiredPortlet("publishing-queue") + .init(); + PublishAuditStatus status = null; try { @@ -42,7 +56,18 @@ public Response get(@PathParam("bundleId") String bundleId) { @POST @Path("/getAll") @Produces(MediaType.APPLICATION_JSON) - public Response getAll( List bundleIds) { + public Response getAll(final List bundleIds, + @Context final HttpServletRequest request, + @Context final HttpServletResponse response) { + + new WebResource.InitBuilder() + .requiredBackendUser(true) + .requiredFrontendUser(false) + .requestAndResponse(request, response) + .rejectWhenNoUser(true) + .requiredPortlet("publishing-queue") + .init(); + try { final List statuses = auditAPI.getPublishAuditStatuses(bundleIds); From 37700cf3e7ea7d92cf41a23c5b25caa87d75d250 Mon Sep 17 00:00:00 2001 From: mbiuki Date: Mon, 4 May 2026 15:21:06 -0400 Subject: [PATCH 06/10] test: use dotCMS-specific SecurityException in AuditPublishingResourceTest The auth check throws com.dotcms.rest.exception.SecurityException (WebApplicationException subclass), not java.lang.SecurityException. @Test(expected = ...) defaults to the imported class, so a no-arg import was matching the wrong type and the test errored even though the auth worked correctly. --- .../rest/AuditPublishingResourceTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java b/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java new file mode 100644 index 000000000000..7226fbaaba85 --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java @@ -0,0 +1,52 @@ +package com.dotcms.rest; + +import com.dotcms.IntegrationTestBase; +import com.dotcms.rest.exception.SecurityException; +import com.dotcms.util.IntegrationTestInitService; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; + +import static org.mockito.Mockito.mock; + +/** + * Verifies that {@link AuditPublishingResource} enforces backend-user + * authentication on its endpoints. Both methods previously skipped + * {@link WebResource#init} entirely and were reachable anonymously. + */ +public class AuditPublishingResourceTest extends IntegrationTestBase { + + @BeforeClass + public static void prepare() throws Exception { + IntegrationTestInitService.getInstance().init(); + } + + /** + * Method to test: {@link AuditPublishingResource#get(String, HttpServletRequest, HttpServletResponse)} + * When: called with a request that carries no authenticated user + * Should: reject the call with a SecurityException. The resource is now + * backend-only and `rejectWhenNoUser(true)` is configured. + */ + @Test(expected = SecurityException.class) + public void test_get_rejectsAnonymousRequest() { + final HttpServletRequest request = mock(HttpServletRequest.class); + final HttpServletResponse response = mock(HttpServletResponse.class); + new AuditPublishingResource().get("any-bundle-id", request, response); + } + + /** + * Method to test: {@link AuditPublishingResource#getAll(java.util.List, HttpServletRequest, HttpServletResponse)} + * When: called with a request that carries no authenticated user + * Should: reject the call with a SecurityException. + */ + @Test(expected = SecurityException.class) + public void test_getAll_rejectsAnonymousRequest() { + final HttpServletRequest request = mock(HttpServletRequest.class); + final HttpServletResponse response = mock(HttpServletResponse.class); + new AuditPublishingResource().getAll(Collections.singletonList("any-bundle-id"), + request, response); + } +} From 408bcc6cdf2aaaeb2b568f2622c29851bba4a0bf Mon Sep 17 00:00:00 2001 From: ddariod Date: Wed, 6 May 2026 11:48:30 -0500 Subject: [PATCH 07/10] add AuditPublishingResourceTest to the MainSuite2a --- dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java index 88c1db2012ab..269a6592f6f2 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java @@ -105,7 +105,8 @@ com.dotmarketing.portlets.categories.business.CategoryAPITest.class, com.dotmarketing.filters.FiltersTest.class, InterceptorHandlerTest.class, - com.dotcms.graphql.datafetcher.page.NumberContentsDataFetcherTest.class + com.dotcms.graphql.datafetcher.page.NumberContentsDataFetcherTest.class, + com.dotcms.rest.AuditPublishingResourceTest.class, }) public class MainSuite2a { From 293c081adeaef9d50478779e0b5149253de75ee8 Mon Sep 17 00:00:00 2001 From: ddariod Date: Wed, 6 May 2026 13:49:46 -0500 Subject: [PATCH 08/10] use PP authorization in AuditPublishingResource instead of WebResource.InitBuilder --- .../dotcms/rest/AuditPublishingResource.java | 44 +++++++++---------- .../rest/AuditPublishingResourceTest.java | 39 ++++++++-------- ...ish_JWT_Token_Test.postman_collection.json | 10 +++++ 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java b/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java index f3dc1595c183..30c488a7762e 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java @@ -3,21 +3,19 @@ import com.dotcms.publisher.business.DotPublisherException; import com.dotcms.publisher.business.PublishAuditAPI; import com.dotcms.publisher.business.PublishAuditStatus; +import com.dotcms.publisher.pusher.AuthCredentialPushPublishUtil; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; -import com.google.common.collect.Lists; -import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; +import java.util.Optional; @Path("/auditPublishing") @Tag(name = "Publishing") @@ -28,16 +26,16 @@ public class AuditPublishingResource { @Path("/get/{bundleId:.*}") @Produces(MediaType.TEXT_XML) public Response get(@PathParam("bundleId") final String bundleId, - @Context final HttpServletRequest request, - @Context final HttpServletResponse response) { + @Context final HttpServletRequest request) { - new WebResource.InitBuilder() - .requiredBackendUser(true) - .requiredFrontendUser(false) - .requestAndResponse(request, response) - .rejectWhenNoUser(true) - .requiredPortlet("publishing-queue") - .init(); + final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken = + AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request); + + final Optional failResponse = PushPublishResourceUtil.getFailResponse(request, ppAuthToken); + + if (failResponse.isPresent()) { + return failResponse.get(); + } PublishAuditStatus status = null; @@ -57,16 +55,16 @@ public Response get(@PathParam("bundleId") final String bundleId, @Path("/getAll") @Produces(MediaType.APPLICATION_JSON) public Response getAll(final List bundleIds, - @Context final HttpServletRequest request, - @Context final HttpServletResponse response) { - - new WebResource.InitBuilder() - .requiredBackendUser(true) - .requiredFrontendUser(false) - .requestAndResponse(request, response) - .rejectWhenNoUser(true) - .requiredPortlet("publishing-queue") - .init(); + @Context final HttpServletRequest request) { + + final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken = + AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request); + + final Optional failResponse = PushPublishResourceUtil.getFailResponse(request, ppAuthToken); + + if (failResponse.isPresent()) { + return failResponse.get(); + } try { final List statuses = auditAPI.getPublishAuditStatuses(bundleIds); diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java b/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java index 7226fbaaba85..b176e7e8d97d 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java @@ -1,21 +1,21 @@ package com.dotcms.rest; import com.dotcms.IntegrationTestBase; -import com.dotcms.rest.exception.SecurityException; import com.dotcms.util.IntegrationTestInitService; import org.junit.BeforeClass; import org.junit.Test; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; import java.util.Collections; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; /** - * Verifies that {@link AuditPublishingResource} enforces backend-user - * authentication on its endpoints. Both methods previously skipped - * {@link WebResource#init} entirely and were reachable anonymously. + * Verifies that {@link AuditPublishingResource} enforces push-publish token + * authentication on its endpoints. Both methods were previously reachable + * anonymously; they now require a valid PP auth token. */ public class AuditPublishingResourceTest extends IntegrationTestBase { @@ -25,28 +25,27 @@ public static void prepare() throws Exception { } /** - * Method to test: {@link AuditPublishingResource#get(String, HttpServletRequest, HttpServletResponse)} - * When: called with a request that carries no authenticated user - * Should: reject the call with a SecurityException. The resource is now - * backend-only and `rejectWhenNoUser(true)` is configured. + * Method to test: {@link AuditPublishingResource#get(String, HttpServletRequest)} + * When: called with a request that carries no push-publish auth token + * Should: return 401 Unauthorized. */ - @Test(expected = SecurityException.class) + @Test public void test_get_rejectsAnonymousRequest() { final HttpServletRequest request = mock(HttpServletRequest.class); - final HttpServletResponse response = mock(HttpServletResponse.class); - new AuditPublishingResource().get("any-bundle-id", request, response); + final Response response = new AuditPublishingResource().get("any-bundle-id", request); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); } /** - * Method to test: {@link AuditPublishingResource#getAll(java.util.List, HttpServletRequest, HttpServletResponse)} - * When: called with a request that carries no authenticated user - * Should: reject the call with a SecurityException. + * Method to test: {@link AuditPublishingResource#getAll(java.util.List, HttpServletRequest)} + * When: called with a request that carries no push-publish auth token + * Should: return 401 Unauthorized. */ - @Test(expected = SecurityException.class) + @Test public void test_getAll_rejectsAnonymousRequest() { final HttpServletRequest request = mock(HttpServletRequest.class); - final HttpServletResponse response = mock(HttpServletResponse.class); - new AuditPublishingResource().getAll(Collections.singletonList("any-bundle-id"), - request, response); + final Response response = new AuditPublishingResource() + .getAll(Collections.singletonList("any-bundle-id"), request); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/main/resources/postman/Push_Publish_JWT_Token_Test.postman_collection.json b/dotcms-postman/src/main/resources/postman/Push_Publish_JWT_Token_Test.postman_collection.json index fcb1467194e1..6f0ef2cdb39d 100644 --- a/dotcms-postman/src/main/resources/postman/Push_Publish_JWT_Token_Test.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/Push_Publish_JWT_Token_Test.postman_collection.json @@ -284,6 +284,16 @@ } ], "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, "method": "GET", "header": [], "url": { From c1b78bd81d758ebab3f392cb39fb6f550101d743 Mon Sep 17 00:00:00 2001 From: ddariod Date: Wed, 6 May 2026 14:17:42 -0500 Subject: [PATCH 09/10] fix(publisher): add PP auth header to audit poll and prevent 500 on missing token Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/dotcms/publisher/business/PublisherQueueJob.java | 2 ++ .../publisher/pusher/AuthCredentialPushPublishUtil.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java b/dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java index 77a080228c90..1d43436d039f 100644 --- a/dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java +++ b/dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java @@ -15,6 +15,7 @@ import com.dotcms.publisher.endpoint.business.PublishingEndPointAPI; import com.dotcms.publisher.environment.bean.Environment; import com.dotcms.publisher.environment.business.EnvironmentAPI; +import com.dotcms.publisher.pusher.AuthCredentialPushPublishUtil; import com.dotcms.publisher.pusher.PushPublisher; import com.dotcms.publisher.pusher.PushPublisherConfig; import com.dotcms.publisher.util.PublisherUtil; @@ -653,6 +654,7 @@ private List getRemoteHistoryFromEndpoint(final List Date: Thu, 7 May 2026 19:59:22 -0400 Subject: [PATCH 10/10] fix(cicd): backport workflow_dispatch trigger to release-26.04.11-03 Backports the LTS workflow fix from main (PR #35612) to enable manual triggering with a version override and dynamic version resolution from .mvn/maven.config. Same change as d408d3ea33 (applied to release-26.04.28-03). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/cicd_5-lts.yml | 40 +++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd_5-lts.yml b/.github/workflows/cicd_5-lts.yml index e97641861ddb..e592bcf95a93 100644 --- a/.github/workflows/cicd_5-lts.yml +++ b/.github/workflows/cicd_5-lts.yml @@ -15,10 +15,17 @@ on: push: branches: - release-* + workflow_dispatch: + inputs: + version: + description: 'Build version for Docker image tagging (e.g. 26.04.28-03). Defaults to the revision in .mvn/maven.config when left empty.' + required: false + type: string + default: '' # Concurrency group to manage multiple runs concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} # Cancel any in-progress runs for the same branch/PR to prevent delays from changes during build cancel-in-progress: true @@ -30,16 +37,43 @@ jobs: with: change-detection: 'enabled' + # Resolve the build version: use the workflow_dispatch input when provided, otherwise + # read -Drevision and -Dchangelist from .mvn/maven.config on the current branch. + # This ensures the Docker image tag always matches what Maven computes for project.version. + setup: + name: Resolve Version + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + outputs: + version: ${{ steps.resolve.outputs.version }} + steps: + - uses: actions/checkout@v4 + - name: Resolve version from input or maven.config + id: resolve + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ -n "$INPUT_VERSION" ]; then + if [[ ! "$INPUT_VERSION" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}(-[0-9]{1,2}|_lts_v[0-9]{1,2})$ ]]; then + echo "::error::Invalid version format: '$INPUT_VERSION'. Expected format: yy.mm.dd-## (e.g. 26.04.28-03)" + exit 1 + fi + echo "version=$INPUT_VERSION" >> "$GITHUB_OUTPUT" + else + revision=$(grep -oP '(?<=-Drevision=)\S+' .mvn/maven.config 2>/dev/null || true) + changelist=$(grep -oP '(?<=-Dchangelist=)\S+' .mvn/maven.config 2>/dev/null || true) + echo "version=${revision}${changelist}" >> "$GITHUB_OUTPUT" + fi + # Build job - only runs if no artifacts were found during initialization build: name: PR Build - needs: [ initialize ] + needs: [ initialize, setup ] if: fromJSON(needs.initialize.outputs.filters).build == 'true' && needs.initialize.outputs.found_artifacts == 'false' uses: ./.github/workflows/cicd_comp_build-phase.yml with: core-build: true run-pr-checks: false - version: '24.12.27' + version: ${{ needs.setup.outputs.version }} permissions: contents: read packages: write