Skip to content

Commit

Permalink
MDEXP-786 - Export Holdings and Items with Custom Export Profile from…
Browse files Browse the repository at this point in the history
… Central Tenant - Permissions handling (#506)

* MDEXP-786 Squashed
  • Loading branch information
obozhko-folio authored Oct 23, 2024
1 parent 75f39a0 commit cb30ad7
Show file tree
Hide file tree
Showing 20 changed files with 468 additions and 14 deletions.
28 changes: 24 additions & 4 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
{
"id": "search",
"version": "0.6 1.0"
},
{
"id": "permissions",
"version": "5.6"
}
],
"provides": [
Expand Down Expand Up @@ -130,7 +134,8 @@
"search.resources.ids.jobs.get",
"consortium-search.holdings.collection.get",
"consortia.user-tenants.collection.get",
"consortium-search.holdings.item.get"
"consortium-search.holdings.item.get",
"data-export.permissions-self-check.get"
]
},
{
Expand Down Expand Up @@ -166,7 +171,8 @@
"user-tenants.collection.get",
"consortium-search.holdings.collection.get",
"consortia.user-tenants.collection.get",
"consortium-search.holdings.item.get"
"consortium-search.holdings.item.get",
"data-export.permissions-self-check.get"
]
},
{
Expand Down Expand Up @@ -462,8 +468,16 @@
]
},
{
"methods": [
"GET"
"methods": [
"GET"
],
"pathPattern": "/data-export/permissions-self-check",
"permissionsRequired": ["data-export.permissions-self-check.get"],
"modulePermissions": ["perms.users.get"]
},
{
"methods": [
"GET"
],
"pathPattern": "/data-export/download-record/{recordId}",
"permissionsRequired": [
Expand Down Expand Up @@ -661,6 +675,11 @@
"displayName": "Data Export - deleted authorities",
"description": "To return deleted authorities"
},
{
"permissionName" : "data-export.permissions-self-check.get",
"displayName" : "Set of users permissions for self check",
"description" : "Set of users permissions for self check"
},
{
"permissionName": "data-export.download-record.item.get",
"displayName": "Data Export - download record",
Expand Down Expand Up @@ -698,6 +717,7 @@
"data-export.related-users.collection.get",
"data-export.export-deleted.post",
"data-export.export-authority-deleted.post",
"data-export.permissions-self-check.get",
"data-export.download-record.item.get"
],
"visible": false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.folio.dataexp.client;

import org.folio.spring.config.FeignClientConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@FeignClient(name = "data-export", configuration = FeignClientConfiguration.class)
public interface PermissionsSelfCheckClient {

@GetMapping(value = "/permissions-self-check", produces = MediaType.APPLICATION_JSON_VALUE)
List<String> getUserPermissionsForSelfCheck();
}
15 changes: 15 additions & 0 deletions src/main/java/org/folio/dataexp/client/UserPermissionsClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.folio.dataexp.client;

import org.folio.dataexp.domain.dto.UserPermissions;
import org.folio.spring.config.FeignClientConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "perms/users", configuration = FeignClientConfiguration.class)
public interface UserPermissionsClient {

@GetMapping(value = "/{userId}/permissions?expanded=true&indexField=userId", produces = MediaType.APPLICATION_JSON_VALUE)
UserPermissions getPermissions(@PathVariable String userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.folio.dataexp.controllers;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.dataexp.client.UserPermissionsClient;
import org.folio.spring.FolioExecutionContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/data-export")
@RequiredArgsConstructor
@Log4j2
public class PermissionsSelfCheckController implements org.folio.dataexp.rest.resource.PermissionsSelfCheckApi {
private final FolioExecutionContext folioExecutionContext;
private final UserPermissionsClient userPermissionsClient;

@Override
public ResponseEntity<List<String>> getUsersPermissions() {
var permissions = userPermissionsClient.getPermissions(folioExecutionContext.getUserId().toString());
return new ResponseEntity<>(permissions.getPermissionNames(), HttpStatus.OK);
}
}
22 changes: 22 additions & 0 deletions src/main/java/org/folio/dataexp/domain/dto/UserPermissions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.folio.dataexp.domain.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.With;

import java.util.ArrayList;
import java.util.List;

@Data
@With
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class UserPermissions {

@JsonProperty("permissionNames")
private List<String> permissionNames = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.folio.dataexp.service.export.LocalStorageWriter;
import org.folio.dataexp.service.export.strategies.handlers.RuleHandler;
import org.folio.dataexp.service.transformationfields.ReferenceDataProvider;
import org.folio.dataexp.service.validators.PermissionsValidator;
import org.folio.processor.RuleProcessor;
import org.folio.spring.FolioModuleMetadata;
import org.springframework.data.domain.PageRequest;
Expand All @@ -46,10 +47,11 @@ public HoldingsExportAllStrategy(InstanceEntityRepository instanceEntityReposito
HoldingsRecordEntityTenantRepository holdingsRecordEntityTenantRepository, MarcInstanceRecordRepository marcInstanceRecordRepository,
InstanceCentralTenantRepository instanceCentralTenantRepository, FolioModuleMetadata folioModuleMetadata,
HoldingsRecordEntityRepository holdingsRecordEntityRepository, MarcRecordEntityRepository marcRecordEntityRepository,
FolioHoldingsAllRepository folioHoldingsAllRepository, MarcHoldingsAllRepository marcHoldingsAllRepository, UserService userService) {
FolioHoldingsAllRepository folioHoldingsAllRepository, MarcHoldingsAllRepository marcHoldingsAllRepository, UserService userService,
PermissionsValidator permissionsValidator) {
super(instanceEntityRepository, itemEntityRepository, ruleFactory, ruleProcessor, ruleHandler, referenceDataProvider,
consortiaService, consortiumSearchClient, holdingsRecordEntityTenantRepository, marcInstanceRecordRepository,
instanceCentralTenantRepository, folioModuleMetadata, userService, holdingsRecordEntityRepository, marcRecordEntityRepository);
instanceCentralTenantRepository, folioModuleMetadata, userService, holdingsRecordEntityRepository, marcRecordEntityRepository, permissionsValidator);
this.folioHoldingsAllRepository = folioHoldingsAllRepository;
this.marcHoldingsAllRepository = marcHoldingsAllRepository;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static org.folio.dataexp.service.export.Constants.INSTANCE_KEY;
import static org.folio.dataexp.service.export.Constants.ITEMS_KEY;
import static org.folio.dataexp.util.ErrorCode.ERROR_CONVERTING_TO_JSON_HOLDING;
import static org.folio.dataexp.util.ErrorCode.ERROR_HOLDINGS_NO_PERMISSION;
import static org.folio.dataexp.util.ErrorCode.ERROR_MESSAGE_JSON_CANNOT_BE_CONVERTED_TO_MARC;
import static org.folio.dataexp.util.ErrorCode.ERROR_MESSAGE_NO_AFFILIATION;
import static org.folio.dataexp.util.ErrorCode.ERROR_MESSAGE_TENANT_NOT_FOUND_FOR_HOLDING;
Expand Down Expand Up @@ -37,6 +38,7 @@
import org.folio.dataexp.service.ConsortiaService;
import org.folio.dataexp.service.export.strategies.handlers.RuleHandler;
import org.folio.dataexp.service.transformationfields.ReferenceDataProvider;
import org.folio.dataexp.service.validators.PermissionsValidator;
import org.folio.processor.RuleProcessor;
import org.folio.processor.referencedata.ReferenceDataWrapper;
import org.folio.processor.rule.Rule;
Expand Down Expand Up @@ -82,6 +84,7 @@ public class HoldingsExportStrategy extends AbstractExportStrategy {

protected final HoldingsRecordEntityRepository holdingsRecordEntityRepository;
protected final MarcRecordEntityRepository marcRecordEntityRepository;
protected final PermissionsValidator permissionsValidator;

private Map<String, Set<UUID>> tenantIdsMap;

Expand Down Expand Up @@ -238,7 +241,14 @@ private Map<String, Set<UUID>> getTenantIds(Set<UUID> ids, String centralTenantI
log.info("ID: {}, tenant: {}, actualTenant: {}", id, curTenant, folioExecutionContext.getTenantId());
if (nonNull(curTenant)) {
if (availableTenants.contains(curTenant) || curTenant.equals(centralTenantId)) {
idsMap.computeIfAbsent(curTenant, k -> new HashSet<>()).add(id);
if (permissionsValidator.checkInstanceViewPermissions(curTenant)) {
idsMap.computeIfAbsent(curTenant, k -> new HashSet<>()).add(id);
} else {
var msgValues = List.of(id.toString(), userService.getUserName(folioExecutionContext.getTenantId(), folioExecutionContext.getUserId().toString()),
curTenant);
errorLogService.saveGeneralErrorWithMessageValues(ERROR_HOLDINGS_NO_PERMISSION.getCode(), msgValues, jobExecutionId);
log.error(format(ERROR_HOLDINGS_NO_PERMISSION.getDescription(), msgValues.toArray()));
}
} else {
var msgValues = List.of(id.toString(), userService.getUserName(folioExecutionContext.getTenantId(), folioExecutionContext.getUserId().toString()), curTenant);
errorLogService.saveGeneralErrorWithMessageValues(ERROR_MESSAGE_NO_AFFILIATION.getCode(), msgValues, jobExecutionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.folio.dataexp.repository.ItemEntityTenantRepository;
import org.folio.dataexp.service.ConsortiaService;
import org.folio.dataexp.service.logs.ErrorLogService;
import org.folio.dataexp.service.validators.PermissionsValidator;
import org.folio.spring.FolioExecutionContext;
import org.springframework.stereotype.Service;

Expand All @@ -27,10 +28,12 @@
import java.util.UUID;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static org.folio.dataexp.service.export.Constants.HOLDINGS_KEY;
import static org.folio.dataexp.service.export.Constants.INSTANCE_HRID_KEY;
import static org.folio.dataexp.service.export.Constants.ITEMS_KEY;
import static org.folio.dataexp.service.export.strategies.AbstractExportStrategy.getAsJsonObject;
import static org.folio.dataexp.util.ErrorCode.ERROR_INSTANCE_NO_PERMISSION;
import static org.folio.dataexp.util.ErrorCode.ERROR_MESSAGE_NO_AFFILIATION;

@Log4j2
Expand All @@ -45,11 +48,13 @@ public class HoldingsItemsResolverService {
private final ConsortiaService consortiaService;
private final UserService userService;
private final ErrorLogService errorLogService;
private final PermissionsValidator permissionsValidator;

@PersistenceContext
protected EntityManager entityManager;

public void retrieveHoldingsAndItemsByInstanceId(JSONObject instance, UUID instanceId, String instanceHrid, MappingProfile mappingProfile, UUID jobExecutionId) {
log.info("retrieveHoldingsAndItemsByInstanceId");
if (!isNeedUpdateWithHoldingsOrItems(mappingProfile)) {
return;
}
Expand All @@ -72,13 +77,27 @@ private void retrieveHoldingsAndItemsByInstanceIdForCentralTenant(JSONObject ins
.filter(h -> !folioExecutionContext.getTenantId().equals(h.getTenantId()))
.collect(Collectors.groupingBy(ConsortiumHolding::getTenantId, Collectors.mapping(ConsortiumHolding::getId, Collectors.toList())));
var userTenants = consortiaService.getAffiliatedTenants(folioExecutionContext.getTenantId(), folioExecutionContext.getUserId().toString());
boolean errorForInstanceAlreadySaved = false;
log.info("consortiaHoldingsIdsPerTenant: {}", consortiaHoldingsIdsPerTenant);
for (var entry : consortiaHoldingsIdsPerTenant.entrySet()) {
log.info("entry: {}", entry);
var localTenant = entry.getKey();
var holdingsIds = entry.getValue().stream().map(UUID::fromString).collect(Collectors.toSet());
if (userTenants.contains(localTenant)) {
var holdingsEntities = holdingsRecordEntityTenantRepository.findByIdIn(localTenant, holdingsIds);
entityManager.clear();
addHoldingsAndItems(instance, holdingsEntities, instanceHrid, mappingProfile, localTenant);
if (permissionsValidator.checkInstanceViewPermissions(localTenant)) {
var holdingsEntities = holdingsRecordEntityTenantRepository.findByIdIn(localTenant, holdingsIds);
entityManager.clear();
addHoldingsAndItems(instance, holdingsEntities, instanceHrid, mappingProfile, localTenant);
} else {
if (!errorForInstanceAlreadySaved) {
var msgValues = List.of(instanceId.toString(), userService.getUserName(folioExecutionContext.getTenantId(), folioExecutionContext.getUserId().toString()),
localTenant);
errorLogService.saveGeneralErrorWithMessageValues(ERROR_INSTANCE_NO_PERMISSION.getCode(), msgValues, jobExecutionId);
log.error(format(ERROR_INSTANCE_NO_PERMISSION.getDescription(), msgValues.toArray()));
errorForInstanceAlreadySaved = true;
}
}

} else {
var userName = userService.getUserName(folioExecutionContext.getTenantId(), folioExecutionContext.getUserId().toString());
holdingsIds.forEach(holdingId -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.folio.dataexp.service.permissions;

import lombok.RequiredArgsConstructor;
import org.folio.dataexp.client.PermissionsSelfCheckClient;
import org.folio.spring.FolioExecutionContext;
import org.folio.spring.FolioModuleMetadata;
import org.folio.spring.scope.FolioExecutionContextSetter;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import java.util.List;

import static org.folio.dataexp.util.FolioExecutionContextUtil.prepareContextForTenant;

@Component
@RequiredArgsConstructor
public class PermissionsProvider {

private final PermissionsSelfCheckClient permissionsSelfCheckClient;
private final FolioExecutionContext folioExecutionContext;
private final FolioModuleMetadata folioModuleMetadata;

@Cacheable(cacheNames = "userPermissions")
public List<String> getUserPermissions(String tenantId, String userId) {
try (var ignored = new FolioExecutionContextSetter(prepareContextForTenant(tenantId, folioModuleMetadata, folioExecutionContext))) {
return permissionsSelfCheckClient.getUserPermissionsForSelfCheck();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.folio.dataexp.service.permissions;

import static org.folio.dataexp.util.Constants.INVENTORY_VIEW_PERMISSION;

import org.springframework.stereotype.Component;

@Component
public class RequiredPermissionResolver {

public String getReadPermission() {
return INVENTORY_VIEW_PERMISSION;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.folio.dataexp.service.validators;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.dataexp.service.permissions.PermissionsProvider;
import org.folio.dataexp.service.permissions.RequiredPermissionResolver;
import org.folio.spring.FolioExecutionContext;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Log4j2
public class PermissionsValidator {

private final PermissionsProvider permissionsProvider;
private final RequiredPermissionResolver requiredPermissionResolver;
private final FolioExecutionContext folioExecutionContext;

public boolean checkInstanceViewPermissions(String tenantId) {
return isInstanceViewPermissionExists(tenantId);
}

public boolean isInstanceViewPermissionExists(String tenantId) {
var readPermissionForEntity = requiredPermissionResolver.getReadPermission();
var userPermissions = permissionsProvider.getUserPermissions(tenantId, folioExecutionContext.getUserId().toString());
var isViewPermissionsExist = userPermissions.contains(readPermissionForEntity);
log.info("isInstanceViewPermissionExists:: user {} has read permissions {} in tenant {}", folioExecutionContext.getUserId(),
isViewPermissionsExist, tenantId);
return isViewPermissionsExist;
}
}
1 change: 1 addition & 0 deletions src/main/java/org/folio/dataexp/util/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public class Constants {
public static final String DEFAULT_AUTHORITY_DELETED_JOB_PROFILE_ID = "2c9be114-6d35-4408-adac-9ead35f51a27";
public static final String DELETED_MARC_IDS_FILE_NAME = "deleted-marc-bib-records.csv";
public static final String DELETED_AUTHORITIES_FILE_NAME = "deleted-authority-records.csv";
public static final String INVENTORY_VIEW_PERMISSION = "ui-inventory.instance.view";
}
4 changes: 3 additions & 1 deletion src/main/java/org/folio/dataexp/util/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public enum ErrorCode {
ERROR_CONVERTING_TO_JSON_INSTANCE("error.convertingToJson.instance", "Error converting to json instance by id %s"),
ERROR_DELETED_DUPLICATED_INSTANCE("error.deletedDuplicate.instance", "Instance record associated with %s has been deleted."),
ERROR_DELETED_TOO_LONG_INSTANCE("error.deletedTooLong.instance", "Instance record with id = %s has been deleted."),
ERROR_NON_EXISTING_INSTANCE("error.nonExisting.instance", "%s");
ERROR_NON_EXISTING_INSTANCE("error.nonExisting.instance", "%s"),
ERROR_HOLDINGS_NO_PERMISSION("error.holdings.noPermission", "%s - the user %s does not have permissions to access the holdings record in %s data tenant."),
ERROR_INSTANCE_NO_PERMISSION("error.instance.noPermission", "%s the user %s does not have permissions to view holdings or items in %s data tenant.");

private final String code;
private final String description;
Expand Down
Loading

0 comments on commit cb30ad7

Please sign in to comment.