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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions .github/workflows/cicd_5-lts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -224,23 +225,29 @@ public PublishAuditStatus getPublishAuditStatus(String bundleId)
@CloseDBIfOpened
public List<PublishAuditStatus> getPublishAuditStatuses(List<String> bundleIds)
throws DotPublisherException {
if (bundleIds == null || bundleIds.isEmpty()) {
return Collections.emptyList();
}
try {
final List<PublishAuditStatus> result = new ArrayList<>();

DotConnect dc = new DotConnect();
final List<String> 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<Map<String, Object>> items = dc.loadObjectResults();
dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders));
Comment thread
sfreudenthaler marked this conversation as resolved.
bundleIds.forEach(dc::addParam);
final List<Map<String, Object>> items = dc.loadObjectResults();

for(Map<String, Object> item: items) {
result.add(turnIntoPublishAuditStatus(NO_LIMIT_ASSETS, item));
for (final Map<String, Object> 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);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -653,6 +654,7 @@ private List<PublishAuditHistory> getRemoteHistoryFromEndpoint(final List<Strin

final String responseBody = webTarget
.request(MediaType.APPLICATION_JSON)
.header("Authorization", AuthCredentialPushPublishUtil.INSTANCE.getRequestToken(targetEndpoint).orElse(""))
.post(Entity.entity(bundleIds, MediaType.APPLICATION_JSON))
.readEntity(String.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ private String getTokenFromRequest(final HttpServletRequest request) {
.startsWith(BEARER)) {

return authorizationHeader.substring(BEARER.length());
} else {
throw new IllegalArgumentException("Bearer Authorization header expected");
}

return StringUtils.EMPTY;
}

public static class PushPublishAuthenticationToken {
Expand Down
33 changes: 28 additions & 5 deletions dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +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.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")
Expand All @@ -24,7 +25,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) {

final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken =
AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request);

final Optional<Response> failResponse = PushPublishResourceUtil.getFailResponse(request, ppAuthToken);

if (failResponse.isPresent()) {
return failResponse.get();
}

PublishAuditStatus status = null;

try {
Expand All @@ -42,7 +54,18 @@ public Response get(@PathParam("bundleId") String bundleId) {
@POST
@Path("/getAll")
@Produces(MediaType.APPLICATION_JSON)
public Response getAll( List<String> bundleIds) {
public Response getAll(final List<String> bundleIds,
@Context final HttpServletRequest request) {

final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken =
AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request);

final Optional<Response> failResponse = PushPublishResourceUtil.getFailResponse(request, ppAuthToken);

if (failResponse.isPresent()) {
return failResponse.get();
}

try {
final List<PublishAuditStatus> statuses = auditAPI.getPublishAuditStatuses(bundleIds);

Expand Down
3 changes: 2 additions & 1 deletion dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PublishAuditStatus> result = publishAuditAPI.getPublishAuditStatuses(
Arrays.asList(bundleId1, bundleId2));

assertNotNull(result);
assertEquals(2, result.size());
final Set<String> 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<PublishAuditStatus> 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<PublishAuditStatus> 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<String> maliciousIds = Arrays.asList(
"x' OR '1'='1",
"x'; DROP TABLE publishing_queue_audit; --",
"x' UNION SELECT '" + realBundleId + "' --",
"x'/*",
"x\\' OR \\'1\\'=\\'1"
);

final List<PublishAuditStatus> 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.dotcms.rest;

import com.dotcms.IntegrationTestBase;
import com.dotcms.util.IntegrationTestInitService;
import org.junit.BeforeClass;
import org.junit.Test;

import javax.servlet.http.HttpServletRequest;
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 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 {

@BeforeClass
public static void prepare() throws Exception {
IntegrationTestInitService.getInstance().init();
}

/**
* 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
public void test_get_rejectsAnonymousRequest() {
final HttpServletRequest request = mock(HttpServletRequest.class);
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)}
* When: called with a request that carries no push-publish auth token
* Should: return 401 Unauthorized.
*/
@Test
public void test_getAll_rejectsAnonymousRequest() {
final HttpServletRequest request = mock(HttpServletRequest.class);
final Response response = new AuditPublishingResource()
.getAll(Collections.singletonList("any-bundle-id"), request);
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,16 @@
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
Expand Down
Loading