From 1a0fcae324feaa4c8b5c97af6353c636aedaa47d Mon Sep 17 00:00:00 2001 From: elmiomar Date: Wed, 11 Oct 2023 12:44:21 -0400 Subject: [PATCH 01/10] add support for uncaching --- .../distrib/service/RPACachingService.java | 7 +++++ .../distrib/web/RPADataCachingController.java | 30 +++++++++++++++++++ .../web/RPARequestHandlerController.java | 4 +-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java b/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java index ec937291..5e51793a 100644 --- a/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java +++ b/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java @@ -62,6 +62,7 @@ public String cacheAndGenerateRandomId(String datasetID, String version) logger.debug("Request to cache dataset with ID=" + datasetID); + // this is to handle ark IDs String dsid = datasetID; if (datasetID.startsWith("ark:/")) { // Split the dataset ID into components @@ -246,4 +247,10 @@ private String getDownloadUrl(String baseDownloadUrl, String randomId, String pa private String generateRandomID(int length, boolean useLetters, boolean useNumbers) { return RandomStringUtils.random(length, useLetters, useNumbers); } + + public boolean uncacheById(String randomId) throws CacheManagementException { + logger.debug("Request to cache dataset with ID=" + randomId); + this.pdrCacheManager.uncache(randomId); + return false; + } } \ No newline at end of file diff --git a/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java b/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java index f585194c..53e84289 100644 --- a/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java +++ b/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -112,6 +113,35 @@ public Map retrieveMetadata(@PathVariable("cacheid") String cach return metadata; } + /** + * This endpoint handles the removal of a dataset from the cache based on its randomly generated ID. + * + * @param randomId The randomly generated ID that was assigned when the dataset was cached. + * + * @return A ResponseEntity containing a status message and HTTP status code. + * + * @throws CacheManagementException If there is an issue with removing the dataset from the cache. + * @throws ResourceNotFoundException If the dataset associated with the randomId is not found in the cache. + * @throws StorageVolumeException If there is an issue with the storage volume. + */ + @PutMapping(value = "/uncache/{randomId}") + public ResponseEntity uncacheDataset( + @PathVariable("randomId") String randomId) + throws CacheManagementException { + + logger.debug("randomId to uncache=" + randomId); + + // Perform uncache operation via the service + boolean success = restrictedSrvc.uncacheById(randomId); + + if (success) { + return new ResponseEntity<>("Dataset was successfully removed from cache.", HttpStatus.OK); + } else { + return new ResponseEntity<>("Failed to uncache the dataset.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @ExceptionHandler(MetadataNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorInfo handleMetadataNotFoundException(MetadataNotFoundException ex) { diff --git a/src/main/java/gov/nist/oar/distrib/web/RPARequestHandlerController.java b/src/main/java/gov/nist/oar/distrib/web/RPARequestHandlerController.java index 0ddee587..183b3f3a 100644 --- a/src/main/java/gov/nist/oar/distrib/web/RPARequestHandlerController.java +++ b/src/main/java/gov/nist/oar/distrib/web/RPARequestHandlerController.java @@ -299,8 +299,8 @@ public ResponseEntity updateRecord(@PathVariable String id, @RequestBody RecordP LOGGER.debug("Missing required claim detected: " + missingClaimName); throw new InvalidRequestException("JWT token invalid"); } catch (JwtException e) { - LOGGER.debug("Token validation failed due to JwtException: " + e.getMessage()); - throw new RequestProcessingException("JWT token validation failed"); + LOGGER.debug("Token validation failed due to a JwtException: " + e.getMessage()); + throw new UnauthorizedException("JWT token validation failed"); } if (tokenDetails != null) { From 668350e8c7f41b436e2cd932e2f7426039fc809c Mon Sep 17 00:00:00 2001 From: elmiomar Date: Wed, 1 Nov 2023 11:07:07 -0400 Subject: [PATCH 02/10] add rpa- keyword to random id --- .../distrib/service/RPACachingService.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java b/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java index 5e51793a..51e04da8 100644 --- a/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java +++ b/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java @@ -74,7 +74,9 @@ public String cacheAndGenerateRandomId(String datasetID, String version) } logger.debug("Caching dataset with dsid=" + dsid); - String randomID = generateRandomID(RANDOM_ID_LENGTH, true, true); + // append "rpa-" with the generated random ID + String randomID = "rpa-" + generateRandomID(RANDOM_ID_LENGTH, true, true); + int prefs = ROLE_RESTRICTED_DATA; if (!version.isEmpty()) @@ -248,9 +250,44 @@ private String generateRandomID(int length, boolean useLetters, boolean useNumbe return RandomStringUtils.random(length, useLetters, useNumbers); } + /** + * Uncache dataset objects using a specified random ID. + * + * @param randomId - The random ID used to fetch and uncache dataset objects. + * @return boolean - True if at least one dataset object was uncached successfully; otherwise, false. + * @throws CacheManagementException if an error occurs during the uncaching process. + */ public boolean uncacheById(String randomId) throws CacheManagementException { - logger.debug("Request to cache dataset with ID=" + randomId); - this.pdrCacheManager.uncache(randomId); - return false; + // Validate input + if (randomId == null || randomId.isEmpty()) { + throw new IllegalArgumentException("Random ID cannot be null or empty."); + } + + logger.debug("Request to uncache dataset with ID=" + randomId); + + // Retrieve dataset objects using the randomId + List objects = this.pdrCacheManager.selectDatasetObjects(randomId, this.pdrCacheManager.VOL_FOR_INFO); + + if (objects.isEmpty()) { + logger.debug("No objects found for ID=" + randomId); + return false; + } + + boolean isUncached = false; + + // Iterate through the retrieved objects and attempt to uncache them + for (CacheObject obj : objects) { + try { + logger.debug("Deleting file with ID=" + obj.id); + this.pdrCacheManager.uncache(obj.id); + isUncached = true; + } catch (CacheManagementException e) { + // Log the exception without throwing it to continue attempting to uncache remaining objects + logger.error("Failed to uncache object with ID=" + obj.id, e); + } + } + + return isUncached; } + } \ No newline at end of file From 878f44fd2be17c0db77a6e6c36d9a0b54e8d7cad Mon Sep 17 00:00:00 2001 From: elmiomar Date: Wed, 1 Nov 2023 11:10:36 -0400 Subject: [PATCH 03/10] update caching logic: cache first then append randomId to status --- ...URLConnectionRPARequestHandlerService.java | 48 ++++++++++++------- .../service/rpa/RecordResponseHandler.java | 2 +- .../rpa/RecordResponseHandlerImpl.java | 18 ++++--- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java index 1ba31865..0de195e1 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java @@ -98,6 +98,7 @@ public class HttpURLConnectionRPARequestHandlerService implements IRPARequestHan */ private RecordResponseHandler recordResponseHandler; + private RPADatasetCacher rpaDatasetCacher; /** * Sets the HTTP URL connection factory. * @@ -164,6 +165,9 @@ public HttpURLConnectionRPARequestHandlerService(RPAConfiguration rpaConfigurati this.recordResponseHandler = new RecordResponseHandlerImpl(this.rpaConfiguration, this.connectionFactory, rpaCachingService); + // Set RPADatasetCacher + this.rpaDatasetCacher = new DefaultRPADatasetCacher(rpaCachingService); + // Set HttpClient this.httpClient = HttpClients.createDefault(); @@ -388,13 +392,16 @@ public RecordStatus updateRecord(String recordId, String status, String smeId) t // Initialize return object RecordStatus recordStatus; - // Get endpoint - String updateRecordUri = getConfig().getSalesforceEndpoints().get(UPDATE_RECORD_ENDPOINT_KEY); + // Cache here before updating the status in SF + LOGGER.info("Starting caching..."); + Record record = this.getRecord(recordId).getRecord(); + String datasetId = record.getUserInfo().getSubject(); + String randomId = this.rpaDatasetCacher.cache(datasetId); + if (randomId == null) + throw new RequestProcessingException("Caching process returned a null randomId"); // Create a valid approval status based on input - String approvalStatus = generateApprovalStatus(status, smeId); - - // TODO: try caching here before updating the status in SF + String approvalStatus = generateApprovalStatus(status, smeId, randomId); // PATCH request payload // Approval_Status__c is how SF service expect the key @@ -404,6 +411,9 @@ public RecordStatus updateRecord(String recordId, String status, String smeId) t // Get token JWTToken token = jwtHelper.getToken(); + // Get endpoint + String updateRecordUri = getConfig().getSalesforceEndpoints().get(UPDATE_RECORD_ENDPOINT_KEY); + // Build request URL String url; try { @@ -448,12 +458,9 @@ public RecordStatus updateRecord(String recordId, String status, String smeId) t throw new RequestProcessingException("I/O error: " + e.getMessage()); } - // Retrieve updated record from SF service - Record record = this.getRecord(recordId).getRecord(); - // Check if status is approved if (recordStatus.getApprovalStatus().toLowerCase().contains("approved")) { - this.recordResponseHandler.onRecordUpdateApproved(record); + this.recordResponseHandler.onRecordUpdateApproved(record, randomId); } else { this.recordResponseHandler.onRecordUpdateDeclined(record); } @@ -462,24 +469,29 @@ public RecordStatus updateRecord(String recordId, String status, String smeId) t } /** - * Generates an approval status string based on the given status and current date/time. - * The date is in ISO 8601 format. + * Generates an approval status string based on the given status, current date/time, and random ID. + * The date is in ISO 8601 format. If the status is "Declined", the randomId will not be appended. * - * @param status the approval status to use, either "Approved" or "Declined" - * @param email the email to append to the status - * @return the generated approval status string, in the format "[status]_[yyyy-MM-dd'T'HH:mm:ss.SSSZ]_[email]" + * @param status the approval status to use, either "Approved" or "Declined" + * @param smeId the SME ID to append to the status + * @param randomId the generated random ID to append (only if status is "Approved") + * @return the generated approval status string. + * If status is "Approved", the format is: + * "[status]_[yyyy-MM-dd'T'HH:mm:ss.SSSZ]_[smeId]_[randomId]". + * If status is "Declined", the format is: + * "[status]_[yyyy-MM-dd'T'HH:mm:ss.SSSZ]_[smeId]" * @throws InvalidRequestException if the provided status is not "Approved" or "Declined" */ - private String generateApprovalStatus(String status, String smeId) throws InvalidRequestException { + private String generateApprovalStatus(String status, String smeId, String randomId) throws InvalidRequestException { String formattedDate = Instant.now().toString(); // ISO 8601 format: 2023-05-09T15:59:03.872Z String approvalStatus; if (status != null) { switch (status.toLowerCase()) { case RECORD_APPROVED_STATUS: - approvalStatus = "Approved_"; + approvalStatus = "Approved_" + formattedDate + "_" + smeId + "_" + randomId; break; case RECORD_DECLINED_STATUS: - approvalStatus = "Declined_"; + approvalStatus = "Declined_" + formattedDate + "_" + smeId; break; default: throw new InvalidRequestException("Invalid approval status: " + status); @@ -487,7 +499,7 @@ private String generateApprovalStatus(String status, String smeId) throws Invali } else { throw new InvalidRequestException("Invalid approval status: status is null"); } - return approvalStatus + formattedDate + "_" + smeId; + return approvalStatus; } } diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandler.java b/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandler.java index a1941a2e..7e29f588 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandler.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandler.java @@ -23,7 +23,7 @@ public interface RecordResponseHandler { * This method is called when a record update operation is successful and user was approved. * @param record The record that was the user approved for. */ - void onRecordUpdateApproved(Record record); + void onRecordUpdateApproved(Record record, String randomId); /** * This method is called when a record update operation is successful but user was declined. diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java b/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java index 5a456954..7a5315a0 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java @@ -92,20 +92,18 @@ public void onRecordCreationFailure(int statusCode) throws RequestProcessingExce } /** - * Called when a record status was updated to "Approved". - * This uses {@link RPADatasetCacher} to cache the dataset. + * Called when a record update operation has been approved. * - * @param record the record that was updated + * @param record the record that was updated and approved + * @param randomId the ID generated after caching the dataset related to the record + * @throws InvalidRequestException if there is an error in the request + * @throws RequestProcessingException if there is an error while processing the request */ @Override - public void onRecordUpdateApproved(Record record) throws InvalidRequestException, RequestProcessingException { - LOGGER.info("User was approved by SME. Starting caching..."); - String datasetId = record.getUserInfo().getSubject(); - // NEW: case dataset using the RPADatasetCacher - String randomId = this.rpaDatasetCacher.cache(datasetId); - if (randomId == null) - throw new RequestProcessingException("Caching process return a null randomId"); + public void onRecordUpdateApproved(Record record, String randomId) throws InvalidRequestException, RequestProcessingException { + LOGGER.info("Dataset was cached successfully. Sending email to user..."); + // Build Download URL String downloadUrl; try { From 02dbd568bece3f7ac42d2493f5803cd26c856db1 Mon Sep 17 00:00:00 2001 From: elmiomar Date: Wed, 29 Nov 2023 11:56:48 -0500 Subject: [PATCH 04/10] update caching logic --- .../service/rpa/DefaultRPADatasetCacher.java | 13 ++++ .../service/rpa/HttpRPADatasetCacher.java | 5 ++ ...URLConnectionRPARequestHandlerService.java | 69 +++++++++++++++---- .../distrib/service/rpa/RPADatasetCacher.java | 2 + .../rpa/RecordResponseHandlerImpl.java | 2 +- 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/DefaultRPADatasetCacher.java b/src/main/java/gov/nist/oar/distrib/service/rpa/DefaultRPADatasetCacher.java index 3ac34daf..0e915b71 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/DefaultRPADatasetCacher.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/DefaultRPADatasetCacher.java @@ -37,6 +37,19 @@ public String cache(String datasetId) throws RequestProcessingException { return randomId; } + @Override + public boolean uncache(String randomId) { + boolean uncached = false; + try { + uncached = rpaCachingService.uncacheById(randomId); + } catch (Exception e) { + this.logCachingException(e); + throw new RequestProcessingException(e.getMessage()); + } + + return uncached; + } + /** * Logs the specified exception to the debug log, along with its stack trace. * diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpRPADatasetCacher.java b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpRPADatasetCacher.java index 9bf5f76f..19204c88 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpRPADatasetCacher.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpRPADatasetCacher.java @@ -50,6 +50,11 @@ public String cache(String datasetId) { return sendHttpRequest(datasetId, url); } + @Override + public boolean uncache(String randomId) { + return false; + } + /** * Builds the URL for the given dataset ID and using the given {@link RPAConfiguration} object. * diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java index 0de195e1..6a391b9e 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java @@ -377,28 +377,52 @@ private String prepareRequestPayload(UserInfoWrapper userInfoWrapper) throws Jso /** - * Updates the status of a record with a given ID. + * Updates the status of a record in the database. + *

+ * This method handles the approval or decline of a record. When a record is approved, it caches the dataset, + * generates a random ID, and appends this ID to the status before updating the record in the database. + * When a record is declined, the dataset is not cached, a null random ID is used, and the record status is + * updated in the database without appending the random ID. + *

+ *

+ * In cases where a record was initially approved (and thus cached with a random ID) but is later declined, + * this method retrieves the random ID from the status, uncaches the dataset using this ID, and updates + * the record status without the random ID. + *

* * @param recordId The ID of the record to update. - * @param status The status to update the record with. - * @return The {@link RecordStatus} object representing the updated record status. + * @param status The new status to be set for the record. Can be 'Approved' or 'Declined'. + * @param smeId The SME ID associated with the record update. + * @return A {@link RecordStatus} object representing the updated record status. * @throws RecordNotFoundException If the record with the given ID is not found. - * @throws InvalidRequestException If the request is invalid. - * @throws RequestProcessingException If there is an error processing the request. + * @throws InvalidRequestException If the provided status is invalid or the request is otherwise invalid. + * @throws RequestProcessingException If there is an error in processing the update request, such as issues + * with caching or communication errors with the database. */ @Override public RecordStatus updateRecord(String recordId, String status, String smeId) throws RecordNotFoundException, InvalidRequestException, RequestProcessingException { // Initialize return object RecordStatus recordStatus; - - // Cache here before updating the status in SF - LOGGER.info("Starting caching..."); Record record = this.getRecord(recordId).getRecord(); String datasetId = record.getUserInfo().getSubject(); - String randomId = this.rpaDatasetCacher.cache(datasetId); - if (randomId == null) - throw new RequestProcessingException("Caching process returned a null randomId"); + String randomId = null; + + // If the record is being approved + if (RECORD_APPROVED_STATUS.equalsIgnoreCase(status)) { + LOGGER.info("Starting caching..."); + randomId = this.rpaDatasetCacher.cache(datasetId); + if (randomId == null) { + throw new RequestProcessingException("Caching process returned a null randomId"); + } + } + // If the record is being declined, check if it needs uncaching + else if (RECORD_DECLINED_STATUS.equalsIgnoreCase(status)) { + randomId = extractRandomIdFromCurrentStatus(record.getUserInfo().getApprovalStatus()); + if (randomId != null) { + this.rpaDatasetCacher.uncache(randomId); + } + } // Create a valid approval status based on input String approvalStatus = generateApprovalStatus(status, smeId, randomId); @@ -468,6 +492,20 @@ public RecordStatus updateRecord(String recordId, String status, String smeId) t return recordStatus; } + private String extractRandomIdFromCurrentStatus(String currentStatus) { + if (currentStatus != null && currentStatus.startsWith("Approved_")) { + String[] parts = currentStatus.split("_"); + + // Since the expected format is "[status]_[yyyy-MM-dd'T'HH:mm:ss.SSSZ]_[smeId]_[randomId]", + // the randomId should be the 4th part, if all parts are present + if (parts.length == 4) { + return parts[3]; + } + } + return null; + } + + /** * Generates an approval status string based on the given status, current date/time, and random ID. * The date is in ISO 8601 format. If the status is "Declined", the randomId will not be appended. @@ -483,12 +521,16 @@ public RecordStatus updateRecord(String recordId, String status, String smeId) t * @throws InvalidRequestException if the provided status is not "Approved" or "Declined" */ private String generateApprovalStatus(String status, String smeId, String randomId) throws InvalidRequestException { - String formattedDate = Instant.now().toString(); // ISO 8601 format: 2023-05-09T15:59:03.872Z + String formattedDate = Instant.now().toString(); String approvalStatus; + if (status != null) { switch (status.toLowerCase()) { case RECORD_APPROVED_STATUS: - approvalStatus = "Approved_" + formattedDate + "_" + smeId + "_" + randomId; + approvalStatus = "Approved_" + formattedDate + "_" + smeId; + if (randomId != null) { + approvalStatus += "_" + randomId; + } break; case RECORD_DECLINED_STATUS: approvalStatus = "Declined_" + formattedDate + "_" + smeId; @@ -502,4 +544,5 @@ private String generateApprovalStatus(String status, String smeId, String random return approvalStatus; } + } diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/RPADatasetCacher.java b/src/main/java/gov/nist/oar/distrib/service/rpa/RPADatasetCacher.java index 2a2150c3..93395cc8 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/RPADatasetCacher.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/RPADatasetCacher.java @@ -14,4 +14,6 @@ public interface RPADatasetCacher { * @throws RequestProcessingException */ String cache(String datasetId) throws RequestProcessingException; + + boolean uncache(String randomId); } diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java b/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java index 7a5315a0..ca7f4150 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java @@ -125,7 +125,7 @@ public void onRecordUpdateApproved(Record record, String randomId) throws Invali * @param record the record that was updated */ @Override - public void onRecordUpdateDeclined(Record record) { + public void onRecordUpdateDeclined(Record record) throws InvalidRequestException, RequestProcessingException { LOGGER.debug("User was declined by SME"); } From 830675c36539eedb75eb5bf44c65ece0673c1150 Mon Sep 17 00:00:00 2001 From: elmiomar Date: Tue, 5 Dec 2023 06:57:16 -0500 Subject: [PATCH 05/10] include aipid in Download URLs --- .../cachemgr/pdr/PDRDatasetRestorer.java | 2 +- .../distrib/service/RPACachingService.java | 43 ++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/cachemgr/pdr/PDRDatasetRestorer.java b/src/main/java/gov/nist/oar/distrib/cachemgr/pdr/PDRDatasetRestorer.java index 12b77b2d..f4da33b1 100644 --- a/src/main/java/gov/nist/oar/distrib/cachemgr/pdr/PDRDatasetRestorer.java +++ b/src/main/java/gov/nist/oar/distrib/cachemgr/pdr/PDRDatasetRestorer.java @@ -645,7 +645,7 @@ public String idForObject(String aipid, String filepath, String forVersion, Stri String id; id = aipid + "/" + filepath; if (target != null && !target.isEmpty()) - id = target + "/" + filepath; + id = target + "/" + aipid + "/" + filepath; if (forVersion != null && forVersion.length() > 0) id += "#" + forVersion; return id; diff --git a/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java b/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java index 51e04da8..b3abf742 100644 --- a/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java +++ b/src/main/java/gov/nist/oar/distrib/service/RPACachingService.java @@ -129,6 +129,7 @@ public Map retrieveMetadata(String randomID) throws CacheManagem /** * Formats the metadata from a cache object to a JSON object with an additional field for the download URL. + * The download URL includes the random temporary ID, aipid, and the file path from the metadata. * * @param inMd the metadata from the cache object * @param randomID the random temporary ID associated with the cache object @@ -139,17 +140,27 @@ private JSONObject formatMetadata(JSONObject inMd, String randomID) throws Reque JSONObject outMd = new JSONObject(); List missingFields = new ArrayList<>(); + String aipid = ""; + if (inMd.has("aipid")) { + aipid = inMd.getString("aipid"); + outMd.put("aipid", aipid); + } else { + missingFields.add("aipid"); + } + if (inMd.has("filepath")) { String downloadURL = getDownloadUrl( rpaConfiguration.getBaseDownloadUrl(), randomID, - inMd.get("filepath").toString()); + aipid, + inMd.getString("filepath")); outMd.put("downloadURL", downloadURL); - outMd.put("filePath", inMd.get("filepath")); + outMd.put("filePath", inMd.getString("filepath")); } else { missingFields.add("filepath"); } + if (inMd.has("contentType")) { outMd.put("mediaType", inMd.get("contentType")); } else { @@ -199,12 +210,6 @@ private JSONObject formatMetadata(JSONObject inMd, String randomID) throws Reque missingFields.add("ediid"); } - if (inMd.has("aipid")) { - outMd.put("aipid", inMd.get("aipid")); - } else { - missingFields.add("aipid"); - } - if (inMd.has("sinceDate")) { outMd.put("sinceDate", inMd.get("sinceDate")); } else { @@ -220,28 +225,44 @@ private JSONObject formatMetadata(JSONObject inMd, String randomID) throws Reque /** - * Constructs a download URL using the given base download URL, random ID, and file path from the metadata. + * Constructs a download URL using the given base download URL, random ID, aipid, and file path from the metadata. * * @param baseDownloadUrl the base download URL * @param randomId the random temporary ID + * @param aipid the aipid from the metadata * @param path the file path from the metadata * @return the download URL as a string * @throws RequestProcessingException if there was an error building the download URL */ - private String getDownloadUrl(String baseDownloadUrl, String randomId, String path) throws RequestProcessingException { + + private String getDownloadUrl(String baseDownloadUrl, String randomId, String aipid, String path) throws RequestProcessingException { URL downloadUrl; try { URL url = new URL(baseDownloadUrl); + StringBuilder pathBuilder = new StringBuilder(); + + // append the randomId to the path + pathBuilder.append(randomId); + + // append the aipid if it's not empty + if (!aipid.isEmpty()) { + pathBuilder.append("/").append(aipid); + } + + // append the file path, ensuring it doesn't start with a "/" if (path.startsWith("/")) { path = path.substring(1); } - downloadUrl = new URL(url, randomId + "/" + path); + pathBuilder.append("/").append(path); + + downloadUrl = new URL(url, pathBuilder.toString()); } catch (MalformedURLException e) { throw new RequestProcessingException("Failed to build downloadUrl: " + e.getMessage()); } return downloadUrl.toString(); } + /** * Generate a random alphanumeric string for the dataset to store * This function uses the {@link RandomStringUtils} from Apache Commons. From 9bbd5847f8ca5298b3a299f2e30c0f16302202c4 Mon Sep 17 00:00:00 2001 From: elmiomar Date: Tue, 5 Dec 2023 07:41:36 -0500 Subject: [PATCH 06/10] add certificates --- docker/build-test/Dockerfile | 24 +++++++---- .../cacerts/Forward_Proxy_NIST_CA.crt | 41 +++++++++++++++++++ docker/build-test/cacerts/NISTRoot02.crt | 29 +++++++++++++ docker/build-test/cacerts/README.md | 13 ++++++ docker/cacerts/README.md | 13 ++++++ 5 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 docker/build-test/cacerts/Forward_Proxy_NIST_CA.crt create mode 100644 docker/build-test/cacerts/NISTRoot02.crt create mode 100644 docker/build-test/cacerts/README.md create mode 100644 docker/cacerts/README.md diff --git a/docker/build-test/Dockerfile b/docker/build-test/Dockerfile index 431e9d1c..92068bad 100644 --- a/docker/build-test/Dockerfile +++ b/docker/build-test/Dockerfile @@ -1,14 +1,20 @@ -FROM ibmjava:8-sdk - -# a hack that gets around an installation problem with update-alternatives, openjdk-8-jdk-headless +FROM eclipse-temurin:8-jdk-focal RUN mkdir -p /usr/share/man/man1 - -RUN echo "deb http://archive.debian.org/debian stretch main contrib non-free" > /etc/apt/sources.list - -RUN apt-get update && apt-get upgrade -y && apt-get install -y netcat-openbsd zip git less \ - python2 curl maven -RUN cd /usr/bin && ln -s python2 python +RUN apt-get update && apt-get install -y netcat-openbsd zip git less \ + ca-certificates python3 curl maven gnupg +RUN cd /usr/bin && ln -s python3 python + +COPY cacerts/README.md cacerts/*.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates +RUN java_certs=$JAVA_HOME/jre/lib/security/cacerts; \ + add_certs=`ls /usr/local/share/ca-certificates/*.crt` && \ + for crt in $add_certs; do \ + name=`basename -s .crt $crt`; \ + echo -n ${name}: " "; \ + keytool -import -keystore $java_certs -trustcacerts -file $crt \ + -storepass changeit -alias $name -noprompt; \ + done; # Create the user that build/test operations should run as. Normally, # this is set to match identity information of the host user that is diff --git a/docker/build-test/cacerts/Forward_Proxy_NIST_CA.crt b/docker/build-test/cacerts/Forward_Proxy_NIST_CA.crt new file mode 100644 index 00000000..6c7a12cb --- /dev/null +++ b/docker/build-test/cacerts/Forward_Proxy_NIST_CA.crt @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIG7TCCBNWgAwIBAgITGAAAAAecWWKCXTfeJAAAAAAABzANBgkqhkiG9w0BAQsF +ADAVMRMwEQYDVQQDEwpOSVNUUm9vdDAyMB4XDTIxMTIwMTE4MDU0NloXDTI2MTIw +MTE4MTU0NlowgcIxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEVMBMG +A1UEBxMMR2FpdGhlcnNidXJnMTcwNQYDVQQKEy5OYXRpb25hbCBJbnN0aXR1dGUg +b2YgU3RhbmRhcmRzIGFuZCBUZWNobm9sb2d5MQ0wCwYDVQQLEwRPSVNNMR4wHAYD +VQQDDBVGb3J3YXJkX1Byb3h5X05JU1RfQ0ExITAfBgkqhkiG9w0BCQEWEm5ldHNl +Y3VyZUBuaXN0LmdvdjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/8 +PgucU6LfbThmVCiQU5zH7HRdJ0QeM8xa9Hy3BnBdD4/CxQklo7dz+AXquaOfI5Br +H8SYZCySWTveFeJW+XvhjmEVpobz8GGrEgdR5nAKg3ZJHvAMPKgGMSnXja227TVj +qqCZX9cIWQifqcM1iWTkS4BW2oZazwXYCqs5dfwy92ey5f/7AYC4dFeL//QtqQs/ +EUFApYabhKLcDLleDh4hwlhbTO9Zjt/eRujB/5f183RVb+igoy/xVZ8S82cNpxHS +2DdO58GZzvAgYMYuXXJkdINkag/fpCXEy9bGaDfydHLpTWviiGz3HfXh/Chb66BG +ZoZJmJrovVO9rSMyptMCAwEAAaOCAoYwggKCMB0GA1UdDgQWBBQquDqJ3U24XoOQ +/6y8kFgbAp9fPDAfBgNVHSMEGDAWgBQlEQPjYg4e56GOSdev1HJtWx0z+TCB/QYD +VR0fBIH1MIHyMIHvoIHsoIHphjFodHRwOi8vbmlzdHBraS5uaXN0Lmdvdi9DZXJ0 +RW5yb2xsL05JU1RSb290MDIuY3JshoGzbGRhcDovLy9DTj1OSVNUUm9vdDAyLENO +PU5JU1Ryb290Q0EwMixDTj1DRFAsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMs +Q049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1OSVNULERDPUdPVj9jZXJ0 +aWZpY2F0ZVJldm9jYXRpb25MaXN0P2Jhc2U/b2JqZWN0Q2xhc3M9Y1JMRGlzdHJp +YnV0aW9uUG9pbnQwggEFBggrBgEFBQcBAQSB+DCB9TBKBggrBgEFBQcwAoY+aHR0 +cDovL25pc3Rwa2kubmlzdC5nb3YvQ2VydEVucm9sbC9OSVNUcm9vdENBMDJfTklT +VFJvb3QwMi5jcnQwgaYGCCsGAQUFBzAChoGZbGRhcDovLy9DTj1OSVNUUm9vdDAy +LENOPUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxD +Tj1Db25maWd1cmF0aW9uLERDPU5JU1QsREM9R09WP2NBQ2VydGlmaWNhdGU/YmFz +ZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MBkGCSsGAQQBgjcU +AgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgGGMA0G +CSqGSIb3DQEBCwUAA4ICAQB3OCkcbjepVN7tbK3PlLzG5HkRBG1QSmFsRnQdUTov +/rWhdLDpHGKO4k/W2zTxNNxPW8ooD1PCy+cIlBLGcq8YcyhvWk0V2Gx1P+/f4+eq +eH4hcUQO/7INohcnh4QXiSVMa7jNaLC+/usqWbsmTvVDbl2aYbQtwizXnUW1qNhz +Bt76OoM7C95rNktNiaJ1VmFmd+Z3rRhzAZiC9XFIwIN1F+um7IG43nsoM4hnCByc +/SBb3LC8R+7vNUYedkrfNPq8SGCHuPuK8H0gJX+8/8hmaaNPtZoe0VZkTdNXitnY +HNof6w5mDoPu9lgLmNO0c36dNrmhHlPAu71EkL3afBhrdgb4Gel0WlENaur2MWf+ +yg6IQz7+aCTu2bMIkW3gm942tp7IrkXMGshUsJjLHFVrpIVkP+70QnO0wGzzQWlI +gt+/gKvj951KGagVzsFyiQtFL9uFYMiS0awLVkSLYtBzdykm8mpG1n6EO5DlEYWe +MOhVSeki05s0+6zUWU6TIhVDgCeUJYvAYAtWVA07Tbb1lb1vP+KbWzFMuAQMrKXV +I0sL/gjcwaj18n8vb0NdVU2n4qoW44gBi8ocgbuBntt63J4GHpaIn/I4OHBiwu/2 +IwUrfePEVCI2pAm/sBfw2XiofAclxBhhJniiRoMYPKCOdnPRP1nUWOdotzPJFPJe +1Q== +-----END CERTIFICATE----- + diff --git a/docker/build-test/cacerts/NISTRoot02.crt b/docker/build-test/cacerts/NISTRoot02.crt new file mode 100644 index 00000000..9aeb53c3 --- /dev/null +++ b/docker/build-test/cacerts/NISTRoot02.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBTCCAu2gAwIBAgIQdRxyg4+47KRFWKY+545EJjANBgkqhkiG9w0BAQsFADAV +MRMwEQYDVQQDEwpOSVNUUm9vdDAyMB4XDTE4MDgwMTE4MTgzOVoXDTM4MDgwMTE4 +Mjc1M1owFTETMBEGA1UEAxMKTklTVFJvb3QwMjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJxQaJgDFbHCPwx8YOrjfthNQP7TOra9C4SkeURpetVq3fk1 +AqGgcqYzN3SRxtx9xJUweFBayO83jyBx5d+LLqX9LctaIrS4gU3uLGqDEQJisMST ++r6/mF51H5xF9AaiH8ca6ZopjigYdcv0ivMiUh8UWDvZF8SnPq4BaId4D3UwfVhV +p8Nh9osU04BXGSOIaN5dL4CdNiOleC7IqAl4wXekOMkNfIErp2QeLnq/g1xIFmCv +Dz+4umnPIVAYvuIKa39irNLi7j9XqUpnNcfBAvaypOe9e31RqWEYbHKhYXtFMJ6v +Ui/d+pPPJ0HfoMu2toCZHgMCxzaFnGh0reMkcCrPpH2EQIQzbJaV4QVRFvAfNIF5 +cwvb6mRJ9pqjlIVAoT+//YUy1IsG+4n0TZAEJa9G61G3bGr7Chh+uWYGfmpevY8I +GUTNmhYc5pGma6TFR3Hqil9PwAnPcXYQDnjhwVOGRrC/Ze9LymT7tUIEX0JKmZ0J +ds50u8T0joWwacwK1RYdj0YC4PLeLFB2obqcfust4KCN/Hw7/pvwN3sFhbC1dn2G +YIjqiDaenI7Gsb2t5Q8AOQbMSCJu0RYI9XN8Uzm+v0zseLF4V0+43PSTxDnlBzms +cpjRsMRk563nVnL4oHa+LhJnB/YTBqE86bzieTiIL7SqGW1hH+RJWn55pFtnAgMB +AAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQl +EQPjYg4e56GOSdev1HJtWx0z+TAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0B +AQsFAAOCAgEAHscEbpIIPKe+avqPPxUJxRnnlV9CoBZSN4IJcA3Iox3f7zJdeLra +hMJq8vJBLK0barh9ofLbviX1tBzAqDFd6RnMaMWTfv2BgjtoZNqfFqRp632ErDTI +ONyHbGOnuWGXatwRNXUIhhx2UGeAy38xrIU8Z0ssTCsRY374WSFYaR5Ww7hfunyi +eBmofMY+j6flNxEqckV3BeIarJxWmpEaAihczZxJsnZXW+D0B7h4EKZ/DakOl2QA +59aE740ToPAl+pAF4OhT53xPlju+tqkaLnVJg/kI7Qrc0S2mHGrXnDl1FUya8VFS +Vm8bf3nd483e3nWnSVU+vItlRIrtoHnLQ7xzMkurUNo2pROR+JgsL5WL0+NDGFjv +Ixf9ReYGN9ujrHtojZiFaDLMPUftV6EVk2qc2d8BMEAnVzy8WJk6iqiWsmYaE2uq +wdQHiP8kwQhXXRbqhfFZWwSisga4TIZu65rR88ah08DOGaTLfqKUnb9WD4dzTDFH +XBl6ryuOeJGBoeJVbjy5938ZKHSS/nP3H/zYwve7xBw8CkmKAA1ECLJ47iWFmlyr +mQkr8lkaupRMxgV8LUml35hI4lT2SbvAbdsuP/RvuvrK+mHS2UEDjG/qz4aTuXrm +uMnUuya/1QGPhFD1oztxrhem2ob2jfkRfWT6wbv8UK7Mniw2zBfISXY= +-----END CERTIFICATE----- diff --git a/docker/build-test/cacerts/README.md b/docker/build-test/cacerts/README.md new file mode 100644 index 00000000..b650e217 --- /dev/null +++ b/docker/build-test/cacerts/README.md @@ -0,0 +1,13 @@ +This directory contains non-standard CA certificates needed to build the docker +images. + +Failures building the Docker containers defined in ../ due to SSL certificate +verification errors may be a consequence of your local network's firewall. In +particular, the firewall may be substituting external site certificates with +its own signed by a non-standard CA certficate (chain). If so, you can place +the necessary certificates into this directory; they will be passed into the +containers, allowing them to safely connect to those external sites. + +Be sure the certificates are in PEM format and include a .crt file extension. + +Do not remove this README file; doing so may cause a Docker build faiure. \ No newline at end of file diff --git a/docker/cacerts/README.md b/docker/cacerts/README.md new file mode 100644 index 00000000..b650e217 --- /dev/null +++ b/docker/cacerts/README.md @@ -0,0 +1,13 @@ +This directory contains non-standard CA certificates needed to build the docker +images. + +Failures building the Docker containers defined in ../ due to SSL certificate +verification errors may be a consequence of your local network's firewall. In +particular, the firewall may be substituting external site certificates with +its own signed by a non-standard CA certficate (chain). If so, you can place +the necessary certificates into this directory; they will be passed into the +containers, allowing them to safely connect to those external sites. + +Be sure the certificates are in PEM format and include a .crt file extension. + +Do not remove this README file; doing so may cause a Docker build faiure. \ No newline at end of file From 364cb8877ef47a4d1e44a8b1090b1b73e6668abb Mon Sep 17 00:00:00 2001 From: elmiomar Date: Wed, 6 Mar 2024 10:57:22 -0500 Subject: [PATCH 07/10] update unit tests --- ...URLConnectionRPARequestHandlerService.java | 4 + ...onnectionRPARequestHandlerServiceTest.java | 149 +++++++++++++++++- 2 files changed, 148 insertions(+), 5 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java index 6a391b9e..c75628d9 100644 --- a/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java +++ b/src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java @@ -140,6 +140,10 @@ public void setRecordResponseHandler(RecordResponseHandler recordResponseHandler this.recordResponseHandler = recordResponseHandler; } + public void seRPADatasetCacher(RPADatasetCacher rpaDatasetCacher) { + this.rpaDatasetCacher = rpaDatasetCacher; + } + /** * Constructs a new instance of the service using the given RPA configuration. * diff --git a/src/test/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerServiceTest.java b/src/test/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerServiceTest.java index 3d15e46e..369a84b3 100644 --- a/src/test/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerServiceTest.java +++ b/src/test/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerServiceTest.java @@ -45,9 +45,13 @@ import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -116,6 +120,9 @@ public class HttpURLConnectionRPARequestHandlerServiceTest { @Mock RPACachingService rpaCachingService; + @Mock + RPADatasetCacher rpaDatasetCacher; + private HttpURLConnectionRPARequestHandlerService service; JWTToken testToken = null; Map map = new HashMap() {{ @@ -132,6 +139,7 @@ public void setUp() { service.setRecaptchaHelper(recaptchaHelper); service.setHttpURLConnectionFactory(url -> mockConnection); service.setRecordResponseHandler(recordResponseHandler); + service.seRPADatasetCacher(rpaDatasetCacher); service.setHttpClient(mockHttpClient); // Set up mock behavior for mockJwtHelper @@ -491,21 +499,45 @@ private String getUpdateUrl(String recordId) { return url; } + /** + * Tests successful record update operation when approving a record. + * + *

+ * This test simulates the scenario where a record is approved, ensuring that the + * caching process is invoked, a PATCH request is made to update the record status in + * Salesforce with a newly generated random ID, and the approval status is updated correctly. + * The test verifies that the final approval status matches the expected format. + *

+     * Status_YYYY-MM-DDTHH:MM:SS.SSSZ_email_randomID
+     * 
+ * Where: + *
    + *
  • Status - Indicates the status of the record (e.g., "Approved" or "Declined").
  • + *
  • YYYY-MM-DDTHH:MM:SS.SSSZ - The timestamp of the approval in ISO 8601 format. 'T' separates the date and time, and 'Z' denotes UTC time zone.
  • + *
  • email - The email address associated with the user who approved the record.
  • + *
  • randomID - A unique identifier generated for the approval process or the record itself.
  • + *
+ *

+ * + * @throws Exception if any error occurs during the test execution. + */ @Test public void testUpdateRecord_success() throws Exception { String recordId = "record12345"; String email = "test@example.com"; - String expectedApprovalStatus = "Approved_2023-05-09T15:59:03.872Z_" + email; + String mockRandomId = "mockRandomId123"; + String expectedApprovalStatus = "Approved_2023-05-09T15:59:03.872Z_" + email + "_" + mockRandomId; // Mock behavior of getRecord method doReturn(getTestRecordWrapper(expectedApprovalStatus)).when(service).getRecord("record12345"); + when(rpaDatasetCacher.cache(anyString())).thenReturn(mockRandomId); // Mock HttpResponse CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); when(httpResponse.getStatusLine()).thenReturn(mock(StatusLine.class)); when(httpResponse.getStatusLine().getStatusCode()).thenReturn(200); when(httpResponse.getEntity()).thenReturn( - new StringEntity("{\"approvalStatus\":\"Approved_2023-05-09T15:59:03.872Z_test@example.com\"," + + new StringEntity("{\"approvalStatus\":\"Approved_2023-05-09T15:59:03.872Z_test@example.com_mockRandomId123\"," + "\"recordId\":\"record12345\"}", ContentType.APPLICATION_JSON) ); @@ -531,13 +563,24 @@ public void testUpdateRecord_success() throws Exception { // We can't test the exact time as it changes when we run the test, but we can verify the format String patchPayload = EntityUtils.toString(patchRequest.getEntity(), StandardCharsets.UTF_8); JSONObject payloadObject = new JSONObject(patchPayload); - // Pattern to match ISO 8601 format - // This pattern matches a string in the format "Approved_YYYY-MM-DDTHH:MM:SS.SSSZ" - String expectedFormat = "Approved_\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z_[\\w.-]+@[\\w.-]+\\.\\w+"; + // The following regex pattern expects: + // - The "Approved" status followed by a date-time in ISO 8601 format. + // - An email address. + // - A random ID (composed of word characters including underscore, alphanumeric, and possibly -) at the end. + String expectedFormat = "Approved_\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z_[\\w.-]+@[\\w.-]+\\.\\w+_\\w+"; assertTrue(payloadObject.get("Approval_Status__c").toString().matches(expectedFormat)); } + /** + * Tests the updateRecord method's behavior when an unknown status is provided. + *

+ * This test ensures that the method throws an InvalidRequestException when attempting to + * update a record with an unrecognized status. The expected behavior is to validate the + * status input and throw an exception with a specific error message if the status does not + * match expected values (e.g., "Approved" or "Declined"). + *

+ */ @Test public void testUpdateRecord_withUnknownStatus() { String recordId = "record12345"; @@ -545,6 +588,9 @@ public void testUpdateRecord_withUnknownStatus() { String email = "test@example.com"; String expectedErrorMessage = "Invalid approval status: HelloWorld"; + // Mock behavior of getRecord method + doReturn(getTestRecordWrapper("Pending")).when(service).getRecord("record12345"); + // Call the method and catch the exception try { // Act @@ -556,5 +602,98 @@ public void testUpdateRecord_withUnknownStatus() { } } + /** + * Tests the record decline operation for a record that has not been previously approved. + * + *

+ * This test case ensures that when a record is declined without prior approval, the record's + * status is updated accordingly without triggering caching or uncaching operations. It verifies + * the successful update of the record's approval status to "Declined" and confirms that no + * caching or uncaching methods are called, as expected for records not previously approved. + *

+ * + * @throws Exception if any error occurs during the test execution. + */ + @Test + public void testDeclineRecordWithoutPriorApproval_success() throws Exception { + String recordId = "record12345"; + String email = "sme@test.com"; + String status = "Declined"; + + // Mock behavior of getRecord method to simulate a record that has not been approved before + doReturn(getTestRecordWrapper("Pending")).when(service).getRecord(recordId); + + // Mock HttpResponse for the decline operation + CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + when(httpResponse.getStatusLine()).thenReturn(mock(StatusLine.class)); + when(httpResponse.getStatusLine().getStatusCode()).thenReturn(200); + when(httpResponse.getEntity()).thenReturn( + new StringEntity("{\"approvalStatus\":\"Declined\",\"recordId\":\"" + recordId + "\"}", + ContentType.APPLICATION_JSON)); + + // Mock the HttpPatch execution + doReturn(httpResponse).when(mockHttpClient).execute(any(HttpPatch.class)); + + // Act + RecordStatus result = service.updateRecord(recordId, status, email); + + // Assert + assertEquals("Declined", result.getApprovalStatus()); + assertEquals(recordId, result.getRecordId()); + + // Verify that caching and uncaching were not invoked + verify(rpaDatasetCacher, never()).cache(anyString()); + verify(rpaDatasetCacher, never()).uncache(anyString()); + } + + /** + * Tests the decline operation for a record that was previously approved. + * + *

+ * This test checks the behavior of the updateRecord method when declining a record that + * has a prior approval status, including a random ID. It simulates retrieving a previously + * approved record, uncaching the dataset associated with the random ID, and updating the + * record's approval status to "Declined". The test verifies that the uncaching operation + * is executed with the correct random ID and that the record's status is correctly updated. + *

+ * + * @throws Exception if any error occurs during the test execution. + */ + @Test + public void testDeclinePreviouslyApprovedRecord_success() throws Exception { + String recordId = "record12345"; + String email = "sme@test.com"; + String status = "Declined"; + String mockRandomId = "mockRandomId123"; + String initialApprovalStatus = "Approved_2023-05-09T15:59:03.872Z_" + email + "_" + mockRandomId; + + // Mock behavior of getRecord method to simulate retrieving a previously approved record + doReturn(getTestRecordWrapper(initialApprovalStatus)).when(service).getRecord(recordId); + + // Simulate `uncache` returning true + when(rpaDatasetCacher.uncache(anyString())).thenReturn(true); + + // Mock HttpResponse for updating the record to "Declined" + CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + when(httpResponse.getStatusLine()).thenReturn(mock(StatusLine.class)); + when(httpResponse.getStatusLine().getStatusCode()).thenReturn(200); + when(httpResponse.getEntity()).thenReturn( + new StringEntity("{\"approvalStatus\":\"Declined\",\"recordId\":\"" + recordId + "\"}", + ContentType.APPLICATION_JSON)); + + // Mock the HttpPatch execution + doReturn(httpResponse).when(mockHttpClient).execute(any(HttpPatch.class)); + + // Act + RecordStatus result = service.updateRecord(recordId, status, email); + + // Assert + assertEquals("Declined", result.getApprovalStatus()); + assertEquals(recordId, result.getRecordId()); + + // Verify uncaching was invoked with the correct random ID + verify(rpaDatasetCacher).uncache(mockRandomId); + } + } From 03cdf70a0d56bc4d10f93ca08150576936c412a7 Mon Sep 17 00:00:00 2001 From: Omar Ilias EL MIMOUNI Date: Wed, 6 Mar 2024 11:06:41 -0500 Subject: [PATCH 08/10] Update Dockerfile - resolve merge conflicts --- docker/build-test/Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker/build-test/Dockerfile b/docker/build-test/Dockerfile index c6ac3cc1..fba91c27 100644 --- a/docker/build-test/Dockerfile +++ b/docker/build-test/Dockerfile @@ -1,4 +1,3 @@ -<<<<<<< HEAD FROM eclipse-temurin:8-jdk-focal RUN mkdir -p /usr/share/man/man1 @@ -16,14 +15,12 @@ RUN java_certs=$JAVA_HOME/jre/lib/security/cacerts; \ keytool -import -keystore $java_certs -trustcacerts -file $crt \ -storepass changeit -alias $name -noprompt; \ done; -======= FROM eclipse-temurin:8 RUN mkdir -p /usr/share/man/man1 RUN apt-get update && apt-get install -y netcat-openbsd zip git less \ python2 curl maven RUN cd /usr/bin && ln -s python2 python ->>>>>>> origin/integration # Create the user that build/test operations should run as. Normally, # this is set to match identity information of the host user that is From 980b5fdbe56a48f2f677a3387ea8a802ec97ff78 Mon Sep 17 00:00:00 2001 From: elmiomar Date: Wed, 6 Mar 2024 11:25:55 -0500 Subject: [PATCH 09/10] fix unit tests --- .../service/RPACachingServiceTest.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/test/java/gov/nist/oar/distrib/service/RPACachingServiceTest.java b/src/test/java/gov/nist/oar/distrib/service/RPACachingServiceTest.java index c3d0f108..52829497 100644 --- a/src/test/java/gov/nist/oar/distrib/service/RPACachingServiceTest.java +++ b/src/test/java/gov/nist/oar/distrib/service/RPACachingServiceTest.java @@ -52,8 +52,8 @@ public void testCacheAndGenerateRandomId_validDatasetID() throws Exception { String result = rpaCachingService.cacheAndGenerateRandomId(datasetID, version); assertNotNull(result); - assertEquals(RPACachingService.RANDOM_ID_LENGTH, result.length()); - assertTrue(result.matches("^[a-zA-Z0-9]*$")); // check that the ID is alphanumeric + assertEquals(RPACachingService.RANDOM_ID_LENGTH + 4, result.length()); // 4 for the 'rpa-' prefix + assertTrue(result.matches("^rpa-[a-zA-Z0-9]+$")); // Check that the ID starts with 'rpa-' followed by alphanumeric chars verify(pdrCacheManager).cacheDataset(eq("mds2-2909"), eq(version), eq(true), eq(RPACachingService.ROLE_RESTRICTED_DATA), eq(result)); } @@ -69,8 +69,8 @@ public void testCacheAndGenerateRandomId_validDatasetArkID() throws Exception { String result = rpaCachingService.cacheAndGenerateRandomId(datasetID, version); assertNotNull(result); - assertEquals(RPACachingService.RANDOM_ID_LENGTH, result.length()); - assertTrue(result.matches("^[a-zA-Z0-9]*$")); // check that the ID is alphanumeric + assertEquals(RPACachingService.RANDOM_ID_LENGTH + 4, result.length()); // 4 for the 'rpa-' prefix + assertTrue(result.matches("^rpa-[a-zA-Z0-9]+$")); // Check that the ID starts with 'rpa-' followed by alphanumeric chars verify(pdrCacheManager).cacheDataset(eq("mds2-2909"), eq(version), eq(true), eq(RPACachingService.ROLE_RESTRICTED_DATA), eq(result)); } @@ -85,6 +85,7 @@ public void testCacheAndGenerateRandomId_invalidDatasetArkID() throws Exception @Test public void testRetrieveMetadata_success() throws Exception { String randomID = "randomId123"; + String aipid = "456"; CacheObject cacheObject1 = new CacheObject("object1", new JSONObject() .put("filepath", "path/to/file1.txt") .put("contentType", "text/plain") @@ -95,7 +96,7 @@ public void testRetrieveMetadata_success() throws Exception { .put("checksum", "abc123") .put("version", "v1") .put("ediid", "123") - .put("aipid", "456") + .put("aipid", aipid) .put("sinceDate", "08-05-2023"), "Volume1"); @@ -109,7 +110,7 @@ public void testRetrieveMetadata_success() throws Exception { .put("checksum", "def456") .put("version", "v2") .put("ediid", "123") - .put("aipid", "456") + .put("aipid", aipid) .put("sinceDate", "08-05-2023"), "Volume1"); @@ -124,7 +125,8 @@ public void testRetrieveMetadata_success() throws Exception { Map expected = new HashMap<>(); expected.put("randomId", randomID); expected.put("metadata", new JSONArray() - .put(new JSONObject().put("downloadURL", testBaseDownloadUrl + "/" + randomID + "/path/to/file1.txt") + .put(new JSONObject().put("downloadURL", testBaseDownloadUrl + "/" + randomID + + "/" + aipid + "/path/to/file1.txt") .put("filePath", "path/to/file1.txt") .put("mediaType", "text/plain") .put("size", 100L) @@ -136,7 +138,8 @@ public void testRetrieveMetadata_success() throws Exception { .put("ediid", "123") .put("aipid", "456") .put("sinceDate", "08-05-2023")) - .put(new JSONObject().put("downloadURL", testBaseDownloadUrl + "/" + randomID + "/path/to/file2.txt") + .put(new JSONObject().put("downloadURL", testBaseDownloadUrl + "/" + randomID + + "/" + aipid + "/path/to/file2.txt") .put("filePath", "path/to/file2.txt") .put("mediaType", "text/plain") .put("size", 100L) @@ -159,6 +162,7 @@ public void testRetrieveMetadata_success() throws Exception { @Test public void testRetrieveMetadata_withMissingFilepath() throws Exception { String randomID = "randomId123"; + String aipid = "456"; CacheObject cacheObject1 = new CacheObject("object1", new JSONObject() .put("contentType", "text/plain") .put("size", 100L) @@ -168,7 +172,7 @@ public void testRetrieveMetadata_withMissingFilepath() throws Exception { .put("checksum", "abc123") .put("version", "v1") .put("ediid", "123") - .put("aipid", "456") + .put("aipid", aipid) .put("sinceDate", "08-05-2023"), "Volume1"); @@ -182,7 +186,7 @@ public void testRetrieveMetadata_withMissingFilepath() throws Exception { .put("checksum", "def456") .put("version", "v2") .put("ediid", "123") - .put("aipid", "456") + .put("aipid", aipid) .put("sinceDate", "08-05-2023"), "Volume1"); @@ -197,7 +201,8 @@ public void testRetrieveMetadata_withMissingFilepath() throws Exception { Map expected = new HashMap<>(); expected.put("randomId", randomID); expected.put("metadata", new JSONArray() - .put(new JSONObject().put("downloadURL", testBaseDownloadUrl + "/" + randomID + "/path/to/file2.txt") + .put(new JSONObject().put("downloadURL", testBaseDownloadUrl + "/" + randomID + + "/" + aipid +"/path/to/file2.txt") .put("filePath", "path/to/file2.txt") .put("mediaType", "text/plain") .put("size", 100L) @@ -207,7 +212,7 @@ public void testRetrieveMetadata_withMissingFilepath() throws Exception { .put("checksum", "def456") .put("version", "v2") .put("ediid", "123") - .put("aipid", "456") + .put("aipid", aipid) .put("sinceDate", "08-05-2023")) .toList()); From 8cc659f97642ff792d8b5835a80f40d5d205b65e Mon Sep 17 00:00:00 2001 From: elmiomar Date: Fri, 8 Mar 2024 15:44:34 -0500 Subject: [PATCH 10/10] remove redundant uncaching endpoint --- .../distrib/web/RPADataCachingController.java | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java b/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java index 4d2520be..e9418f2b 100644 --- a/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java +++ b/src/main/java/gov/nist/oar/distrib/web/RPADataCachingController.java @@ -149,34 +149,6 @@ public Map retrieveMetadata(@PathVariable("cacheid") String cach return metadata; } - /** - * This endpoint handles the removal of a dataset from the cache based on its randomly generated ID. - * - * @param randomId The randomly generated ID that was assigned when the dataset was cached. - * - * @return A ResponseEntity containing a status message and HTTP status code. - * - * @throws CacheManagementException If there is an issue with removing the dataset from the cache. - * @throws ResourceNotFoundException If the dataset associated with the randomId is not found in the cache. - * @throws StorageVolumeException If there is an issue with the storage volume. - */ - @PutMapping(value = "/uncache/{randomId}") - public ResponseEntity uncacheDataset( - @PathVariable("randomId") String randomId) - throws CacheManagementException { - - logger.debug("randomId to uncache=" + randomId); - - // Perform uncache operation via the service - boolean success = restrictedSrvc.uncacheById(randomId); - - if (success) { - return new ResponseEntity<>("Dataset was successfully removed from cache.", HttpStatus.OK); - } else { - return new ResponseEntity<>("Failed to uncache the dataset.", HttpStatus.INTERNAL_SERVER_ERROR); - } - } - @ExceptionHandler(MetadataNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND)