Merge branch 'RED-7176' into 'master'

RED-7176: Soft-/Hard-/Restore Dossier without Dossier Owner not possible

Closes RED-7176

See merge request redactmanager/persistence-service!323
This commit is contained in:
Maverick Studer 2024-01-25 15:50:48 +01:00
commit 4cfe7d9c95
4 changed files with 69 additions and 27 deletions

View File

@ -20,13 +20,13 @@ import java.util.stream.Collectors;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.DossierCreatorService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
@ -42,6 +42,7 @@ import com.iqser.red.service.persistence.management.v1.processor.roles.Applicati
import com.iqser.red.service.persistence.management.v1.processor.service.AccessControlService;
import com.iqser.red.service.persistence.management.v1.processor.service.DossierManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.FilterByPermissionsService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.NotificationPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.users.UserService;
@ -346,12 +347,14 @@ public class DossierController implements DossierResource {
}
@PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "') && hasPermission(#dossierId, 'Dossier', 'ACCESS_OBJECT')")
@PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
public void deleteDossier(@PathVariable(DOSSIER_ID_PARAM) String dossierId) {
var dossierToBeDeleted = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierId, true, false));
Dossier dossier = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierId, true, false));
if (dossier.getOwnerId() != null && !dossier.getOwnerId().equals(KeycloakSecurity.getUserId())) {
throw new AccessDeniedException("Can not delete dossier that is owned by a different user");
}
dossierManagementService.delete(dossierId);
@ -362,14 +365,14 @@ public class DossierController implements DossierResource {
.message("Dossier moved to trash.")
.build());
dossierToBeDeleted.getMemberIds()
dossier.getMemberIds()
.stream()
.filter(m -> !KeycloakSecurity.getUserId().equals(m))
.forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
.userId(member)
.issuerId(KeycloakSecurity.getUserId())
.notificationType(NotificationType.DOSSIER_DELETED.name())
.target(Map.of("dossierId", dossierId, "dossierName", dossierToBeDeleted.getDossierName()))
.target(Map.of("dossierId", dossierId, "dossierName", dossier.getDossierName()))
.build()));
}
@ -477,15 +480,13 @@ public class DossierController implements DossierResource {
@PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
@PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
public void hardDeleteDossiers(@RequestParam(DOSSIER_ID_PARAM) Set<String> dossierIds) {
for (String dossierId : dossierIds) {
accessControlService.verifyUserIsDossierOwner(dossierId);
}
dossierManagementService.hardDeleteDossiers(dossierIds);
var filteredDossierIds = filterDossierIdsByOwnedKeepUnowned(dossierIds);
for (String dossierId : dossierIds) {
dossierManagementService.hardDeleteDossiers(filteredDossierIds);
for (String dossierId : filteredDossierIds) {
auditPersistenceService.audit(AuditRequest.builder()
.userId(KeycloakSecurity.getUserId())
@ -499,12 +500,13 @@ public class DossierController implements DossierResource {
@PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
@PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
public void undeleteDossiers(@RequestBody Set<String> dossierIds) {
dossierManagementService.undeleteDossiers(dossierIds);
for (String dossierId : dossierIds) {
var filteredDossierIds = filterDossierIdsByOwnedKeepUnowned(dossierIds);
dossierManagementService.undeleteDossiers(filteredDossierIds);
for (String dossierId : filteredDossierIds) {
auditPersistenceService.audit(AuditRequest.builder()
.userId(KeycloakSecurity.getUserId())
.objectId(dossierId)
@ -515,4 +517,16 @@ public class DossierController implements DossierResource {
}
}
private Set<String> filterDossierIdsByOwnedKeepUnowned(Set<String> dossierIds) {
return dossierIds.stream()
.map(id -> dossierManagementService.getDossierById(id, true, true))
.map(dossierACLService::enhanceDossierWithACLData)
.filter(dossier -> dossier.getOwnerId() == null || dossier.getOwnerId().equals(KeycloakSecurity.getUserId()))
.map(Dossier::getId)
.collect(Collectors.toSet());
}
}

View File

@ -133,7 +133,7 @@ public interface DossierResource {
@ResponseStatus(value = HttpStatus.NO_CONTENT)
@PostMapping(value = ARCHIVE_DOSSIERS_PATH + ARCHIVE_PATH)
@Operation(summary = "Archives an existing dossier.", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Successfully archived the dossier."), @ApiResponse(responseCode = "400", description = "Incorrect dossier ID entered to archive dossier."), @ApiResponse(responseCode = "403", description = "Forbidden operation while archiving."), @ApiResponse(responseCode = "404", description = "Dossier not found")})
@ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Successfully archived the dossiers."), @ApiResponse(responseCode = "400", description = "Incorrect dossier ID entered to archive dossier."), @ApiResponse(responseCode = "403", description = "Forbidden operation while archiving."), @ApiResponse(responseCode = "404", description = "Dossier not found")})
void archiveDossiers(@RequestBody Set<String> dossierIds);
@ -147,14 +147,14 @@ public interface DossierResource {
@ResponseStatus(value = HttpStatus.NO_CONTENT)
@DeleteMapping(value = DELETED_DOSSIERS_PATH + HARD_DELETE_PATH)
@Operation(summary = "Hard deletes existing dossiers.", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Successfully hard deleted the dossier."), @ApiResponse(responseCode = "404", description = "Not found")})
@ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Successfully hard deleted the dossiers."), @ApiResponse(responseCode = "404", description = "Not found")})
void hardDeleteDossiers(@RequestParam(DOSSIER_ID_PARAM) Set<String> dossierIds);
@ResponseBody
@ResponseStatus(value = HttpStatus.NO_CONTENT)
@PostMapping(value = DELETED_DOSSIERS_PATH + UNDELETE_PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Restores dossiers.", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "201", description = "Successfully restored the dossiers."), @ApiResponse(responseCode = "400", description = "Incorrect dossier ID entered to restore dossier."), @ApiResponse(responseCode = "403", description = "Forbidden operation while restoring."), @ApiResponse(responseCode = "409", description = "Conflict occurred while restoring.")})
@ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Successfully restored the dossiers."), @ApiResponse(responseCode = "400", description = "Incorrect dossier ID entered to restore dossier."), @ApiResponse(responseCode = "403", description = "Forbidden operation while restoring."), @ApiResponse(responseCode = "409", description = "Conflict occurred while restoring.")})
void undeleteDossiers(@RequestBody Set<String> dossierIds);
}

View File

@ -1,8 +1,13 @@
package com.iqser.red.service.persistence.management.v1.processor.service;
import java.util.Collection;
import java.util.Collections;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.acls.AclPermissionEvaluator;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@ -10,6 +15,8 @@ import com.iqser.red.service.persistence.management.v1.processor.acl.custom.doss
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.roles.ApplicationRoles;
import com.iqser.red.service.persistence.management.v1.processor.service.users.UserService;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.WorkflowStatus;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;
@ -23,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
public class AccessControlService {
private final FileStatusManagementService fileStatusManagementService;
private final UserService userService;
private final DossierManagementService dossierManagementService;
private final DossierACLService dossierACLService;
private final AclPermissionEvaluator aclPermissionEvaluator;
@ -120,16 +128,19 @@ public class AccessControlService {
}
@PostAuthorize("hasAuthority('RED_MANAGER') || hasPermission(#dossierId, 'Dossier', 'APPROVE') || hasPermission(#dossierId, 'Dossier', 'OWNER')")
public void verifyUserIsDossierOwnerOrApproverOrManager(String dossierId) {
}
public boolean hasUserViewPermissionsForDossier(String dossierId) {
return aclPermissionEvaluator.hasPermission(SecurityContextHolder.getContext().getAuthentication(), dossierId, "Dossier", "VIEW_OBJECT");
}
public boolean hasUserAuthority(String authority) {
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(a -> a.getAuthority().equals(authority));
}
public void verifyFileIsNotApproved(String dossierId, String fileId) {
try {

View File

@ -1,16 +1,18 @@
package com.iqser.red.service.persistence.management.v1.processor.service;
import java.util.List;
import java.util.Set;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
/*
* Service for executing Spring Security filtering operations
* Used to filter nested collections (i.e. Dossiers) in objects (i.e. DossierStatus)
* Mainly used to prevent information leakage
* Service for executing Spring Security filtering operations
* Used to filter nested collections (i.e. Dossiers) in objects (i.e. DossierStatus)
* Mainly used to prevent information leakage
*/
@Service
public class FilterByPermissionsService {
@ -39,6 +41,21 @@ public class FilterByPermissionsService {
}
@PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
public Set<String> onlyAccessibleDossierIds(Set<String> dossierIds) {
return dossierIds;
}
@PreFilter("hasPermission(filterObject, 'Dossier', 'OWNER')")
public Set<String> onlyOwnedDossierIds(Set<String> dossierIds) {
return dossierIds;
}
@PreFilter("hasPermission(filterObject.dossierId, 'Dossier', 'ACCESS_OBJECT')")
public List<Dossier> onlyAccessibleDossiers(List<Dossier> dossiers) {