Skip to content

Commit

Permalink
fix: Grant temp access only if user in search scope [DHIS2-18784] [2.…
Browse files Browse the repository at this point in the history
…40] (#19711)
  • Loading branch information
muilpp authored Jan 20, 2025
1 parent e12fcc6 commit 2944fb7
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ boolean hasAccessUsingContext(
* @param reason The reason for requesting temporary ownership
*/
void grantTemporaryOwnership(
TrackedEntityInstance entityInstance, Program program, User user, String reason);
TrackedEntityInstance entityInstance, Program program, User user, String reason)
throws ForbiddenException;

/**
* Ownership check can be skipped if the user is super user or if the program type is without
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,23 +233,57 @@ public void assignOwnership(
@Override
@Transactional
public void grantTemporaryOwnership(
TrackedEntityInstance entityInstance, Program program, User user, String reason) {
if (canSkipOwnershipCheck(user, program) || entityInstance == null) {
return;
TrackedEntityInstance entityInstance, Program program, User user, String reason)
throws ForbiddenException {

validateGrantTemporaryOwnershipInputs(entityInstance, program, user);

if (config.isEnabled(CHANGELOG_TRACKER)) {
programTempOwnershipAuditService.addProgramTempOwnershipAudit(
new ProgramTempOwnershipAudit(program, entityInstance, reason, user.getUsername()));
}

if (program.isProtected()) {
if (config.isEnabled(CHANGELOG_TRACKER)) {
programTempOwnershipAuditService.addProgramTempOwnershipAudit(
new ProgramTempOwnershipAudit(program, entityInstance, reason, user.getUsername()));
}
ProgramTempOwner programTempOwner =
new ProgramTempOwner(
program, entityInstance, reason, user, TEMPORARY_OWNERSHIP_VALIDITY_IN_HOURS);
programTempOwnerService.addProgramTempOwner(programTempOwner);
tempOwnerCache.invalidate(
getTempOwnershipCacheKey(entityInstance.getUid(), program.getUid(), user.getUid()));
ProgramTempOwner programTempOwner =
new ProgramTempOwner(
program, entityInstance, reason, user, TEMPORARY_OWNERSHIP_VALIDITY_IN_HOURS);
programTempOwnerService.addProgramTempOwner(programTempOwner);
tempOwnerCache.invalidate(
getTempOwnershipCacheKey(entityInstance.getUid(), program.getUid(), user.getUid()));
}

private void validateGrantTemporaryOwnershipInputs(
TrackedEntityInstance entityInstance, Program program, User user) throws ForbiddenException {
if (program == null) {
throw new ForbiddenException(
"Temporary ownership not created. Program supplied does not exist.");
}

if (user.isSuper()) {
throw new ForbiddenException("Temporary ownership not created. Current user is a superuser.");
}

if (ProgramType.WITHOUT_REGISTRATION == program.getProgramType()) {
throw new ForbiddenException(
"Temporary ownership not created. Program supplied is not a tracker program.");
}

if (!program.isProtected()) {
throw new ForbiddenException(
String.format(
"Temporary ownership can only be granted to protected programs. %s access level is %s.",
program.getUid(), program.getAccessLevel().name()));
}

if (!isOwnerInUserSearchScope(user, entityInstance, program)) {
throw new ForbiddenException(
"The owner of the entity-program combination is not in the user's search scope.");
}
}

private boolean isOwnerInUserSearchScope(
User user, TrackedEntityInstance trackedEntity, Program program) {
return organisationUnitService.isInUserSearchHierarchyCached(
user, getOwner(trackedEntity.getId(), program, trackedEntity::getOrganisationUnit));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@
*/
package org.hisp.dhis.trackedentity;

import static org.hisp.dhis.common.AccessLevel.AUDITED;
import static org.hisp.dhis.common.AccessLevel.CLOSED;
import static org.hisp.dhis.common.AccessLevel.OPEN;
import static org.hisp.dhis.common.OrganisationUnitSelectionMode.ACCESSIBLE;
import static org.hisp.dhis.security.acl.AccessStringHelper.DEFAULT;
import static org.hisp.dhis.security.acl.AccessStringHelper.FULL;
import static org.hisp.dhis.user.UserRole.AUTHORITY_ALL;
import static org.hisp.dhis.utils.Assertions.assertContains;
import static org.hisp.dhis.utils.Assertions.assertContainsOnly;
import static org.hisp.dhis.utils.Assertions.assertIsEmpty;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -43,6 +47,7 @@
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.hisp.dhis.common.AccessLevel;
import org.hisp.dhis.dxf2.events.EnrollmentEventsParams;
import org.hisp.dhis.dxf2.events.EnrollmentParams;
Expand All @@ -56,12 +61,15 @@
import org.hisp.dhis.program.ProgramInstance;
import org.hisp.dhis.program.ProgramInstanceService;
import org.hisp.dhis.program.ProgramService;
import org.hisp.dhis.program.ProgramType;
import org.hisp.dhis.test.integration.IntegrationTestBase;
import org.hisp.dhis.user.User;
import org.hisp.dhis.user.UserService;
import org.hisp.dhis.user.sharing.Sharing;
import org.hisp.dhis.user.sharing.UserAccess;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;

/**
Expand Down Expand Up @@ -149,7 +157,7 @@ protected void setUpTest() throws Exception {
programA.setSharing(new Sharing(FULL, userAccess));
programService.updateProgram(programA);
programB = createProgram('B');
programB.setAccessLevel(AccessLevel.CLOSED);
programB.setAccessLevel(CLOSED);
programB.setTrackedEntityType(trackedEntityType);
programService.addProgram(programB);
programB.setSharing(new Sharing(DEFAULT, userAccess));
Expand All @@ -166,25 +174,20 @@ protected void setUpTest() throws Exception {
}

@Test
void testAssignOwnership() {
void shouldFailWhenGrantingTemporaryOwnershipAndUserNotInSearchScope() {
assertTrue(trackerOwnershipAccessManager.hasAccess(userA, entityInstanceA1, programA));
assertFalse(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceA1, programA));
assertTrue(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceB1, programA));
trackerOwnershipAccessManager.assignOwnership(
entityInstanceA1, programA, organisationUnitB, false, true);
assertFalse(trackerOwnershipAccessManager.hasAccess(userA, entityInstanceA1, programA));
assertTrue(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceA1, programA));
}

@Test
void testGrantTemporaryOwnershipWithAudit() {
assertTrue(trackerOwnershipAccessManager.hasAccess(userA, entityInstanceA1, programA));
assertFalse(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceA1, programA));
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, programA, userB, "testing reason");
assertTrue(trackerOwnershipAccessManager.hasAccess(userA, entityInstanceA1, programA));
assertTrue(trackerOwnershipAccessManager.hasAccess(userA, entityInstanceA1, programA));
assertTrue(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceA1, programA));
Exception exception =
assertThrows(
ForbiddenException.class,
() ->
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, programA, userB, "testing reason"));

assertEquals(
"The owner of the entity-program combination is not in the user's search scope.",
exception.getMessage());
}

@Test
Expand All @@ -208,9 +211,13 @@ void shouldHaveAccessWhenProgramProtectedAndUserInCaptureScope() {
}

@Test
void shouldHaveAccessWhenProgramProtectedAndHasTemporaryAccess() {
void shouldHaveAccessWhenProgramProtectedAndHasTemporaryAccess() throws ForbiddenException {
userB.setTeiSearchOrganisationUnits(Set.of(organisationUnitA));
userService.updateUser(userB);

trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, programA, userB, "test protected program");

assertTrue(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceA1, programA));
assertTrue(
trackerOwnershipAccessManager.hasAccess(
Expand All @@ -233,14 +240,67 @@ void shouldHaveAccessWhenProgramClosedAndUserInCaptureScope() {
userB, entityInstanceB1.getUid(), entityInstanceB1.getOrganisationUnit(), programB));
}

private static Stream<Program> providePrograms() {
return Stream.of(createProgram(OPEN), createProgram(AUDITED), createProgram(CLOSED));
}

@ParameterizedTest
@MethodSource("providePrograms")
void shouldFailWhenGrantingTemporaryOwnershipToProgramWithAccessLevelOtherThanProtected(
Program program) {
Exception exception =
assertThrows(
ForbiddenException.class,
() ->
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, program, userB, "test temporary ownership"));

assertContains(
"Temporary ownership can only be granted to protected programs.", exception.getMessage());
}

@Test
void shouldNotHaveAccessWhenProgramClosedAndUserHasTemporaryAccess() {
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, programB, userB, "test closed program");
assertFalse(trackerOwnershipAccessManager.hasAccess(userB, entityInstanceA1, programB));
assertFalse(
trackerOwnershipAccessManager.hasAccess(
userB, entityInstanceA1.getUid(), entityInstanceA1.getOrganisationUnit(), programB));
void shouldFailWhenGrantingTemporaryAccessIfUserIsSuperuser() {
Exception exception =
assertThrows(
ForbiddenException.class,
() ->
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, programA, superUser, "test temporary ownership"));

assertEquals(
"Temporary ownership not created. Current user is a superuser.", exception.getMessage());
}

@Test
void shouldFailWhenGrantingTemporaryAccessIfProgramIsNull() {
Exception exception =
assertThrows(
ForbiddenException.class,
() ->
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, null, userB, "test temporary ownership"));

assertEquals(
"Temporary ownership not created. Program supplied does not exist.",
exception.getMessage());
}

@Test
void shouldFailWhenGrantingTemporaryAccessIfProgramIsNotTrackerProgram() {
Program eventProgram = createProgram(AccessLevel.PROTECTED);
eventProgram.setProgramType(ProgramType.WITHOUT_REGISTRATION);

Exception exception =
assertThrows(
ForbiddenException.class,
() ->
trackerOwnershipAccessManager.grantTemporaryOwnership(
entityInstanceA1, eventProgram, userB, "test temporary ownership"));

assertEquals(
"Temporary ownership not created. Program supplied is not a tracker program.",
exception.getMessage());
}

@Test
Expand Down Expand Up @@ -427,4 +487,11 @@ private TrackedEntityInstanceParams createInstanceParams() {
false,
false);
}

private static Program createProgram(AccessLevel accessLevel) {
Program program = new Program();
program.setAccessLevel(accessLevel);

return program;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@

import static org.hisp.dhis.web.WebClientUtils.assertStatus;

import java.util.Set;
import org.hisp.dhis.organisationunit.OrganisationUnit;
import org.hisp.dhis.user.User;
import org.hisp.dhis.web.HttpStatus;
import org.hisp.dhis.webapi.DhisControllerConvenienceTest;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -42,36 +45,66 @@
*/
class TrackerOwnershipControllerTest extends DhisControllerConvenienceTest {

private String ouId;
private String orgUnitAUid;

private String pId;

private String teiId;

private String pId;
private User regularUser;

@BeforeEach
void setUp() {
ouId =
orgUnitAUid =
assertStatus(
HttpStatus.CREATED,
POST(
"/organisationUnits/",
"{'name':'My Unit', 'shortName':'OU1', 'openingDate': '2020-01-01'}"));
String orgUnitBUid =
assertStatus(
HttpStatus.CREATED,
POST(
"/organisationUnits/",
"{'name':'My Unit', 'shortName':'OU1', 'openingDate': '2020-01-01'}"));
String tetId = assertStatus(HttpStatus.CREATED, POST("/trackedEntityTypes/", "{'name': 'A'}"));

OrganisationUnit orgUnitA = manager.get(OrganisationUnit.class, orgUnitAUid);
OrganisationUnit orgUnitB = manager.get(OrganisationUnit.class, orgUnitBUid);

regularUser =
createAndAddUser(
false, "regular-user", Set.of(orgUnitA, orgUnitB), Set.of(orgUnitA, orgUnitB));
User superuser =
createAndAddUser(true, "superuser", Set.of(orgUnitA, orgUnitB), Set.of(orgUnitA, orgUnitB));
injectSecurityContext(superuser);

String tetId =
assertStatus(
HttpStatus.CREATED,
POST(
"/trackedEntityTypes/",
"{'name': 'A', 'sharing':{'external':false,'public':'rwrw----'}}"));

teiId =
assertStatus(
HttpStatus.OK,
POST(
"/trackedEntityInstances",
"{'name':'A', 'trackedEntityType':'" + tetId + "', 'orgUnit':'" + ouId + "'}"));
"{'name':'A', 'trackedEntityType':'"
+ tetId
+ "', 'orgUnit':'"
+ orgUnitAUid
+ "'}"));
pId =
assertStatus(
HttpStatus.CREATED,
POST(
"/programs/",
"{'name':'P1', 'shortName':'P1', 'programType':'WITHOUT_REGISTRATION', 'organisationUnits': [{'id':'"
+ ouId
+ "'}]}"));
"{'name':'P1', 'shortName':'P1', 'programType':'WITH_REGISTRATION', 'accessLevel':'PROTECTED', 'trackedEntityType': {'id': '"
+ tetId
+ "'}, 'organisationUnits': [{'id':'"
+ orgUnitAUid
+ "'}], 'sharing':{'external':false,'public':'rwrw----'}}"));
}

@Test
Expand All @@ -85,12 +118,13 @@ void testUpdateTrackerProgramOwner() {
"/tracker/ownership/transfer?trackedEntityInstance={tei}&program={prog}&ou={ou}",
teiId,
pId,
ouId)
orgUnitAUid)
.content(HttpStatus.OK));
}

@Test
void testOverrideOwnershipAccess() {
injectSecurityContext(regularUser);
assertWebMessage(
"OK",
200,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ public WebMessage updateTrackerProgramOwner(
public WebMessage overrideOwnershipAccess(
@RequestParam String trackedEntityInstance,
@RequestParam String reason,
@RequestParam String program) {
@RequestParam String program)
throws ForbiddenException {
trackerOwnershipAccessManager.grantTemporaryOwnership(
trackedEntityInstanceService.getTrackedEntityInstance(trackedEntityInstance),
programService.getProgram(program),
Expand Down

0 comments on commit 2944fb7

Please sign in to comment.