From ed312abd92de2f7b070008e5ce698031bbf84fe5 Mon Sep 17 00:00:00 2001 From: siarhei-charniak Date: Thu, 2 Nov 2023 16:14:03 +0300 Subject: [PATCH] MODEXPS-167 - Implement refresh token rotation (#275) --- descriptors/ModuleDescriptor-template.json | 70 +++++++- pom.xml | 5 + .../java/org/folio/des/client/AuthClient.java | 23 --- .../des/client/DataExportSpringClient.java | 16 ++ .../folio/des/client/PermissionsClient.java | 22 --- .../org/folio/des/client/UsersClient.java | 25 --- .../config/FolioExecutionContextHelper.java | 88 ---------- .../folio/des/controller/JobsController.java | 11 ++ .../scheduling/quartz/job/OldDeleteJob.java | 13 +- .../quartz/job/acquisition/EdifactJob.java | 22 ++- .../quartz/job/bursar/BursarJob.java | 19 +- .../org/folio/des/security/AuthService.java | 79 --------- .../des/security/SecurityManagerService.java | 162 ------------------ .../folio/des/service/FolioTenantService.java | 10 +- .../des/service/impl/JobServiceImpl.java | 12 +- src/main/resources/application.yml | 8 +- .../permissions/system-user-permissions.csv | 4 +- src/main/resources/swagger.api/jobs.yaml | 29 ++++ .../FolioExecutionContextHelperTest.java | 86 ---------- .../quartz/EdifactExportJobSchedulerTest.java | 7 + .../quartz/job/OldDeleteJobTest.java | 13 +- .../job/acquisition/EdifactJobTest.java | 30 ++-- .../quartz/job/bursar/BursarJobTest.java | 26 ++- .../security/SecurityManagerServiceTest.java | 151 ---------------- .../des/service/FolioTenantServiceTest.java | 10 +- .../java/org/folio/des/support/BaseTest.java | 4 + src/test/resources/mappings/authn.json | 9 +- 27 files changed, 255 insertions(+), 699 deletions(-) delete mode 100644 src/main/java/org/folio/des/client/AuthClient.java create mode 100644 src/main/java/org/folio/des/client/DataExportSpringClient.java delete mode 100644 src/main/java/org/folio/des/client/PermissionsClient.java delete mode 100644 src/main/java/org/folio/des/client/UsersClient.java delete mode 100644 src/main/java/org/folio/des/config/FolioExecutionContextHelper.java delete mode 100644 src/main/java/org/folio/des/security/AuthService.java delete mode 100644 src/main/java/org/folio/des/security/SecurityManagerService.java delete mode 100644 src/test/java/org/folio/des/config/FolioExecutionContextHelperTest.java delete mode 100644 src/test/java/org/folio/des/security/SecurityManagerServiceTest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 44a5d255..4aeadaf6 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -68,7 +68,32 @@ "permissionsRequired": [ "data-export.job.item.post" ], - "modulePermissions": [] + "modulePermissions": [ + "accounts.transfer.post", + "accounts.collection.get", + "circulation-logs.collection.get", + "configuration.entries.collection.get", + "configuration.entries.item.get", + "configuration.entries.item.post", + "configuration.entries.item.put", + "configuration.entries.item.delete", + "data-export.job.collection.get", + "data-export.config.collection.get", + "feefineactions.collection.get", + "finance.expense-classes.item.get", + "inventory-storage.holdings.item.get", + "inventory-storage.identifier-types.item.get", + "inventory-storage.locations.item.get", + "inventory-storage.material-types.item.get", + "inventory-storage.service-points.collection.get", + "organizations-storage.organizations.item.get", + "orders-storage.po-lines.collection.get", + "orders-storage.purchase-orders.collection.get", + "transfers.collection.get", + "users.collection.get", + "users.item.post", + "users.item.put" + ] }, { "methods": [ @@ -151,6 +176,41 @@ "modulePermissions": [ "configuration.entries.item.delete" ] + }, + { + "methods": [ + "POST" + ], + "pathPattern": "/data-export-spring/jobs/send", + "permissionsRequired": [ + "data-export.job.item.send" + ], + "modulePermissions": [ + "accounts.transfer.post", + "accounts.collection.get", + "circulation-logs.collection.get", + "configuration.entries.collection.get", + "configuration.entries.item.get", + "configuration.entries.item.post", + "configuration.entries.item.put", + "configuration.entries.item.delete", + "data-export.job.collection.get", + "data-export.config.collection.get", + "feefineactions.collection.get", + "finance.expense-classes.item.get", + "inventory-storage.holdings.item.get", + "inventory-storage.identifier-types.item.get", + "inventory-storage.locations.item.get", + "inventory-storage.material-types.item.get", + "inventory-storage.service-points.collection.get", + "organizations-storage.organizations.item.get", + "orders-storage.po-lines.collection.get", + "orders-storage.purchase-orders.collection.get", + "transfers.collection.get", + "users.collection.get", + "users.item.post", + "users.item.put" + ] } ] }, @@ -250,6 +310,11 @@ "displayName": "get data export jobs", "description": "Get data export jobs" }, + { + "permissionName": "data-export.job.item.send", + "displayName": "send job to kafka", + "description": "Send job to Kafka" + }, { "permissionName": "data-export.config.all", "displayName": "data export configurations - all permissions", @@ -271,7 +336,8 @@ "data-export.job.item.get", "data-export.job.collection.get", "data-export.job.item.download", - "data-export.job.item.resend" + "data-export.job.item.resend", + "data-export.job.item.send" ] }, { diff --git a/pom.xml b/pom.xml index 721d317d..bebb77ae 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,11 @@ folio-spring-cql ${folio-spring-base.version} + + org.folio + folio-spring-system-user + ${folio-spring-base.version} + org.springframework.boot spring-boot-properties-migrator diff --git a/src/main/java/org/folio/des/client/AuthClient.java b/src/main/java/org/folio/des/client/AuthClient.java deleted file mode 100644 index ec988ed4..00000000 --- a/src/main/java/org/folio/des/client/AuthClient.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.folio.des.client; - -import org.folio.des.domain.dto.SystemUserParameters; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient("authn") -public interface AuthClient { - - @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) - ResponseEntity getApiKey(@RequestBody SystemUserParameters authObject); - - @PostMapping(value = "/credentials", consumes = MediaType.APPLICATION_JSON_VALUE) - void saveCredentials(@RequestBody SystemUserParameters systemUserParameters); - - @DeleteMapping(value = "/credentials", consumes = MediaType.APPLICATION_JSON_VALUE) - void deleteCredentials(@RequestParam("userId") String userId); -} diff --git a/src/main/java/org/folio/des/client/DataExportSpringClient.java b/src/main/java/org/folio/des/client/DataExportSpringClient.java new file mode 100644 index 00000000..deba06ba --- /dev/null +++ b/src/main/java/org/folio/des/client/DataExportSpringClient.java @@ -0,0 +1,16 @@ +package org.folio.des.client; + +import org.folio.des.config.feign.FeignClientConfiguration; +import org.folio.des.domain.dto.Job; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "data-export-spring", configuration = FeignClientConfiguration.class) +public interface DataExportSpringClient { + @PostMapping(value = "/jobs") + Job upsertJob(@RequestBody Job job); + + @PostMapping(value = "/jobs/send") + void sendJob(@RequestBody Job job); +} diff --git a/src/main/java/org/folio/des/client/PermissionsClient.java b/src/main/java/org/folio/des/client/PermissionsClient.java deleted file mode 100644 index 56d8a90b..00000000 --- a/src/main/java/org/folio/des/client/PermissionsClient.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.folio.des.client; - -import org.folio.des.domain.dto.permissions.Permission; -import org.folio.des.domain.dto.permissions.PermissionUser; -import org.folio.des.domain.dto.permissions.PermissionUserCollection; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; - -@FeignClient("perms/users") -public interface PermissionsClient { - - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - PermissionUserCollection get(@RequestParam("query") String query); - - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - PermissionUser create(@RequestBody PermissionUser permissionUser); - - @PostMapping(value = "/{userId}/permissions?indexField=userId", consumes = MediaType.APPLICATION_JSON_VALUE) - void addPermission(@PathVariable("userId") String userId, Permission permission); - -} diff --git a/src/main/java/org/folio/des/client/UsersClient.java b/src/main/java/org/folio/des/client/UsersClient.java deleted file mode 100644 index 506ec9bb..00000000 --- a/src/main/java/org/folio/des/client/UsersClient.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.folio.des.client; - -import org.folio.des.domain.dto.User; -import org.folio.des.domain.dto.UserCollection; -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; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient("users") -public interface UsersClient { - - @GetMapping - UserCollection getUsersByQuery(@RequestParam("query") String query); - - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - void saveUser(@RequestBody User user); - - @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) - void updateUser(@PathVariable String id, @RequestBody User user); -} diff --git a/src/main/java/org/folio/des/config/FolioExecutionContextHelper.java b/src/main/java/org/folio/des/config/FolioExecutionContextHelper.java deleted file mode 100644 index 944d9daa..00000000 --- a/src/main/java/org/folio/des/config/FolioExecutionContextHelper.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.folio.des.config; - -import static java.util.Objects.nonNull; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.StringUtils; -import org.folio.des.security.AuthService; -import org.folio.des.security.JWTokenUtils; -import org.folio.des.security.SecurityManagerService; -import org.folio.spring.DefaultFolioExecutionContext; -import org.folio.spring.FolioExecutionContext; -import org.folio.spring.FolioModuleMetadata; -import org.folio.spring.integration.XOkapiHeaders; -import org.folio.spring.scope.FolioExecutionContextSetter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -@Log4j2 -@RequiredArgsConstructor -public class FolioExecutionContextHelper { - - private final FolioModuleMetadata folioModuleMetadata; - private final FolioExecutionContext folioExecutionContext; - private final AuthService authService; - private final SecurityManagerService securityManagerService; - @Value("${folio.okapi.url}") - private String okapiUrl; - - public void registerTenant() { - securityManagerService.prepareSystemUser(folioExecutionContext.getOkapiUrl(), folioExecutionContext.getTenantId()); - } - - public FolioExecutionContext getFolioExecutionContext(String tenantId) { - Map> tenantOkapiHeaders = new HashMap<>() {{ - put(XOkapiHeaders.TENANT, List.of(tenantId)); - put(XOkapiHeaders.URL, List.of(okapiUrl)); - }}; - - // We only have headers['tenant', 'url'] to set up 'execution context' with minimum headers['tenant', 'url', 'token', 'user']. - // We will do it in two steps: Calling 'auth/login' does not require any permission, so in first one we create 'execution context' with headers['tenant', 'url'] - // and should be able to get a 'token' with required permissions. And then we start second 'execution context' with headers['tenant', 'url', 'token'] - // to get 'system-user-id', at this point we already have 'token' so request is authorized. ('system-user' is created when 'tenant' is registered) - try (var context = new FolioExecutionContextSetter(new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders))) { - String systemUserToken = authService.getTokenForSystemUser(tenantId, okapiUrl); - if (StringUtils.isNotBlank(systemUserToken)) { - tenantOkapiHeaders.put(XOkapiHeaders.TOKEN, List.of(systemUserToken)); - } else { - // If we do not get a 'token' we will not have required permissions to do further requests so stop the process - throw new IllegalStateException(String.format("Cannot create FolioExecutionContext for Tenant: %s because of absent token", tenantId)); - } - } - - try (var context = new FolioExecutionContextSetter(new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders))) { - String systemUserId = authService.getSystemUserId(); - if (nonNull(systemUserId)) { - tenantOkapiHeaders.put(XOkapiHeaders.USER_ID, List.of(systemUserId)); - } - } - return new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders); - } - - public static String getUserName(FolioExecutionContext context) { - String jwt = context.getToken(); - Optional userInfo = StringUtils.isBlank(jwt) ? Optional.empty() : JWTokenUtils.parseToken(jwt); - return StringUtils.substring(userInfo.map(JWTokenUtils.UserInfo::getUserName).orElse(null), 0, 50); - } - - public static UUID getUserId(FolioExecutionContext context) { - var userIdStr = context.getUserId(); - UUID result = null; - if (nonNull(userIdStr)) { - try { - result = userIdStr; - } catch (Exception ignore) { - // Nothing to do - } - } - return result; - } -} diff --git a/src/main/java/org/folio/des/controller/JobsController.java b/src/main/java/org/folio/des/controller/JobsController.java index b53e042f..3b32db02 100644 --- a/src/main/java/org/folio/des/controller/JobsController.java +++ b/src/main/java/org/folio/des/controller/JobsController.java @@ -10,10 +10,12 @@ import java.util.UUID; import lombok.extern.log4j.Log4j2; +import org.folio.des.builder.job.JobCommandSchedulerBuilder; import org.folio.des.domain.dto.ExportTypeSpecificParameters; import org.folio.des.domain.dto.Job; import org.folio.des.domain.dto.JobCollection; import org.folio.des.rest.resource.JobsApi; +import org.folio.des.service.JobExecutionService; import org.folio.des.service.JobService; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -32,6 +34,8 @@ public class JobsController implements JobsApi { private final JobService service; + private final JobCommandSchedulerBuilder jobCommandSchedulerBuilder; + private final JobExecutionService jobExecutionService; @Override public ResponseEntity getJobById(UUID id) { @@ -69,6 +73,13 @@ public ResponseEntity downloadExportedFileByJobId(UUID id) { return ResponseEntity.ok(new InputStreamResource(service.downloadExportedFile(id))); } + @Override + public ResponseEntity sendJob(Job job) { + log.info("sendJob:: with job={}.", job); + jobExecutionService.sendJobCommand(jobCommandSchedulerBuilder.buildJobCommand(job)); + return new ResponseEntity<>(HttpStatus.OK); + } + private boolean isMissingRequiredParameters(Job job) { var exportTypeParameters = job.getExportTypeSpecificParameters(); return (BULK_EDIT_QUERY == job.getType() && (isNull(job.getEntityType()) || isBlank(exportTypeParameters.getQuery()))) || diff --git a/src/main/java/org/folio/des/scheduling/quartz/job/OldDeleteJob.java b/src/main/java/org/folio/des/scheduling/quartz/job/OldDeleteJob.java index 40eba62e..da96f9e6 100644 --- a/src/main/java/org/folio/des/scheduling/quartz/job/OldDeleteJob.java +++ b/src/main/java/org/folio/des/scheduling/quartz/job/OldDeleteJob.java @@ -2,9 +2,11 @@ import static org.folio.des.scheduling.quartz.QuartzConstants.TENANT_ID_PARAM; -import org.folio.des.config.FolioExecutionContextHelper; import org.folio.des.service.JobService; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.context.ExecutionContextBuilder; import org.folio.spring.scope.FolioExecutionContextSetter; +import org.folio.spring.service.SystemUserService; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @@ -16,13 +18,14 @@ public class OldDeleteJob implements org.quartz.Job { private final JobService jobService; - private final FolioExecutionContextHelper contextHelper; + private final ExecutionContextBuilder contextBuilder; + private final SystemUserService systemUserService; private static final String PARAM_NOT_FOUND_MESSAGE = "'%s' param is missing in the jobExecutionContext"; @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { String tenantId = getTenantId(jobExecutionContext); - try (var context = new FolioExecutionContextSetter(contextHelper.getFolioExecutionContext(tenantId))) { + try (var context = new FolioExecutionContextSetter(folioExecutionContext(tenantId))) { jobService.deleteOldJobs(); } log.info("execute:: deleteOldJobs executed"); @@ -35,4 +38,8 @@ private String getTenantId(JobExecutionContext jobExecutionContext) { } return tenantId; } + + private FolioExecutionContext folioExecutionContext(String tenantId) { + return contextBuilder.forSystemUser(systemUserService.getAuthedSystemUser(tenantId)); + } } diff --git a/src/main/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJob.java b/src/main/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJob.java index fe1df9b8..045293d4 100644 --- a/src/main/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJob.java +++ b/src/main/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJob.java @@ -3,16 +3,17 @@ import static org.folio.des.scheduling.quartz.QuartzConstants.EXPORT_CONFIG_ID_PARAM; import static org.folio.des.scheduling.quartz.QuartzConstants.TENANT_ID_PARAM; -import org.folio.des.builder.job.JobCommandSchedulerBuilder; -import org.folio.des.config.FolioExecutionContextHelper; +import org.folio.des.client.DataExportSpringClient; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.Job; import org.folio.des.exceptions.SchedulingException; -import org.folio.des.service.JobExecutionService; import org.folio.des.service.JobService; import org.folio.des.service.config.impl.ExportTypeBasedConfigManager; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.context.ExecutionContextBuilder; import org.folio.spring.exception.NotFoundException; import org.folio.spring.scope.FolioExecutionContextSetter; +import org.folio.spring.service.SystemUserService; import org.quartz.JobExecutionContext; import org.quartz.JobKey; @@ -24,22 +25,21 @@ public class EdifactJob implements org.quartz.Job { private static final String PARAM_NOT_FOUND_MESSAGE = "'%s' param is missing in the jobExecutionContext"; private final ExportTypeBasedConfigManager exportTypeBasedConfigManager; - private final JobExecutionService jobExecutionService; private final JobService jobService; - private final JobCommandSchedulerBuilder jobSchedulerCommandBuilder; - private final FolioExecutionContextHelper contextHelper; + private final ExecutionContextBuilder contextBuilder; + private final SystemUserService systemUserService; + private final DataExportSpringClient dataExportSpringClient; @Override public void execute(JobExecutionContext jobExecutionContext) { try { String tenantId = getTenantId(jobExecutionContext); - try (var context = new FolioExecutionContextSetter(contextHelper.getFolioExecutionContext(tenantId))) { + try (var context = new FolioExecutionContextSetter(folioExecutionContext(tenantId))) { Job job = getJob(jobExecutionContext); Job resultJob = jobService.upsertAndSendToKafka(job, false, false); log.info("execute:: configured task saved in DB jobId: {}", resultJob.getId()); if (resultJob.getId() != null) { - var jobCommand = jobSchedulerCommandBuilder.buildJobCommand(resultJob); - jobExecutionService.sendJobCommand(jobCommand); + dataExportSpringClient.sendJob(resultJob); log.info("execute:: configured task scheduled and sent to kafka for jobId: {}", resultJob.getId()); } } @@ -95,4 +95,8 @@ private void deleteJob(JobExecutionContext jobExecutionContext) { log.warn("deleteJob:: exception deleting job '{}'", jobKey, e); } } + + private FolioExecutionContext folioExecutionContext(String tenantId) { + return contextBuilder.forSystemUser(systemUserService.getAuthedSystemUser(tenantId)); + } } diff --git a/src/main/java/org/folio/des/scheduling/quartz/job/bursar/BursarJob.java b/src/main/java/org/folio/des/scheduling/quartz/job/bursar/BursarJob.java index 4fce06d8..03dd0684 100644 --- a/src/main/java/org/folio/des/scheduling/quartz/job/bursar/BursarJob.java +++ b/src/main/java/org/folio/des/scheduling/quartz/job/bursar/BursarJob.java @@ -5,14 +5,16 @@ import java.util.Date; -import org.folio.des.config.FolioExecutionContextHelper; +import org.folio.des.client.DataExportSpringClient; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.Job; import org.folio.des.exceptions.SchedulingException; -import org.folio.des.service.JobService; import org.folio.des.service.config.impl.ExportTypeBasedConfigManager; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.context.ExecutionContextBuilder; import org.folio.spring.exception.NotFoundException; import org.folio.spring.scope.FolioExecutionContextSetter; +import org.folio.spring.service.SystemUserService; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobKey; @@ -23,9 +25,10 @@ @Log4j2 @RequiredArgsConstructor public class BursarJob implements org.quartz.Job { - private final FolioExecutionContextHelper contextHelper; + private final ExecutionContextBuilder contextBuilder; + private final SystemUserService systemUserService; private final ExportTypeBasedConfigManager exportTypeBasedConfigManager; - private final JobService jobService; + private final DataExportSpringClient dataExportSpringClient; private static final String PARAM_NOT_FOUND_MESSAGE = "'%s' param is missing in the jobExecutionContext"; @Override @@ -34,9 +37,9 @@ public void execute(JobExecutionContext jobExecutionContext) throws JobExecution String tenantId = getTenantId(jobExecutionContext); var current = new Date(); - try (var context = new FolioExecutionContextSetter(contextHelper.getFolioExecutionContext(tenantId))) { + try (var context = new FolioExecutionContextSetter(folioExecutionContext(tenantId))) { Job scheduledJob = getJob(jobExecutionContext); - Job resultJob = jobService.upsertAndSendToKafka(scheduledJob, true); + Job resultJob = dataExportSpringClient.upsertJob(scheduledJob); log.info("execute:: configureTasks executed for jobId: {} at: {}", resultJob.getId(), current); } } @@ -87,4 +90,8 @@ private void deleteJob(JobExecutionContext jobExecutionContext) { log.warn("deleteJob:: exception deleting job '{}'", jobKey, e); } } + + private FolioExecutionContext folioExecutionContext(String tenantId) { + return contextBuilder.forSystemUser(systemUserService.getAuthedSystemUser(tenantId)); + } } diff --git a/src/main/java/org/folio/des/security/AuthService.java b/src/main/java/org/folio/des/security/AuthService.java deleted file mode 100644 index 8fa4918a..00000000 --- a/src/main/java/org/folio/des/security/AuthService.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.folio.des.security; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.folio.des.client.AuthClient; -import org.folio.des.client.UsersClient; -import org.folio.des.domain.dto.SystemUserParameters; -import org.folio.des.domain.dto.User; -import org.folio.spring.integration.XOkapiHeaders; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -@Log4j2 -@RequiredArgsConstructor -public class AuthService { - - private final AuthClient authClient; - private final UsersClient usersClient; - - @Value("${folio.system.username}") - private String username; - @Value("${folio.system.password}") - private String password; - - public String getTokenForSystemUser(String tenant, String url) { - SystemUserParameters userParameters = - SystemUserParameters.builder() - .okapiUrl(url) - .tenantId(tenant) - .username(username) - .password(password) - .build(); - - log.info("Attempt login with url={} tenant={} username={}.", url, tenant, username); - - ResponseEntity authResponse = authClient.getApiKey(userParameters); - - var token = authResponse.getHeaders().get(XOkapiHeaders.TOKEN); - if (isNotEmpty(token)) { - log.info("Logged in as {}.", username); - userParameters.setOkapiToken(token.get(0)); - } else { - log.error("Can't get token logging in as {}.", username); - } - return userParameters.getOkapiToken(); - } - - public String getSystemUserId() { - Optional optionalUser = usersClient.getUsersByQuery("username==" + username).getUsers().stream().findFirst(); - - if (optionalUser.isEmpty()) { - log.error("Can't find user id by username {}.", username); - return null; - } - return optionalUser.get().getId(); - } - - private boolean isNotEmpty(java.util.List token) { - return CollectionUtils.isNotEmpty(token) && StringUtils.isNotBlank(token.get(0)); - } - - public void deleteCredentials(String userId) { - authClient.deleteCredentials(userId); - - log.info("Removed credentials for user {}.", userId); - } - - public void saveCredentials(SystemUserParameters systemUserParameters) { - authClient.saveCredentials(systemUserParameters); - - log.info("Saved credentials for user {}.", systemUserParameters.getUsername()); - } -} diff --git a/src/main/java/org/folio/des/security/SecurityManagerService.java b/src/main/java/org/folio/des/security/SecurityManagerService.java deleted file mode 100644 index 0bc0abc1..00000000 --- a/src/main/java/org/folio/des/security/SecurityManagerService.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.folio.des.security; - -import com.google.common.io.Resources; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.folio.des.client.PermissionsClient; -import org.folio.des.client.UsersClient; -import org.folio.des.domain.dto.Personal; -import org.folio.des.domain.dto.SystemUserParameters; -import org.folio.des.domain.dto.User; -import org.folio.des.domain.dto.permissions.Permission; -import org.folio.des.domain.dto.permissions.PermissionUser; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -@Log4j2 -@RequiredArgsConstructor -public class SecurityManagerService { - - private static final String PERMISSIONS_FILE_PATH = "permissions/system-user-permissions.csv"; - private static final String USER_LAST_NAME = "SystemDataExportS"; - - private final PermissionsClient permissionsClient; - private final UsersClient usersClient; - private final AuthService authService; - - @Value("${folio.system.username}") - private String username; - @Value("${folio.system.password}") - private String password; - - public void prepareSystemUser(String okapiUrl, String tenantId) { - Optional userOptional = getUser(username); - - User user; - if (userOptional.isPresent()) { - user = userOptional.get(); - updateUser(user); - } else { - user = createUser(username); - } - - try { - authService.deleteCredentials(user.getId()); - } catch (feign.FeignException.NotFound e) { - // ignore if not exist - } - authService.saveCredentials(SystemUserParameters.builder() - .id(UUID.randomUUID()) - .username(username) - .password(password) - .okapiUrl(okapiUrl) - .tenantId(tenantId) - .build()); - - Optional permissionUserOptional = permissionsClient.get("userId==" + user.getId()) - .getPermissionUsers() - .stream() - .findFirst(); - if (permissionUserOptional.isPresent()) { - addPermissions(permissionUserOptional.get()); - } else { - createPermissionUser(user.getId()); - } - } - - private Optional getUser(String username) { - return usersClient.getUsersByQuery("username==" + username).getUsers().stream().findFirst(); - } - - private User createUser(String username) { - var result = createUserObject(username); - log.info("Creating {}.", result); - usersClient.saveUser(result); - return result; - } - - private void updateUser(User user) { - if (existingUserUpToDate(user)) { - log.info("{} is up to date.", user); - } else { - populateMissingUserProperties(user); - log.info("Updating {}.", user); - usersClient.updateUser(user.getId(), user); - } - } - - private PermissionUser createPermissionUser(String userId) { - List perms = readPermissionsFromResource(PERMISSIONS_FILE_PATH); - if (CollectionUtils.isEmpty(perms)) { - throw new IllegalStateException("No user permissions found in " + PERMISSIONS_FILE_PATH); - } - - var permissionUser = PermissionUser.of(UUID.randomUUID().toString(), userId, perms); - log.info("Creating {}.", permissionUser); - return permissionsClient.create(permissionUser); - } - - private void addPermissions(PermissionUser permissionUser) { - var permissions = readPermissionsFromResource(PERMISSIONS_FILE_PATH); - if (CollectionUtils.isEmpty(permissions)) { - throw new IllegalStateException("No user permissions found in " + PERMISSIONS_FILE_PATH); - } - - permissions.removeAll(permissionUser.getPermissions()); - permissions.forEach(permission -> { - var p = new Permission(); - p.setPermissionName(permission); - try { - log.info("Adding to user {} permission {}.", permissionUser.getUserId(), p); - permissionsClient.addPermission(permissionUser.getUserId(), p); - } catch (Exception e) { - log.error(String.format("Error adding permission %s to %s.", permission, username), e); - } - }); - } - - private List readPermissionsFromResource(String permissionsFilePath) { - List result = new ArrayList<>(); - var url = Resources.getResource(permissionsFilePath); - - try { - result = Resources.readLines(url, StandardCharsets.UTF_8); - } catch (IOException e) { - log.error(String.format("Can't read user permissions from %s.", permissionsFilePath), e); - } - - return result; - } - - private User createUserObject(String username) { - final var result = new User(); - - result.setId(UUID.randomUUID().toString()); - result.setActive(true); - result.setUsername(username); - - populateMissingUserProperties(result); - - return result; - } - - private boolean existingUserUpToDate(User user) { - return user.getPersonal() != null && StringUtils.isNotBlank(user.getPersonal().getLastName()); - } - - private User populateMissingUserProperties(User user) { - user.setPersonal(new Personal()); - user.getPersonal().setLastName(USER_LAST_NAME); - return user; - } - -} diff --git a/src/main/java/org/folio/des/service/FolioTenantService.java b/src/main/java/org/folio/des/service/FolioTenantService.java index d94f3019..47254427 100644 --- a/src/main/java/org/folio/des/service/FolioTenantService.java +++ b/src/main/java/org/folio/des/service/FolioTenantService.java @@ -1,6 +1,5 @@ package org.folio.des.service; -import org.folio.des.config.FolioExecutionContextHelper; import org.folio.des.config.kafka.KafkaService; import org.folio.des.scheduling.acquisition.EdifactScheduledJobInitializer; import org.folio.des.scheduling.bursar.BursarScheduledJobInitializer; @@ -9,6 +8,7 @@ import org.folio.des.service.config.BulkEditConfigService; import org.folio.spring.FolioExecutionContext; import org.folio.spring.liquibase.FolioSpringLiquibase; +import org.folio.spring.service.PrepareSystemUserService; import org.folio.spring.service.TenantService; import org.folio.tenant.domain.dto.TenantAttributes; import org.springframework.context.annotation.Primary; @@ -22,21 +22,21 @@ @Primary public class FolioTenantService extends TenantService { - private final FolioExecutionContextHelper contextHelper; private final KafkaService kafka; private final BulkEditConfigService bulkEditConfigService; private final EdifactScheduledJobInitializer edifactScheduledJobInitializer; private final ScheduledJobsRemover scheduledJobsRemover; private final BursarScheduledJobInitializer bursarScheduledJobInitializer; private final OldJobDeleteScheduler oldJobDeleteScheduler; + private final PrepareSystemUserService prepareSystemUserService; public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext context, FolioSpringLiquibase folioSpringLiquibase, - FolioExecutionContextHelper contextHelper, KafkaService kafka, + PrepareSystemUserService prepareSystemUserService, KafkaService kafka, BulkEditConfigService bulkEditConfigService, EdifactScheduledJobInitializer edifactScheduledJobInitializer, ScheduledJobsRemover scheduledJobsRemover, BursarScheduledJobInitializer bursarScheduledJobInitializer, OldJobDeleteScheduler oldJobDeleteScheduler) { super(jdbcTemplate, context, folioSpringLiquibase); - this.contextHelper = contextHelper; + this.prepareSystemUserService = prepareSystemUserService; this.kafka = kafka; this.bulkEditConfigService = bulkEditConfigService; this.edifactScheduledJobInitializer = edifactScheduledJobInitializer; @@ -48,7 +48,7 @@ public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext conte @Override protected void afterTenantUpdate(TenantAttributes tenantAttributes) { try { - contextHelper.registerTenant(); + prepareSystemUserService.setupSystemUser(); bursarScheduledJobInitializer.initAllScheduledJob(tenantAttributes); bulkEditConfigService.checkBulkEditConfiguration(); edifactScheduledJobInitializer.initAllScheduledJob(tenantAttributes); diff --git a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java index c5bab28e..8ba224d4 100644 --- a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java +++ b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java @@ -1,5 +1,6 @@ package org.folio.des.service.impl; +import static java.util.Objects.nonNull; import static org.folio.des.domain.dto.ExportType.BULK_EDIT_IDENTIFIERS; import static org.folio.des.domain.dto.ExportType.BULK_EDIT_QUERY; import static org.folio.des.domain.dto.ExportType.BULK_EDIT_UPDATE; @@ -25,7 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.folio.des.client.ConfigurationClient; import org.folio.des.client.ExportWorkerClient; -import org.folio.des.config.FolioExecutionContextHelper; +import org.folio.des.security.JWTokenUtils; import org.folio.des.domain.dto.PresignedUrl; import org.folio.des.domain.dto.ExportType; import org.folio.des.domain.dto.ExportTypeSpecificParameters; @@ -147,7 +148,7 @@ public org.folio.des.domain.dto.Job upsertAndSendToKafka(org.folio.des.domain.dt if (StringUtils.isBlank(result.getName())) { result.setName(String.format("%06d", repository.getNextJobNumber())); } - String userName = FolioExecutionContextHelper.getUserName(context); + String userName = getUserName(context); if (StringUtils.isBlank(result.getSource())) { result.setSource(userName); } @@ -161,7 +162,7 @@ public org.folio.des.domain.dto.Job upsertAndSendToKafka(org.folio.des.domain.dt if (result.getCreatedDate() == null) { result.setCreatedDate(now); } - UUID userId = FolioExecutionContextHelper.getUserId(context); + UUID userId = context.getUserId(); if (result.getCreatedByUserId() == null) { result.setCreatedByUserId(userId); } @@ -339,4 +340,9 @@ public static Job dtoToEntity(org.folio.des.domain.dto.Job dto) { return result; } + private String getUserName(FolioExecutionContext context) { + String jwt = context.getToken(); + Optional userInfo = StringUtils.isBlank(jwt) ? Optional.empty() : JWTokenUtils.parseToken(jwt); + return StringUtils.substring(userInfo.map(JWTokenUtils.UserInfo::getUserName).orElse(null), 0, 50); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5271f85d..bbd06851 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,11 @@ folio: - system: + system-user: username: ${SYSTEM_USER_NAME:data-export-system-user} password: ${SYSTEM_USER_PASSWORD} - okapi: - url: ${OKAPI_URL:http://okapi:9130} + lastname: System + permissionsFilePath: permissions/system-user-permissions.csv + okapi-url: ${OKAPI_URL:http://okapi:9130} + environment: ${ENV:folio} tenant: validation: enabled: true diff --git a/src/main/resources/permissions/system-user-permissions.csv b/src/main/resources/permissions/system-user-permissions.csv index da9d8f35..ade71b20 100644 --- a/src/main/resources/permissions/system-user-permissions.csv +++ b/src/main/resources/permissions/system-user-permissions.csv @@ -1,3 +1,5 @@ +data-export.job.item.post +data-export.job.item.send accounts.transfer.post accounts.collection.get circulation-logs.collection.get @@ -21,4 +23,4 @@ orders-storage.purchase-orders.collection.get transfers.collection.get users.collection.get users.item.post -users.item.put +users.item.put \ No newline at end of file diff --git a/src/main/resources/swagger.api/jobs.yaml b/src/main/resources/swagger.api/jobs.yaml index 90444b40..38a6fb5e 100644 --- a/src/main/resources/swagger.api/jobs.yaml +++ b/src/main/resources/swagger.api/jobs.yaml @@ -185,6 +185,35 @@ paths: schema: type: string format: binary + /jobs/send: + post: + description: Send job via Kafka + operationId: sendJob + responses: + "200": + description: Job was sent + "400": + description: Bad Request + content: + application/json: + example: + $ref: "#/components/examples/errors" + schema: + $ref: "#/components/schemas/errors" + "500": + description: Internal server errors, e.g. due to misconfiguration + content: + application/json: + example: + $ref: "#/components/examples/errors" + schema: + $ref: "#/components/schemas/errors" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/job" + required: true components: schemas: jobStatus: diff --git a/src/test/java/org/folio/des/config/FolioExecutionContextHelperTest.java b/src/test/java/org/folio/des/config/FolioExecutionContextHelperTest.java deleted file mode 100644 index e0869207..00000000 --- a/src/test/java/org/folio/des/config/FolioExecutionContextHelperTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.folio.des.config; - -import org.folio.des.support.BaseTest; -import org.folio.spring.FolioExecutionContext; -import org.folio.spring.integration.XOkapiHeaders; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class FolioExecutionContextHelperTest extends BaseTest { - - @Autowired - private FolioExecutionContextHelper contextHelper; - - private static final String SYSTEM_USER = """ - { - "users": [ - { - "username": "data-export-system-user", - "id": "a85c45b7-d427-4122-8532-5570219c5e59", - "active": true, - "departments": [], - "proxyFor": [], - "personal": { - "addresses": [] - }, - "createdDate": "2021-03-17T15:30:07.106+00:00", - "updatedDate": "2021-03-17T15:30:07.106+00:00", - "metadata": { - "createdDate": "2021-03-17T15:21:26.064+00:00", - "updatedDate": "2021-03-17T15:30:07.043+00:00" - } - } - ], - "totalRecords": 1, - "resultInfo": { - "totalRecords": 1, - "facets": [], - "diagnostics": [] - } - } - """; - - @Test - void shouldGetFolioExecutionContext() { - // request to get token for 'data-export-system-user' - wireMockServer.stubFor( - post(urlEqualTo("/authn/login")) - .willReturn(aResponse() - .withHeader(XOkapiHeaders.TOKEN, TOKEN))); - - // request to get list of users by 'username' (='data-export-system-user') - wireMockServer.stubFor( - get(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user")) - .willReturn(aResponse() - .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .withBody(SYSTEM_USER))); - - // 'execution context' should be created according to 'data-export-system-user' headers - FolioExecutionContext executionContext = contextHelper.getFolioExecutionContext(TENANT); - - assertEquals(TENANT, executionContext.getTenantId()); - assertEquals(wireMockServer.baseUrl(), executionContext.getOkapiUrl()); - assertEquals(TOKEN, executionContext.getToken()); - assertEquals("a85c45b7-d427-4122-8532-5570219c5e59", executionContext.getUserId().toString()); - } - - @Test - void shouldGetExceptionWhenThereIsNoToken() { - // request to get response without 'XOkapiHeaders.TOKEN' - wireMockServer.stubFor( - post(urlEqualTo("/authn/login")) - .willReturn(aResponse())); - - // should get exception: verify exception type and message - Exception exception = assertThrows(IllegalStateException.class, () -> contextHelper.getFolioExecutionContext(TENANT)); - assertEquals(String.format("Cannot create FolioExecutionContext for Tenant: %s because of absent token", TENANT), exception.getMessage()); - } -} diff --git a/src/test/java/org/folio/des/scheduling/quartz/EdifactExportJobSchedulerTest.java b/src/test/java/org/folio/des/scheduling/quartz/EdifactExportJobSchedulerTest.java index cbe09ea8..fd4bdcec 100644 --- a/src/test/java/org/folio/des/scheduling/quartz/EdifactExportJobSchedulerTest.java +++ b/src/test/java/org/folio/des/scheduling/quartz/EdifactExportJobSchedulerTest.java @@ -2,6 +2,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -148,6 +149,12 @@ void testJobRemovedForRemovedConfiguration() throws SchedulerException { .scheduleFrequency(1) .timeZone(ZoneId.systemDefault().getId()); + wireMockServer.stubFor(post(urlEqualTo("/authn/login-with-expiry")) + .willReturn(aResponse().withStatus(201) + .withBody("{\"accessTokenExpiration\":\"2050-01-01T23:59:59Z\", \"refreshTokenExpiration\":\"2050-01-01T23:59:59Z\"}") + .withHeader("Content-Type", "application/json") + .withHeader("Set-Cookie", "folioAccessToken=AAA-BBB-CCC; Max-Age=600; Expires=Fri, 01 Sep 2030 13:04:35 GMT; Path=/; Secure; HTTPOnly; SameSite=None"))); + edifactExportJobScheduler.scheduleExportJob(config); // job should be scheduled var jobKeys = scheduler.getJobKeys(GroupMatcher.anyJobGroup()); diff --git a/src/test/java/org/folio/des/scheduling/quartz/job/OldDeleteJobTest.java b/src/test/java/org/folio/des/scheduling/quartz/job/OldDeleteJobTest.java index dd2a9f61..b9142ea0 100644 --- a/src/test/java/org/folio/des/scheduling/quartz/job/OldDeleteJobTest.java +++ b/src/test/java/org/folio/des/scheduling/quartz/job/OldDeleteJobTest.java @@ -7,10 +7,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.folio.des.config.FolioExecutionContextHelper; import org.folio.des.service.JobService; import org.folio.spring.FolioExecutionContext; import org.folio.spring.FolioModuleMetadata; +import org.folio.spring.context.ExecutionContextBuilder; +import org.folio.spring.model.SystemUser; +import org.folio.spring.service.SystemUserService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -31,7 +33,9 @@ class OldDeleteJobTest { @Mock private JobService jobService; @Mock - private FolioExecutionContextHelper contextHelper; + private ExecutionContextBuilder contextBuilder; + @Mock + private SystemUserService systemUserService; @InjectMocks private OldDeleteJob oldDeleteJob; @Mock @@ -41,10 +45,11 @@ class OldDeleteJobTest { @Test void testSuccessfulExecute() throws JobExecutionException { when(jobExecutionContext.getJobDetail()).thenReturn(getJobDetail()); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); doNothing().when(jobService).deleteOldJobs(); oldDeleteJob.execute(jobExecutionContext); - verify(contextHelper).getFolioExecutionContext(TENANT_ID); + verify(jobService).deleteOldJobs(); } @Test diff --git a/src/test/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJobTest.java b/src/test/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJobTest.java index 44d7a8c4..28b5e899 100644 --- a/src/test/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJobTest.java +++ b/src/test/java/org/folio/des/scheduling/quartz/job/acquisition/EdifactJobTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -12,7 +13,7 @@ import java.util.UUID; import org.folio.des.builder.job.JobCommandSchedulerBuilder; -import org.folio.des.config.FolioExecutionContextHelper; +import org.folio.des.client.DataExportSpringClient; import org.folio.des.domain.dto.EdiSchedule; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportType; @@ -26,7 +27,10 @@ import org.folio.des.service.config.impl.ExportTypeBasedConfigManager; import org.folio.spring.FolioExecutionContext; import org.folio.spring.FolioModuleMetadata; +import org.folio.spring.context.ExecutionContextBuilder; import org.folio.spring.exception.NotFoundException; +import org.folio.spring.model.SystemUser; +import org.folio.spring.service.SystemUserService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -48,7 +52,9 @@ class EdifactJobTest { @Mock private JobCommandSchedulerBuilder jobSchedulerCommandBuilder; @Mock - private FolioExecutionContextHelper contextHelper; + private ExecutionContextBuilder contextBuilder; + @Mock + private SystemUserService systemUserService; @Mock private ExportTypeBasedConfigManager exportTypeBasedConfigManager; @InjectMocks @@ -57,6 +63,8 @@ class EdifactJobTest { private JobExecutionContext jobExecutionContext; @Mock private Scheduler scheduler; + @Mock + private DataExportSpringClient dataExportSpringClient; private FolioExecutionContext folioExecutionContext = new TestFolioExecutionContext(); private static final String TENANT_ID = "some_test_tenant"; private static final String EXPORT_CONFIG_ID = "some_test_export_config_id"; @@ -65,29 +73,29 @@ class EdifactJobTest { @Test void testExecuteSuccessful() { + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); when(jobExecutionContext.getJobDetail()).thenReturn(getJobDetail()); when(exportTypeBasedConfigManager.getConfigById(EXPORT_CONFIG_ID)).thenReturn(getExportConfig()); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); when(jobService.upsertAndSendToKafka(any(), eq(false), eq(false))).thenReturn(new Job().id(UUID.randomUUID())); + doNothing().when(dataExportSpringClient).sendJob(any()); edifactJob.execute(jobExecutionContext); - verify(contextHelper).getFolioExecutionContext(TENANT_ID); verify(jobService).upsertAndSendToKafka(any(), eq(false), eq(false)); - verify(jobSchedulerCommandBuilder).buildJobCommand(any()); - verify(jobExecutionService).sendJobCommand(any()); + verify(dataExportSpringClient).sendJob(any()); } @Test void testExecuteSuccessfulSkipKafkaWhenNoJobId() { + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); when(jobExecutionContext.getJobDetail()).thenReturn(getJobDetail()); when(exportTypeBasedConfigManager.getConfigById(EXPORT_CONFIG_ID)).thenReturn(getExportConfig()); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); when(jobService.upsertAndSendToKafka(any(), eq(false), eq(false))).thenReturn(new Job()); edifactJob.execute(jobExecutionContext); - verify(contextHelper).getFolioExecutionContext(TENANT_ID); verify(jobService).upsertAndSendToKafka(any(), eq(false), eq(false)); verify(jobSchedulerCommandBuilder, times(0)).buildJobCommand(any()); verify(jobExecutionService, times(0)).sendJobCommand(any()); @@ -105,10 +113,11 @@ void testExecuteFailureWhenNoTenantIdPassed() { @Test void testExecuteFailureWhenNoExportConfigIdPassed() { + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); JobDetail jobDetail = getJobDetail(); jobDetail.getJobDataMap().remove("exportConfigId"); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); verifyExceptionThrownAndJobNotExecuted(IllegalArgumentException.class, "'exportConfigId' param is missing in the jobExecutionContext", jobExecutionContext); @@ -116,9 +125,10 @@ void testExecuteFailureWhenNoExportConfigIdPassed() { @Test void testExecuteFailureAndJobDeletedWhenExportConfigNotFound() throws SchedulerException { + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); when(jobExecutionContext.getJobDetail()).thenReturn(getJobDetail()); when(jobExecutionContext.getScheduler()).thenReturn(scheduler); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); when(exportTypeBasedConfigManager.getConfigById(EXPORT_CONFIG_ID)) .thenThrow(new NotFoundException("config not found")); diff --git a/src/test/java/org/folio/des/scheduling/quartz/job/bursar/BursarJobTest.java b/src/test/java/org/folio/des/scheduling/quartz/job/bursar/BursarJobTest.java index 5d1ec339..c58673ea 100644 --- a/src/test/java/org/folio/des/scheduling/quartz/job/bursar/BursarJobTest.java +++ b/src/test/java/org/folio/des/scheduling/quartz/job/bursar/BursarJobTest.java @@ -4,13 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.UUID; import org.folio.des.builder.job.JobCommandSchedulerBuilder; -import org.folio.des.config.FolioExecutionContextHelper; +import org.folio.des.client.DataExportSpringClient; import org.folio.des.domain.dto.EdiSchedule; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportType; @@ -24,7 +25,10 @@ import org.folio.des.service.config.impl.ExportTypeBasedConfigManager; import org.folio.spring.FolioExecutionContext; import org.folio.spring.FolioModuleMetadata; +import org.folio.spring.context.ExecutionContextBuilder; import org.folio.spring.exception.NotFoundException; +import org.folio.spring.model.SystemUser; +import org.folio.spring.service.SystemUserService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -53,7 +57,9 @@ class BursarJobTest { @Mock private JobCommandSchedulerBuilder jobSchedulerCommandBuilder; @Mock - private FolioExecutionContextHelper contextHelper; + private ExecutionContextBuilder contextBuilder; + @Mock + private SystemUserService systemUserService; @Mock private ExportTypeBasedConfigManager exportTypeBasedConfigManager; @InjectMocks @@ -62,17 +68,19 @@ class BursarJobTest { private JobExecutionContext jobExecutionContext; @Mock private Scheduler scheduler; + @Mock + private DataExportSpringClient dataExportSpringClient; private final FolioExecutionContext folioExecutionContext = new TestFolioExecutionContext(); @Test void testSuccessfulExecute() throws JobExecutionException { when(jobExecutionContext.getJobDetail()).thenReturn(getJobDetail()); when(exportTypeBasedConfigManager.getConfigById(EXPORT_CONFIG_ID)).thenReturn(getExportConfig()); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); - when(jobService.upsertAndSendToKafka(any(), eq(true))).thenReturn(new Job().id(UUID.randomUUID())); + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); + when(dataExportSpringClient.upsertJob(any())).thenReturn(new Job().id(UUID.randomUUID())); bursarJob.execute(jobExecutionContext); - verify(contextHelper).getFolioExecutionContext(TENANT_ID); - verify(jobService).upsertAndSendToKafka(any(), eq(true)); + verify(dataExportSpringClient).upsertJob(any()); } @Test @@ -90,7 +98,8 @@ void testExecuteFailureWhenConfigIdNotFoundInJobDetail() { JobDetail jobDetail = getJobDetail(); jobDetail.getJobDataMap().remove("exportConfigId"); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); String message = assertThrows(IllegalArgumentException.class, () -> bursarJob.execute(jobExecutionContext)).getMessage(); assertEquals("'exportConfigId' param is missing in the jobExecutionContext", message); @@ -99,7 +108,8 @@ void testExecuteFailureWhenConfigIdNotFoundInJobDetail() { @Test void testExecuteFailureWhenWhenConfigIdNotFoundInSettings() throws SchedulerException { when(jobExecutionContext.getJobDetail()).thenReturn(getJobDetail()); - when(contextHelper.getFolioExecutionContext(any())).thenReturn(folioExecutionContext); + when(systemUserService.getAuthedSystemUser(any())).thenReturn(SystemUser.builder().build()); + when(contextBuilder.forSystemUser(any())).thenReturn(folioExecutionContext); when(jobExecutionContext.getScheduler()).thenReturn(scheduler); when(exportTypeBasedConfigManager.getConfigById(EXPORT_CONFIG_ID)) .thenThrow(new NotFoundException("config not found")); diff --git a/src/test/java/org/folio/des/security/SecurityManagerServiceTest.java b/src/test/java/org/folio/des/security/SecurityManagerServiceTest.java deleted file mode 100644 index 36c7259c..00000000 --- a/src/test/java/org/folio/des/security/SecurityManagerServiceTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.folio.des.security; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.delete; -import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; - -import org.folio.des.support.BaseTest; -import org.folio.spring.DefaultFolioExecutionContext; -import org.folio.spring.FolioModuleMetadata; -import org.folio.spring.integration.XOkapiHeaders; -import org.folio.spring.scope.FolioExecutionContextSetter; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -class SecurityManagerServiceTest extends BaseTest { - - @Autowired private SecurityManagerService securityManagerService; - @Autowired private FolioModuleMetadata folioModuleMetadata; - private static final String SYS_USER_EXIST_RESPONSE = - "{\n" - + " \"users\": [\n" - + " {\n" - + " \"username\": \"data-export-system-user\",\n" - + " \"id\": \"a85c45b7-d427-4122-8532-5570219c5e59\",\n" - + " \"active\": true,\n" - + " \"departments\": [],\n" - + " \"proxyFor\": [],\n" - + " \"personal\": {\n" - + " \"addresses\": []\n" - + " },\n" - + " \"createdDate\": \"2021-03-17T15:30:07.106+00:00\",\n" - + " \"updatedDate\": \"2021-03-17T15:30:07.106+00:00\",\n" - + " \"metadata\": {\n" - + " \"createdDate\": \"2021-03-17T15:21:26.064+00:00\",\n" - + " \"updatedDate\": \"2021-03-17T15:30:07.043+00:00\"\n" - + " }\n" - + " }\n" - + " ],\n" - + " \"totalRecords\": 1,\n" - + " \"resultInfo\": {\n" - + " \"totalRecords\": 1,\n" - + " \"facets\": [],\n" - + " \"diagnostics\": []\n" - + " }\n" - + "}"; - - private static final String USER_PERMS_RESPONSE = - "{ \"permissionUsers\": [],\n \"totalRecords\": 0,\n \"resultInfo\": {\n \"totalRecords\": 0,\n \"facets\": [],\n \"diagnostics\": []\n }\n}"; - - - @Test - @DisplayName("Update user") - void prepareSystemUser() { - - wireMockServer.stubFor( - get(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user")) - .willReturn( - aResponse() - .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .withBody(SYS_USER_EXIST_RESPONSE))); - - wireMockServer.stubFor( - get(urlEqualTo("/perms/users?query=userId%3D%3Da85c45b7-d427-4122-8532-5570219c5e59")) - .willReturn( - aResponse() - .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .withBody(USER_PERMS_RESPONSE))); - - wireMockServer.stubFor( - delete(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59")) - .willReturn( - aResponse() - .withStatus(HttpStatus.NO_CONTENT.value()))); - - Map> tenantOkapiHeaders = new HashMap<>() {{ - put(XOkapiHeaders.TENANT, List.of(TENANT)); - put(XOkapiHeaders.URL, List.of(wireMockServer.baseUrl())); - put(XOkapiHeaders.TOKEN, List.of(TOKEN)); - }}; - - try (var context = new FolioExecutionContextSetter(new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders))) { - securityManagerService.prepareSystemUser(wireMockServer.baseUrl(), TENANT); - } - - wireMockServer.verify( - getRequestedFor(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user"))); - wireMockServer.verify( - putRequestedFor(urlEqualTo("/users/a85c45b7-d427-4122-8532-5570219c5e59"))); - wireMockServer.verify( - deleteRequestedFor(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59"))); - wireMockServer.verify( - postRequestedFor(urlEqualTo("/authn/credentials"))); - } - - @Test - @DisplayName("Update user without previous password") - void prepareSystemUserWithoutPreviousPassword() { - - wireMockServer.stubFor( - get(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user")) - .willReturn( - aResponse() - .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .withBody(SYS_USER_EXIST_RESPONSE))); - - wireMockServer.stubFor( - get(urlEqualTo("/perms/users?query=userId%3D%3Da85c45b7-d427-4122-8532-5570219c5e59")) - .willReturn( - aResponse() - .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .withBody(USER_PERMS_RESPONSE))); - - wireMockServer.stubFor( - delete(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59")) - .willReturn( - aResponse() - .withStatus(HttpStatus.NOT_FOUND.value()))); - - Map> tenantOkapiHeaders = new HashMap<>() {{ - put(XOkapiHeaders.TENANT, List.of(TENANT)); - put(XOkapiHeaders.URL, List.of(wireMockServer.baseUrl())); - put(XOkapiHeaders.TOKEN, List.of(TOKEN)); - }}; - - try (var context = new FolioExecutionContextSetter(new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders))) { - securityManagerService.prepareSystemUser(wireMockServer.baseUrl(), TENANT); - } - - wireMockServer.verify( - getRequestedFor(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user"))); - wireMockServer.verify( - putRequestedFor(urlEqualTo("/users/a85c45b7-d427-4122-8532-5570219c5e59"))); - wireMockServer.verify( - deleteRequestedFor(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59"))); - wireMockServer.verify( - postRequestedFor(urlEqualTo("/authn/credentials"))); - } -} diff --git a/src/test/java/org/folio/des/service/FolioTenantServiceTest.java b/src/test/java/org/folio/des/service/FolioTenantServiceTest.java index ea061e76..a1e3cc74 100644 --- a/src/test/java/org/folio/des/service/FolioTenantServiceTest.java +++ b/src/test/java/org/folio/des/service/FolioTenantServiceTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.folio.des.config.FolioExecutionContextHelper; import org.folio.des.config.kafka.KafkaService; import org.folio.des.scheduling.acquisition.EdifactScheduledJobInitializer; import org.folio.des.scheduling.bursar.BursarScheduledJobInitializer; @@ -14,6 +13,7 @@ import org.folio.des.scheduling.quartz.ScheduledJobsRemover; import org.folio.des.service.config.BulkEditConfigService; import org.folio.spring.FolioExecutionContext; +import org.folio.spring.service.PrepareSystemUserService; import org.folio.tenant.domain.dto.TenantAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,8 +27,6 @@ class FolioTenantServiceTest { @InjectMocks FolioTenantService folioTenantService; - @Mock - FolioExecutionContextHelper contextHelper; @Mock KafkaService kafka; @Mock @@ -45,12 +43,14 @@ class FolioTenantServiceTest { @Mock OldJobDeleteScheduler oldJobDeleteScheduler; + @Mock + PrepareSystemUserService prepareSystemUserService; @Test void shouldDoProcessAfterTenantUpdating() { TenantAttributes tenantAttributes = createTenantAttributes(); - doNothing().when(contextHelper).registerTenant(); + doNothing().when(prepareSystemUserService).setupSystemUser(); doNothing().when(bulkEditConfigService).checkBulkEditConfiguration(); doNothing().when(edifactScheduledJobInitializer).initAllScheduledJob(tenantAttributes); doNothing().when(kafka).createKafkaTopics(); @@ -60,7 +60,7 @@ void shouldDoProcessAfterTenantUpdating() { folioTenantService.afterTenantUpdate(tenantAttributes); - verify(contextHelper, times(1)).registerTenant(); + verify(prepareSystemUserService).setupSystemUser(); verify(bulkEditConfigService, times(1)).checkBulkEditConfiguration(); verify(edifactScheduledJobInitializer, times(1)).initAllScheduledJob(tenantAttributes); verify(bursarScheduledJobInitializer, times(1)).initAllScheduledJob(tenantAttributes); diff --git a/src/test/java/org/folio/des/support/BaseTest.java b/src/test/java/org/folio/des/support/BaseTest.java index 5fbcbaf8..a24a18dd 100644 --- a/src/test/java/org/folio/des/support/BaseTest.java +++ b/src/test/java/org/folio/des/support/BaseTest.java @@ -6,6 +6,7 @@ import java.util.List; +import org.folio.spring.config.properties.FolioEnvironment; import org.folio.spring.integration.XOkapiHeaders; import org.folio.tenant.domain.dto.TenantAttributes; import org.junit.jupiter.api.AfterAll; @@ -59,6 +60,8 @@ public abstract class BaseTest { protected MockMvc mockMvc; @Autowired protected Scheduler scheduler; + @Autowired + private FolioEnvironment folioEnvironment; static { postgreDBContainer.start(); @@ -85,6 +88,7 @@ static void beforeAll(@Autowired MockMvc mockMvc) { @BeforeEach void beforeEach() throws SchedulerException { + folioEnvironment.setOkapiUrl(wireMockServer.baseUrl()); scheduler.clear(); } diff --git a/src/test/resources/mappings/authn.json b/src/test/resources/mappings/authn.json index 0c071848..620dd38e 100644 --- a/src/test/resources/mappings/authn.json +++ b/src/test/resources/mappings/authn.json @@ -3,14 +3,15 @@ { "request": { "method": "POST", - "url": "/authn/login" + "url": "/authn/login-with-expiry" }, "response": { - "status": 200, + "status": 201, "headers": { "Content-Type": "application/json", - "x-okapi-token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWt1X2FkbWluIiwidXNlcl9pZCI6IjFkM2I1OGNiLTA3YjUtNWZjZC04YTJhLTNjZTA2YTBlYjkwZiIsImlhdCI6MTYxNjQyMDM5MywidGVuYW50IjoiZGlrdSJ9.2nvEYQBbJP1PewEgxixBWLHSX_eELiBEBpjufWiJZRs" - } + "set-cookie": "folioAccessToken=AAA-BBB-CCC; Max-Age=600; Expires=Fri, 01 Sep 2030 13:04:35 GMT; Path=/; Secure; HTTPOnly; SameSite=None" + }, + "body": "{ \n \"accessTokenExpiration\": \"2030-09-01T13:04:35Z\",\n \"refreshTokenExpiration\": \"2030-09-08T12:54:35Z\"\n}" } }, {