From 62f5274945f2951e37a39425732c3448a01b0a2e Mon Sep 17 00:00:00 2001 From: sharonl <6546457+sharonluong@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:22:25 -0500 Subject: [PATCH] Bxc 4362 invalidate schedule (#1656) * BXC-4362 add time-based invalidation code and tests * BXC-4362 update logic to be clearer and add another time based test --------- Co-authored-by: Sharon Luong --- .../processing/SingleUseKeyService.java | 62 +++++++++++++++---- .../processing/SingleUseKeyServiceTest.java | 22 ++++++- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyService.java index 51ea61dc82..408737f0d2 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyService.java @@ -2,6 +2,11 @@ import edu.unc.lib.boxc.model.api.exceptions.RepositoryException; import org.apache.commons.csv.CSVRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; import java.io.IOException; import java.nio.file.Path; @@ -19,6 +24,8 @@ * Generate and invalidate access keys for single use links * @author snluong */ +@Configuration +@EnableScheduling public class SingleUseKeyService { public static final String ID = "UUID"; public static final String ACCESS_KEY = "Access Key"; @@ -28,6 +35,7 @@ public class SingleUseKeyService { public static final String KEY = "key"; private Path csvPath; private ReentrantLock lock = new ReentrantLock(); + private static final Logger log = LoggerFactory.getLogger(SingleUseKeyService.class); /** * Generates an access key for a particular ID, adds it to the CSV, and returns the key @@ -70,37 +78,67 @@ public boolean keyIsValid(String key) { } /** - * Invalidates a key by removing its entry from the CSV - * @param key access key of the box-c record + * Invalidates a single use key CSV record by removing it from the CSV + * @param key access key of the box-c record, may be null for time-based invalidation */ public void invalidate(String key) { lock.lock(); try { var csvRecords = parseCsv(CSV_HEADERS, csvPath); var updatedRecords = new ArrayList<>(); - var keyExists = false; + var recordsChanged = false; for (CSVRecord record : csvRecords) { - if (key.equals(record.get(ACCESS_KEY))) { - keyExists = true; + if (recordShouldBeInvalidated(key, record)) { + recordsChanged = true; } else { - // add the rest of the keys to list + // keep this record as it is valid updatedRecords.add(record); } } - if (keyExists) { - try (var csvPrinter = createNewCsvPrinter(CSV_HEADERS, csvPath)) { - csvPrinter.flush(); - csvPrinter.printRecords(updatedRecords); - } + if (recordsChanged) { + writeNewCsv(updatedRecords); } } catch (IOException e) { - throw new RepositoryException("Failed to invalidate key in Single Use Key CSV", e); + throw new RepositoryException("Failed to invalidate record in Single Use Key CSV", e); } finally { lock.unlock(); } } + /** + * Determines if the specific record should be invalidated + * @param key the access key, may be null if we want a time-based eval + * @param row the csv row that is the CSV record + * @return true if the record should be invalidated + */ + private boolean recordShouldBeInvalidated(String key, CSVRecord row) { + if (key == null) { + // if record's timestamp is in the past, it should be invalidated + var currentTime = System.currentTimeMillis(); + return Long.parseLong(row.get(TIMESTAMP)) < currentTime; + } else { + // if the record's key matches, it should be invalidated + return key.equals(row.get(ACCESS_KEY)); + } + } + + /** + * We are running a cron job every hour to invalidate any expired single use keys + */ + @Scheduled(cron = "0 0 * * * *") + public void scheduleInvalidation() { + log.info("Invalidate Single Use Key Cron is running!"); + invalidate(null); + } + + private void writeNewCsv(ArrayList records) throws IOException { + try (var csvPrinter = createNewCsvPrinter(CSV_HEADERS, csvPath)) { + csvPrinter.flush(); + csvPrinter.printRecords(records); + } + } + public static String getKey() { return UUID.randomUUID().toString().replace("-", "") + Long.toHexString(System.nanoTime()); } diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyServiceTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyServiceTest.java index 58a78e0e90..46590bd1c2 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyServiceTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyServiceTest.java @@ -101,7 +101,7 @@ public void testKeyIsNotValidCurrentTimeIsMoreThan24hLater() throws IOException } @Test - public void testInvalidate() throws IOException { + public void testInvalidateWithKey() throws IOException { var key = SingleUseKeyService.getKey(); var expirationTimestamp = System.currentTimeMillis() + DAY_MILLISECONDS; generateDefaultCsv(csvPath, key, expirationTimestamp); @@ -124,6 +124,26 @@ public void testInvalidateWhenKeyIsNotPresent() throws IOException { assertEquals(3, records.size()); } + @Test + public void testInvalidateTimeBasedAllExpired() throws IOException { + var expirationTimestamp = System.currentTimeMillis() - (2 * DAY_MILLISECONDS); + generateDefaultCsv(csvPath,null, expirationTimestamp); + singleUseKeyService.invalidate(null); + + var records = parseCsv(CSV_HEADERS, csvPath); + assertEquals(0, records.size()); + } + + @Test + public void testInvalidateTimeBasedNoneExpired() throws IOException { + var expirationTimestamp = System.currentTimeMillis() + (2 * DAY_MILLISECONDS); + generateDefaultCsv(csvPath,null, expirationTimestamp); + singleUseKeyService.invalidate(null); + + var records = parseCsv(CSV_HEADERS, csvPath); + assertEquals(3, records.size()); + } + @Test public void testInvalidateMultipleTimes() throws IOException { var key = SingleUseKeyService.getKey();