Skip to content

Commit

Permalink
feat: validate access to ordering tea and de
Browse files Browse the repository at this point in the history
  • Loading branch information
lucaCambi77 committed Nov 5, 2023
1 parent 2e9f30b commit 234152f
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,26 @@
import static org.hisp.dhis.common.OrganisationUnitSelectionMode.CAPTURE;
import static org.hisp.dhis.security.Authorities.F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.hisp.dhis.common.OrganisationUnitSelectionMode;
import org.hisp.dhis.dataelement.DataElement;
import org.hisp.dhis.feedback.BadRequestException;
import org.hisp.dhis.feedback.ForbiddenException;
import org.hisp.dhis.program.Program;
import org.hisp.dhis.security.acl.AclService;
import org.hisp.dhis.trackedentity.TrackedEntityAttribute;
import org.hisp.dhis.user.User;
import org.springframework.stereotype.Component;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Component
@RequiredArgsConstructor
public class OperationsParamsValidator {

private final AclService aclService;

/**
* Validates the user is authorized and/or has the necessary configuration set up in case the org
* unit mode is ALL, ACCESSIBLE or CAPTURE. If the mode used is none of these three, no validation
Expand All @@ -50,7 +60,7 @@ public class OperationsParamsValidator {
* @throws BadRequestException if a validation error occurs for any of the three aforementioned
* modes
*/
public static void validateOrgUnitMode(
public void validateOrgUnitMode(
OrganisationUnitSelectionMode orgUnitMode, User user, Program program)
throws BadRequestException {
switch (orgUnitMode) {
Expand All @@ -61,7 +71,7 @@ public static void validateOrgUnitMode(
}
}

private static void validateUserCanSearchOrgUnitModeALL(User user) throws BadRequestException {
private void validateUserCanSearchOrgUnitModeALL(User user) throws BadRequestException {
if (user != null
&& !(user.isSuper()
|| user.isAuthorized(F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS.name()))) {
Expand All @@ -70,7 +80,7 @@ private static void validateUserCanSearchOrgUnitModeALL(User user) throws BadReq
}
}

private static void validateUserScope(
private void validateUserScope(
User user, Program program, OrganisationUnitSelectionMode orgUnitMode)
throws BadRequestException {

Expand All @@ -89,11 +99,41 @@ private static void validateUserScope(
}
}

private static void validateCaptureScope(User user) throws BadRequestException {
private void validateCaptureScope(User user) throws BadRequestException {
if (user == null) {
throw new BadRequestException("User is required for orgUnitMode: " + CAPTURE);
} else if (user.getOrganisationUnits().isEmpty()) {
throw new BadRequestException("User needs to be assigned data capture orgunits");
}
}

public void validateOrderableAttributes(List<Order> order, User user) throws ForbiddenException {
Set<TrackedEntityAttribute> orderableTeas =
order.stream()
.filter(o -> o.getField() instanceof TrackedEntityAttribute)
.map(o -> (TrackedEntityAttribute) o.getField())
.collect(Collectors.toSet());

for (TrackedEntityAttribute tea : orderableTeas) {
if (!aclService.canDataRead(user, tea)) {
throw new ForbiddenException(
"User has no access to tracked entity attribute: " + tea.getUid());
}
}
}

public void validateOrderableDataElements(List<Order> order, User user)
throws ForbiddenException {
Set<DataElement> orderableDes =
order.stream()
.filter(o -> o.getField() instanceof DataElement)
.map(o -> (DataElement) o.getField())
.collect(Collectors.toSet());

for (DataElement de : orderableDes) {
if (!aclService.canDataRead(user, de)) {
throw new ForbiddenException("User has no access to data element: " + de.getUid());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
package org.hisp.dhis.tracker.export.event;

import static org.hisp.dhis.security.Authorities.F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS;
import static org.hisp.dhis.tracker.export.OperationsParamsValidator.validateOrgUnitMode;

import java.util.List;
import java.util.Map;
Expand All @@ -53,6 +52,7 @@
import org.hisp.dhis.trackedentity.TrackedEntityAttribute;
import org.hisp.dhis.trackedentity.TrackedEntityAttributeService;
import org.hisp.dhis.trackedentity.TrackedEntityService;
import org.hisp.dhis.tracker.export.OperationsParamsValidator;
import org.hisp.dhis.tracker.export.Order;
import org.hisp.dhis.user.CurrentUserService;
import org.hisp.dhis.user.User;
Expand Down Expand Up @@ -85,6 +85,8 @@ class EventOperationParamsMapper {

private final DataElementService dataElementService;

private final OperationsParamsValidator operationsParamsValidator;

@Transactional(readOnly = true)
public EventQueryParams map(EventOperationParams operationParams)
throws BadRequestException, ForbiddenException {
Expand All @@ -96,7 +98,7 @@ public EventQueryParams map(EventOperationParams operationParams)
OrganisationUnit orgUnit = validateRequestedOrgUnit(operationParams.getOrgUnitUid());
validateUser(user, program, programStage, orgUnit);

validateOrgUnitMode(operationParams.getOrgUnitMode(), user, program);
operationsParamsValidator.validateOrgUnitMode(operationParams.getOrgUnitMode(), user, program);

TrackedEntity trackedEntity = validateTrackedEntity(operationParams.getTrackedEntityUid());

Expand All @@ -115,6 +117,8 @@ public EventQueryParams map(EventOperationParams operationParams)
mapDataElementFilters(queryParams, operationParams.getDataElementFilters());
mapAttributeFilters(queryParams, operationParams.getAttributeFilters());
mapOrderParam(queryParams, operationParams.getOrder());
operationsParamsValidator.validateOrderableAttributes(queryParams.getOrder(), user);
operationsParamsValidator.validateOrderableDataElements(queryParams.getOrder(), user);

return queryParams
.setProgram(program)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.hisp.dhis.trackedentity.TrackedEntityAttributeService;
import org.hisp.dhis.trackedentity.TrackedEntityType;
import org.hisp.dhis.trackedentity.TrackedEntityTypeService;
import org.hisp.dhis.tracker.export.OperationsParamsValidator;
import org.hisp.dhis.tracker.export.Order;
import org.hisp.dhis.user.User;
import org.springframework.stereotype.Component;
Expand All @@ -67,6 +68,8 @@ class TrackedEntityOperationParamsMapper {

@Nonnull private final TrackedEntityAttributeService attributeService;

@Nonnull private final OperationsParamsValidator operationsParamsValidator;

@Transactional(readOnly = true)
public TrackedEntityQueryParams map(TrackedEntityOperationParams operationParams)
throws BadRequestException, ForbiddenException {
Expand All @@ -84,6 +87,7 @@ public TrackedEntityQueryParams map(TrackedEntityOperationParams operationParams
mapAttributeFilters(params, operationParams.getFilters());

mapOrderParam(params, operationParams.getOrder());
operationsParamsValidator.validateOrderableAttributes(params.getOrder(), user);

params
.setProgram(program)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

import static org.hisp.dhis.common.OrganisationUnitSelectionMode.ALL;
import static org.hisp.dhis.common.OrganisationUnitSelectionMode.CAPTURE;
import static org.hisp.dhis.tracker.export.OperationsParamsValidator.validateOrgUnitMode;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Set;
Expand All @@ -44,6 +43,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
Expand All @@ -57,6 +57,8 @@ class OperationsParamsValidatorTest {

private final Program program = new Program("program");

@InjectMocks private OperationsParamsValidator operationsParamsValidator;

@BeforeEach
public void setUp() {
OrganisationUnit organisationUnit = createOrgUnit("orgUnit", PARENT_ORG_UNIT_UID);
Expand All @@ -67,7 +69,8 @@ public void setUp() {
void shouldFailWhenOuModeCaptureAndUserHasNoOrgUnitsAssigned() {
Exception exception =
Assertions.assertThrows(
BadRequestException.class, () -> validateOrgUnitMode(CAPTURE, new User(), program));
BadRequestException.class,
() -> operationsParamsValidator.validateOrgUnitMode(CAPTURE, new User(), program));

assertEquals("User needs to be assigned data capture orgunits", exception.getMessage());
}
Expand All @@ -80,7 +83,8 @@ void shouldFailWhenOuModeRequiresUserScopeOrgUnitAndUserHasNoOrgUnitsAssigned(
OrganisationUnitSelectionMode orgUnitMode) {
Exception exception =
Assertions.assertThrows(
BadRequestException.class, () -> validateOrgUnitMode(orgUnitMode, new User(), program));
BadRequestException.class,
() -> operationsParamsValidator.validateOrgUnitMode(orgUnitMode, new User(), program));

assertEquals(
"User needs to be assigned either search or data capture org units",
Expand All @@ -91,7 +95,8 @@ void shouldFailWhenOuModeRequiresUserScopeOrgUnitAndUserHasNoOrgUnitsAssigned(
void shouldFailWhenOuModeAllAndNotSuperuser() {
Exception exception =
Assertions.assertThrows(
BadRequestException.class, () -> validateOrgUnitMode(ALL, new User(), program));
BadRequestException.class,
() -> operationsParamsValidator.validateOrgUnitMode(ALL, new User(), program));

assertEquals(
"Current user is not authorized to query across all organisation units",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ void shouldMapOrderInGivenOrder() throws BadRequestException, ForbiddenException
de1.setUid(DE_1_UID);
when(dataElementService.getDataElement(DE_1_UID)).thenReturn(de1);
when(dataElementService.getDataElement(TEA_1_UID)).thenReturn(null);
when(aclService.canDataRead(user, de1)).thenReturn(true);
when(aclService.canDataRead(user, tea1)).thenReturn(true);

EventOperationParams operationParams =
eventBuilder
Expand Down Expand Up @@ -574,6 +576,65 @@ void shouldMapOrgUnitAndModeWhenModeAllAndUserIsAuthorized(String userName)
assertEquals(ALL, params.getOrgUnitMode());
}

@Test
void shouldThrowWhenUserHasNoAccessToDataElementOrder() {
User user = new User();
UserRole userRole = new UserRole();
userRole.setAuthorities(Set.of(F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS.name()));
user.setUserRoles(Set.of(userRole));

when(currentUserService.getCurrentUser()).thenReturn(user);
when(organisationUnitService.getOrganisationUnit(orgUnitId)).thenReturn(orgUnit);

String deUid = CodeGenerator.generateUid();
DataElement de = new DataElement();
de.setUid(deUid);
when(dataElementService.getDataElement(deUid)).thenReturn(de);
when(aclService.canDataRead(user, de)).thenReturn(false);

EventOperationParams operationParams =
eventBuilder
.orgUnitMode(ALL)
.orgUnitUid(orgUnit.getUid())
.orderBy(UID.of(deUid), SortDirection.DESC)
.build();

ForbiddenException exception =
assertThrows(ForbiddenException.class, () -> mapper.map(operationParams));

assertEquals("User has no access to data element: " + deUid, exception.getMessage());
}

@Test
void shouldThrowWhenUserHasNoAccessToTrackedEntityAttributeOrder() {
User user = new User();
UserRole userRole = new UserRole();
userRole.setAuthorities(Set.of(F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS.name()));
user.setUserRoles(Set.of(userRole));

when(currentUserService.getCurrentUser()).thenReturn(user);
when(organisationUnitService.getOrganisationUnit(orgUnitId)).thenReturn(orgUnit);

String teaUid = CodeGenerator.generateUid();
TrackedEntityAttribute tea = new TrackedEntityAttribute();
tea.setUid(teaUid);
when(trackedEntityAttributeService.getTrackedEntityAttribute(teaUid)).thenReturn(tea);
when(aclService.canDataRead(user, tea)).thenReturn(false);

EventOperationParams operationParams =
eventBuilder
.orgUnitMode(ALL)
.orgUnitUid(orgUnit.getUid())
.orderBy(UID.of(teaUid), SortDirection.DESC)
.build();

ForbiddenException exception =
assertThrows(ForbiddenException.class, () -> mapper.map(operationParams));

assertEquals(
"User has no access to tracked entity attribute: " + teaUid, exception.getMessage());
}

private User createUserWithAuthority(Authorities authority) {
User user = new User();
UserRole userRole = new UserRole();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import org.hisp.dhis.program.ProgramService;
import org.hisp.dhis.program.ProgramStage;
import org.hisp.dhis.program.ProgramStatus;
import org.hisp.dhis.security.acl.AclService;
import org.hisp.dhis.trackedentity.TrackedEntityAttribute;
import org.hisp.dhis.trackedentity.TrackedEntityAttributeService;
import org.hisp.dhis.trackedentity.TrackedEntityType;
Expand Down Expand Up @@ -110,6 +111,8 @@ class TrackedEntityOperationParamsMapperTest {

@Mock private TrackedEntityTypeService trackedEntityTypeService;

@Mock private AclService aclService;

@InjectMocks private TrackedEntityOperationParamsMapper mapper;

private User user;
Expand Down Expand Up @@ -553,4 +556,26 @@ void shouldFailToMapGivenInvalidOrderNameWhichIsAValidUID() {
assertThrows(BadRequestException.class, () -> mapper.map(operationParams));
assertStartsWith("Cannot order by 'lastUpdated'", exception.getMessage());
}

@Test
void shouldThrowWhenUserHasNoAccessToTrackedEntityAttributeOrder() {
TrackedEntityAttribute tea = new TrackedEntityAttribute();
tea.setUid(TEA_1_UID);
when(attributeService.getTrackedEntityAttribute(TEA_1_UID)).thenReturn(tea);

when(aclService.canDataRead(user, tea)).thenReturn(false);

TrackedEntityOperationParams operationParams =
TrackedEntityOperationParams.builder()
.orgUnitMode(ACCESSIBLE)
.user(user)
.orderBy(UID.of(TEA_1_UID), SortDirection.ASC)
.build();

ForbiddenException exception =
assertThrows(ForbiddenException.class, () -> mapper.map(operationParams));

assertEquals(
"User has no access to tracked entity attribute: " + TEA_1_UID, exception.getMessage());
}
}
Loading

0 comments on commit 234152f

Please sign in to comment.