Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.util.EnumSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.hisp.dhis.common.BaseIdentifiableObject;
import org.hisp.dhis.common.DimensionItemType;
import org.hisp.dhis.common.DxfNamespaces;
import org.hisp.dhis.common.MetadataObject;

Expand All @@ -50,6 +53,15 @@
@Accessors(chain = true)
@JacksonXmlRootElement(localName = "aggregateDataExchange", namespace = DxfNamespaces.DXF_2_0)
public class AggregateDataExchange extends BaseIdentifiableObject implements MetadataObject {

/** Allowed dimension item types for source request data items. */
public static final Set<DimensionItemType> ALLOWED_DX_ITEM_TYPES =
EnumSet.of(
DimensionItemType.INDICATOR,
DimensionItemType.DATA_ELEMENT,
DimensionItemType.DATA_ELEMENT_OPERAND,
DimensionItemType.PROGRAM_INDICATOR);

/** Data exchange source. */
@JsonProperty
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ public enum ErrorCode {
E6304("Aggregate data exchange target API must be specified when target type is EXTERNAL"),
E6305(
"Aggregate data exchange target API must specify either access token or username and password"),
E6306(
"Aggregate data exchange source request contains data item with unsupported type: `{0}`, allowed types are: {1}"),

/*Analytics table hook*/
E6400("Analytics table hook `{0}` is a duplicate of `{1}`"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.hisp.dhis.analytics.AnalyticsService;
import org.hisp.dhis.analytics.DataQueryParams;
import org.hisp.dhis.analytics.DataQueryService;
import org.hisp.dhis.common.DimensionalItemObject;
import org.hisp.dhis.common.DimensionalObject;
import org.hisp.dhis.common.Grid;
import org.hisp.dhis.common.IdScheme;
Expand Down Expand Up @@ -150,6 +151,17 @@ public ImportSummaries exchangeData(
return summaries;
}

if (!hasAllowedDxItemTypes(exchange)) {
summaries.addImportSummary(
new ImportSummary(
ImportStatus.ERROR,
String.format(
"Aggregate data exchange '%s' contains source request data items with unsupported types, "
+ "allowed types are: %s",
exchange.getDisplayName(), AggregateDataExchange.ALLOWED_DX_ITEM_TYPES)));
return summaries;
}

progress.startingStage(toStageDescription(exchange), FailurePolicy.SKIP_ITEM);
progress.runStage(
exchange.getSource().getRequests().stream(),
Expand Down Expand Up @@ -538,4 +550,24 @@ private static String toStageSummary(int success, int failed, AggregateDataExcha
boolean isPersisted(AggregateDataExchange exchange) {
return exchange != null && exchange.getId() > 0;
}

/**
* Returns true if all data items across all source requests of the given exchange are of allowed
* types.
*
* @param exchange the {@link AggregateDataExchange}.
* @return true if all data item types are allowed.
*/
private boolean hasAllowedDxItemTypes(AggregateDataExchange exchange) {
return exchange.getSource().getRequests().stream()
.flatMap(
request -> {
IdScheme inputIdScheme = toIdSchemeOrDefault(request.getInputIdScheme());
DimensionalObject dxDimension =
toDimensionalObject(DATA_X_DIM_ID, request.getDx(), inputIdScheme);
return dxDimension.getItems().stream();
})
.map(DimensionalItemObject::getDimensionItemType)
.allMatch(AggregateDataExchange.ALLOWED_DX_ITEM_TYPES::contains);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import java.util.Date;
Expand All @@ -51,16 +52,21 @@
import org.hisp.dhis.analytics.AnalyticsService;
import org.hisp.dhis.analytics.DataQueryParams;
import org.hisp.dhis.analytics.DataQueryService;
import org.hisp.dhis.common.BaseDimensionalItemObject;
import org.hisp.dhis.common.BaseDimensionalObject;
import org.hisp.dhis.common.DimensionItemType;
import org.hisp.dhis.common.DimensionType;
import org.hisp.dhis.common.DisplayProperty;
import org.hisp.dhis.common.IdScheme;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.dataexchange.client.Dhis2Client;
import org.hisp.dhis.datavalue.DataExportService;
import org.hisp.dhis.dxf2.common.ImportOptions;
import org.hisp.dhis.dxf2.importsummary.ImportStatus;
import org.hisp.dhis.dxf2.importsummary.ImportSummaries;
import org.hisp.dhis.feedback.ForbiddenException;
import org.hisp.dhis.importexport.ImportStrategy;
import org.hisp.dhis.scheduling.JobProgress;
import org.hisp.dhis.security.acl.AclService;
import org.hisp.dhis.user.User;
import org.hisp.dhis.user.UserDetails;
Expand Down Expand Up @@ -315,4 +321,48 @@ void testGetSourceDataValueSetsWithoutAccess() {
service.getSourceDataValueSets(
UserDetails.fromUser(new User()), "uid", new SourceDataQueryParams()));
}

@Test
@SuppressWarnings("unchecked")
void testExchangeDataWithForbiddenDxItemType() {
when(aclService.canDataWrite(any(UserDetails.class), any(IdentifiableObject.class)))
.thenReturn(true);

BaseDimensionalItemObject forbiddenItem = new BaseDimensionalItemObject();
forbiddenItem.setDimensionItemType(DimensionItemType.REPORTING_RATE);

when(dataQueryService.getDimension(
eq(DATA_X_DIM_ID),
any(),
any(Date.class),
nullable(List.class),
anyBoolean(),
nullable(DisplayProperty.class),
nullable(IdScheme.class)))
.thenReturn(
new BaseDimensionalObject(DATA_X_DIM_ID, DimensionType.DATA_X, List.of(forbiddenItem)));

SourceRequest sourceRequest =
new SourceRequest()
.setName("SourceRequestA")
.setDx(List.of("reportingRate123"))
.setPe(List.of("202101"))
.setOu(List.of("orgUnitA"));

Source source = new Source().setRequests(List.of(sourceRequest));
TargetRequest targetRequest = new TargetRequest();
Target target =
new Target().setType(TargetType.INTERNAL).setApi(new Api()).setRequest(targetRequest);
AggregateDataExchange exchange =
new AggregateDataExchange().setSource(source).setTarget(target);

ImportSummaries summaries =
service.exchangeData(UserDetails.fromUser(new User()), exchange, JobProgress.noop());

assertEquals(1, summaries.getImportSummaries().size());
assertEquals(ImportStatus.ERROR, summaries.getImportSummaries().get(0).getStatus());
assertTrue(
summaries.getImportSummaries().get(0).getDescription().contains("unsupported types"));
verifyNoInteractions(analyticsService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.hisp.dhis.common.CodeGenerator;
import org.hisp.dhis.common.DimensionService;
import org.hisp.dhis.common.DimensionalItemObject;
import org.hisp.dhis.common.IdScheme;
import org.hisp.dhis.dataexchange.aggregate.AggregateDataExchange;
import org.hisp.dhis.dataexchange.aggregate.Api;
import org.hisp.dhis.dataexchange.aggregate.SourceRequest;
Expand All @@ -60,6 +63,8 @@ public class AggregateDataExchangeObjectBundleHook
@Qualifier(AES_128_STRING_ENCRYPTOR)
private final PooledPBEStringEncryptor encryptor;

private final DimensionService dimensionService;

@Override
public void validate(
AggregateDataExchange exchange, ObjectBundle bundle, Consumer<ErrorReport> addReports) {
Expand Down Expand Up @@ -122,6 +127,37 @@ private void validateSource(AggregateDataExchange exchange, Consumer<ErrorReport
if (isEmpty(request.getDx()) || isEmpty(request.getPe()) || isEmpty(request.getOu())) {
addReports.accept(new ErrorReport(AggregateDataExchange.class, ErrorCode.E6303));
}

validateSourceDxItemTypes(request, addReports);
}
}

/**
* Validates that source request data items are of allowed types.
*
* @param request the {@link SourceRequest}.
* @param addReports the list of {@link ErrorReport}.
*/
private void validateSourceDxItemTypes(SourceRequest request, Consumer<ErrorReport> addReports) {
IdScheme idScheme =
request.getInputIdScheme() != null
? IdScheme.from(request.getInputIdScheme())
: IdScheme.UID;

for (String item : request.getDx()) {
DimensionalItemObject dxObject =
dimensionService.getDataDimensionalItemObject(idScheme, item);

if (dxObject != null
&& !AggregateDataExchange.ALLOWED_DX_ITEM_TYPES.contains(
dxObject.getDimensionItemType())) {
addReports.accept(
new ErrorReport(
AggregateDataExchange.class,
ErrorCode.E6306,
dxObject.getDimensionItemType(),
AggregateDataExchange.ALLOWED_DX_ITEM_TYPES));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@
import static org.hisp.dhis.test.TestBase.getAggregateDataExchange;
import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;
import org.hisp.dhis.common.CodeGenerator;
import org.hisp.dhis.common.DimensionItemType;
import org.hisp.dhis.common.DimensionService;
import org.hisp.dhis.common.DimensionalItemObject;
import org.hisp.dhis.dataexchange.aggregate.AggregateDataExchange;
import org.hisp.dhis.dataexchange.aggregate.Api;
import org.hisp.dhis.dataexchange.aggregate.Source;
Expand All @@ -57,6 +64,8 @@
class AggregateDataExchangeObjectBundleHookTest {
@Mock private ObjectBundle objectBundle;

@Mock private DimensionService dimensionService;

@InjectMocks private AggregateDataExchangeObjectBundleHook objectBundleHook;

@Test
Expand Down Expand Up @@ -150,6 +159,48 @@ void testMissingTargetApiUrl() {
assertErrorCode(ErrorCode.E4000, objectBundleHook.validate(exchange, objectBundle));
}

@Test
void testSourceDxItemDisallowedReportingRate() {
AggregateDataExchange exchange = getAggregateDataExchange('A');

DimensionalItemObject reportingRate = mock(DimensionalItemObject.class);
when(reportingRate.getDimensionItemType()).thenReturn(DimensionItemType.REPORTING_RATE);
when(dimensionService.getDataDimensionalItemObject(any(), eq("LrDpG50RAU9")))
.thenReturn(reportingRate);

assertErrorCode(ErrorCode.E6306, objectBundleHook.validate(exchange, objectBundle));
}

@Test
void testSourceDxItemDisallowedProgramDataElement() {
AggregateDataExchange exchange = getAggregateDataExchange('A');

DimensionalItemObject programDataElement = mock(DimensionalItemObject.class);
when(programDataElement.getDimensionItemType())
.thenReturn(DimensionItemType.PROGRAM_DATA_ELEMENT);
when(dimensionService.getDataDimensionalItemObject(any(), eq("LrDpG50RAU9")))
.thenReturn(programDataElement);

assertErrorCode(ErrorCode.E6306, objectBundleHook.validate(exchange, objectBundle));
}

@Test
void testSourceDxItemAllowedTypes() {
AggregateDataExchange exchange = getAggregateDataExchange('A');

DimensionalItemObject dataElement = mock(DimensionalItemObject.class);
when(dataElement.getDimensionItemType()).thenReturn(DimensionItemType.DATA_ELEMENT);
when(dimensionService.getDataDimensionalItemObject(any(), eq("LrDpG50RAU9")))
.thenReturn(dataElement);

DimensionalItemObject indicator = mock(DimensionalItemObject.class);
when(indicator.getDimensionItemType()).thenReturn(DimensionItemType.INDICATOR);
when(dimensionService.getDataDimensionalItemObject(any(), eq("uR5HCiJhQ1w")))
.thenReturn(indicator);

assertIsEmpty(objectBundleHook.validate(exchange, objectBundle));
}

@Test
void testMissingTargetApiAuthentication() {
Api api = new Api();
Expand Down