Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate access to ordering tea and de #15601

Closed
wants to merge 1 commit into from
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
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
Loading