From eebc6141ffaeca9c9d8123e2753bbec11d651025 Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Thu, 14 Dec 2023 17:02:35 -0500 Subject: [PATCH 01/10] BXC-4358 start single use links service --- .../processing/SingleUseKeyService.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyService.java 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 new file mode 100644 index 0000000000..bfc6dfa94c --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyService.java @@ -0,0 +1,100 @@ +package edu.unc.lib.boxc.web.services.processing; + +import edu.unc.lib.boxc.model.api.exceptions.RepositoryException; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Generate and invalidate access keys for single use links + * @author snluong + */ +public class SingleUseKeyService { + private static final String ID = "UUID"; + private static final String ACCESS_KEY = "Access Key"; + private static final String TIMESTAMP = "Timestamp"; + private static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; + private Path csvPath; + public String generate(String id) { + var lock = new ReentrantLock(); + var key = getKey(); + lock.lock(); + try (var csvPrinter = createCsvPrinter(csvPath)) { + csvPrinter.printRecord(id, key, System.currentTimeMillis()); + } catch (Exception e) { + throw new RepositoryException("Failed to write new key to Single Use Key CSV", e); + } finally { + lock.unlock(); + } + return key; + } + public boolean keyIsValid(String key) throws IOException { + var csvRecords = parseCsv(csvPath); + for (CSVRecord record : csvRecords) { + if (key.equals(record.get(ACCESS_KEY))) { + return true; + } + } + return false; + } + public void invalidate(String key) { + var lock = new ReentrantLock(); + lock.lock(); + try { + List csvRecords = parseCsv(csvPath); + var updatedRecords = new ArrayList<>(); + for (CSVRecord record : csvRecords) { + if (!key.equals(record.get(ACCESS_KEY))) { + updatedRecords.add(record); + } + } +// try (var csvPrinter = createCsvPrinter(csvPath)) { +// csvPrinter.flush(); +// csvPrinter.printRecords(updatedRecords); +// } catch (Exception e) { +// throw new IOException("Failed rewrite of Single Use Key CSV"); +// } +// try (FileWriter writer = new FileWriter(csvPath.toFile())) { +// +// } + + } catch (IOException e) { + throw new RepositoryException("Failed to invalidate key in Single Use Key CSV", e); + } finally { + lock.unlock(); + } + + } + private CSVPrinter createCsvPrinter(Path csvPath) throws IOException { + var writer = Files.newBufferedWriter(csvPath); + return new CSVPrinter(writer, CSVFormat.DEFAULT + .withHeader(CSV_HEADERS)); + } + + private List parseCsv(Path csvPath) throws IOException { + Reader reader = Files.newBufferedReader(csvPath); + return new CSVParser(reader, CSVFormat.DEFAULT + .withFirstRecordAsHeader() + .withHeader(CSV_HEADERS) + .withTrim()) + .getRecords(); + } + private String getKey() { + return UUID.randomUUID().toString().replace("-", "") + Long.toHexString(System.nanoTime()); + } + + public void setCsvPath(Path csvPath) { + this.csvPath = csvPath; + } +} From ead17a3c96c1c820a468008119f42b401eb2f43d Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Fri, 15 Dec 2023 16:58:14 -0500 Subject: [PATCH 02/10] BXC-4358 start on test for single use links --- .../processing/SingleUseKeyService.java | 4 +- .../processing/SingleUseKeyServiceTest.java | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyServiceTest.java 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 bfc6dfa94c..b96dbd73c7 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 @@ -22,7 +22,7 @@ */ public class SingleUseKeyService { private static final String ID = "UUID"; - private static final String ACCESS_KEY = "Access Key"; + public static final String ACCESS_KEY = "Access Key"; private static final String TIMESTAMP = "Timestamp"; private static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; private Path csvPath; @@ -90,7 +90,7 @@ private List parseCsv(Path csvPath) throws IOException { .withTrim()) .getRecords(); } - private String getKey() { + 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 new file mode 100644 index 0000000000..3ce6e46a00 --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/SingleUseKeyServiceTest.java @@ -0,0 +1,50 @@ +package edu.unc.lib.boxc.web.services.processing; + +import org.apache.commons.csv.CSVRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author snluong + */ +public class SingleUseKeyServiceTest { + @TempDir + public Path tmpFolder; + private String csvPath; + + private static final String UUID_1 = "f277bb38-272c-471c-a28a-9887a1328a1f"; + private static final String UUID_2 = "ba70a1ee-fa7c-437f-a979-cc8b16599652"; + private static final String UUID_3 = "83c2d7f8-2e6b-4f0b-ab7e-7397969c0682"; + + @BeforeEach + public void setup() throws IOException { + var csvDirectory = tmpFolder.resolve("singleUseKey"); + csvPath = File.createTempFile("accessKeys", ".csv", csvDirectory.toFile()).toString(); + } + + @Test + public void testInvalidate() { + + } + + private void assertDoesNotContainEntry(List csvRecords, String key) { + for (CSVRecord record : csvRecords) { + if (key.equals(record.get(SingleUseKeyService.ACCESS_KEY))) { + fail("Entry found for uuid " + key); + return; + } + } + } + + private void generateCsv(String key) { + } +} From c29aeeea3f528d91f5f1e19518ee3acfe06c2847 Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Tue, 19 Dec 2023 16:35:19 -0500 Subject: [PATCH 03/10] BXC-4358 update service and make tests pass --- .../processing/SingleUseKeyService.java | 22 +++--- .../processing/SingleUseKeyServiceTest.java | 68 ++++++++++++++++--- 2 files changed, 69 insertions(+), 21 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 b96dbd73c7..cebc66b754 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 @@ -21,10 +21,10 @@ * @author snluong */ public class SingleUseKeyService { - private static final String ID = "UUID"; + public static final String ID = "UUID"; public static final String ACCESS_KEY = "Access Key"; private static final String TIMESTAMP = "Timestamp"; - private static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; + public static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; private Path csvPath; public String generate(String id) { var lock = new ReentrantLock(); @@ -59,16 +59,12 @@ public void invalidate(String key) { updatedRecords.add(record); } } -// try (var csvPrinter = createCsvPrinter(csvPath)) { -// csvPrinter.flush(); -// csvPrinter.printRecords(updatedRecords); -// } catch (Exception e) { -// throw new IOException("Failed rewrite of Single Use Key CSV"); -// } -// try (FileWriter writer = new FileWriter(csvPath.toFile())) { -// -// } - + try (var csvPrinter = createCsvPrinter(csvPath)) { + csvPrinter.flush(); + csvPrinter.printRecords(updatedRecords); + } catch (Exception e) { + throw new IOException("Failed rewrite of Single Use Key CSV"); + } } catch (IOException e) { throw new RepositoryException("Failed to invalidate key in Single Use Key CSV", e); } finally { @@ -82,7 +78,7 @@ private CSVPrinter createCsvPrinter(Path csvPath) throws IOException { .withHeader(CSV_HEADERS)); } - private List parseCsv(Path csvPath) throws IOException { + public List parseCsv(Path csvPath) throws IOException { Reader reader = Files.newBufferedReader(csvPath); return new CSVParser(reader, CSVFormat.DEFAULT .withFirstRecordAsHeader() 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 3ce6e46a00..ded7b086fa 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 @@ -1,5 +1,7 @@ package edu.unc.lib.boxc.web.services.processing; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,7 +12,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Objects; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** @@ -19,32 +24,79 @@ public class SingleUseKeyServiceTest { @TempDir public Path tmpFolder; - private String csvPath; + private Path csvPath; private static final String UUID_1 = "f277bb38-272c-471c-a28a-9887a1328a1f"; private static final String UUID_2 = "ba70a1ee-fa7c-437f-a979-cc8b16599652"; private static final String UUID_3 = "83c2d7f8-2e6b-4f0b-ab7e-7397969c0682"; + private static final String UUID_TEST = "0e33ad0b-7a16-4bfa-b833-6126c262d889"; + private String[] ids = {UUID_1, UUID_2, UUID_3}; + private SingleUseKeyService singleUseKeyService; @BeforeEach public void setup() throws IOException { - var csvDirectory = tmpFolder.resolve("singleUseKey"); - csvPath = File.createTempFile("accessKeys", ".csv", csvDirectory.toFile()).toString(); + csvPath = tmpFolder.resolve("singleUseKey"); + singleUseKeyService = new SingleUseKeyService(); + singleUseKeyService.setCsvPath(csvPath); } @Test - public void testInvalidate() { + public void testGenerate() throws IOException { + generateDefaultCsv(null); + var oldRecords = singleUseKeyService.parseCsv(csvPath); + assertDoesNotContainValue(oldRecords, SingleUseKeyService.ID, UUID_TEST); + var key = singleUseKeyService.generate(UUID_TEST); + var newRecords = singleUseKeyService.parseCsv(csvPath); + assertContainsAccessKeyPair(newRecords, UUID_TEST, key); } - private void assertDoesNotContainEntry(List csvRecords, String key) { + @Test + public void testKeyIsValid() throws IOException { + var key = SingleUseKeyService.getKey(); + generateDefaultCsv(key); + assertTrue(singleUseKeyService.keyIsValid(key)); + } + + @Test + public void testInvalidate() throws IOException { + var key = SingleUseKeyService.getKey(); + generateDefaultCsv(key); + singleUseKeyService.invalidate(key); + + var records = singleUseKeyService.parseCsv(csvPath); + assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key); + } + + private void assertDoesNotContainValue(List csvRecords, String column, String value) { for (CSVRecord record : csvRecords) { - if (key.equals(record.get(SingleUseKeyService.ACCESS_KEY))) { - fail("Entry found for uuid " + key); + if (value.equals(record.get(column))) { + fail("Entry found!"); return; } } } - private void generateCsv(String key) { + private void assertContainsAccessKeyPair(List csvRecords, String id, String key) { + for (CSVRecord record : csvRecords) { + if (id.equals(record.get(SingleUseKeyService.ID))) { + assertEquals(key, record.get(SingleUseKeyService.ACCESS_KEY)); + return; + } + } + fail("No access key pair found"); + } + + private void generateDefaultCsv(String key) throws IOException { + try (var writer = Files.newBufferedWriter(csvPath); + var csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT + .withHeader(SingleUseKeyService.CSV_HEADERS))) { + for (String id : ids) { + csvPrinter.printRecord(id, SingleUseKeyService.getKey(), System.currentTimeMillis()); + } + if (key != null) { + csvPrinter.printRecord(UUID_TEST, key, System.currentTimeMillis()); + } + } } } From fbaf9e5f2bdc5462439cef637525743b40d4d2d1 Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Tue, 19 Dec 2023 16:49:32 -0500 Subject: [PATCH 04/10] BXC-4358 add javadocs and clean up imports --- .../processing/SingleUseKeyService.java | 19 ++++++++++++++++++- .../processing/SingleUseKeyServiceTest.java | 2 -- 2 files changed, 18 insertions(+), 3 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 cebc66b754..7a40dc696c 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 @@ -6,7 +6,6 @@ import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; -import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.nio.file.Files; @@ -26,6 +25,12 @@ public class SingleUseKeyService { private static final String TIMESTAMP = "Timestamp"; public static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; private Path csvPath; + + /** + * Generates an access key for a particular ID, adds it to the CSV, and returns the key + * @param id UUID of the record + * @return generated access key + */ public String generate(String id) { var lock = new ReentrantLock(); var key = getKey(); @@ -39,6 +44,13 @@ public String generate(String id) { } return key; } + + /** + * Determines if a key is valid by seeing if it is in the CSV + * @param key access key for single use link + * @return true if key is in the CSV, otherwise false + * @throws IOException + */ public boolean keyIsValid(String key) throws IOException { var csvRecords = parseCsv(csvPath); for (CSVRecord record : csvRecords) { @@ -48,6 +60,11 @@ public boolean keyIsValid(String key) throws IOException { } return false; } + + /** + * Invalidates a key by removing its entry from the CSV + * @param key access key of the record + */ public void invalidate(String key) { var lock = new ReentrantLock(); lock.lock(); 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 ded7b086fa..37d3fbe6c8 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 @@ -7,12 +7,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; From c9edd82f028c5b029069990b9c593830d389bd03 Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Wed, 20 Dec 2023 15:25:02 -0500 Subject: [PATCH 05/10] BXC-4358 refactor and DRYing up --- .../processing/SingleUseKeyService.java | 42 ++++++++++--------- .../lib/boxc/web/services/utils/CsvUtil.java | 33 +++++++++++++++ .../MemberOrderCsvExporterTest.java | 18 +++----- .../processing/SingleUseKeyServiceTest.java | 30 +++++++++---- 4 files changed, 83 insertions(+), 40 deletions(-) create mode 100644 web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java 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 7a40dc696c..7b2be1d113 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 @@ -15,6 +15,9 @@ import java.util.UUID; import java.util.concurrent.locks.ReentrantLock; +import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter; +import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; + /** * Generate and invalidate access keys for single use links * @author snluong @@ -35,7 +38,7 @@ public String generate(String id) { var lock = new ReentrantLock(); var key = getKey(); lock.lock(); - try (var csvPrinter = createCsvPrinter(csvPath)) { + try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { csvPrinter.printRecord(id, key, System.currentTimeMillis()); } catch (Exception e) { throw new RepositoryException("Failed to write new key to Single Use Key CSV", e); @@ -46,15 +49,16 @@ public String generate(String id) { } /** - * Determines if a key is valid by seeing if it is in the CSV + * Determines if a key is valid by seeing if it is in the CSV, connected to the proper ID + * @param id uuid of the box-c record * @param key access key for single use link * @return true if key is in the CSV, otherwise false * @throws IOException */ - public boolean keyIsValid(String key) throws IOException { - var csvRecords = parseCsv(csvPath); + public boolean keyIsValid(String id, String key) throws IOException { + var csvRecords = parseCsv(CSV_HEADERS, csvPath); for (CSVRecord record : csvRecords) { - if (key.equals(record.get(ACCESS_KEY))) { + if (accessKeyMatchesUuid(record, id, key)) { return true; } } @@ -63,20 +67,20 @@ public boolean keyIsValid(String key) throws IOException { /** * Invalidates a key by removing its entry from the CSV - * @param key access key of the record + * @param key access key of the box-c record */ public void invalidate(String key) { var lock = new ReentrantLock(); lock.lock(); try { - List csvRecords = parseCsv(csvPath); + var csvRecords = parseCsv(CSV_HEADERS, csvPath); var updatedRecords = new ArrayList<>(); for (CSVRecord record : csvRecords) { if (!key.equals(record.get(ACCESS_KEY))) { updatedRecords.add(record); } } - try (var csvPrinter = createCsvPrinter(csvPath)) { + try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { csvPrinter.flush(); csvPrinter.printRecords(updatedRecords); } catch (Exception e) { @@ -89,20 +93,18 @@ public void invalidate(String key) { } } - private CSVPrinter createCsvPrinter(Path csvPath) throws IOException { - var writer = Files.newBufferedWriter(csvPath); - return new CSVPrinter(writer, CSVFormat.DEFAULT - .withHeader(CSV_HEADERS)); - } - public List parseCsv(Path csvPath) throws IOException { - Reader reader = Files.newBufferedReader(csvPath); - return new CSVParser(reader, CSVFormat.DEFAULT - .withFirstRecordAsHeader() - .withHeader(CSV_HEADERS) - .withTrim()) - .getRecords(); + /** + * Check that the access key is connected to the uuid in question + * @param record CSV entry + * @param uuid uuid of the box-c record + * @param key access key + * @return true if they are in the same CSV line + */ + private boolean accessKeyMatchesUuid(CSVRecord record,String uuid, String key) { + return uuid.equals(record.get(ID)) && key.equals(record.get(ACCESS_KEY)); } + public static String getKey() { return UUID.randomUUID().toString().replace("-", "") + Long.toHexString(System.nanoTime()); } diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java new file mode 100644 index 0000000000..e73c744eab --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java @@ -0,0 +1,33 @@ +package edu.unc.lib.boxc.web.services.utils; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; + +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Util for CSV-related operations + * @author snluong + */ +public class CsvUtil { + public static List parseCsv(String[] headers, Path csvPath) throws IOException { + Reader reader = Files.newBufferedReader(csvPath); + return new CSVParser(reader, CSVFormat.DEFAULT + .withFirstRecordAsHeader() + .withHeader(headers) + .withTrim()) + .getRecords(); + } + + public static CSVPrinter createCsvPrinter(String[] headers, Path csvPath) throws IOException { + var writer = Files.newBufferedWriter(csvPath); + return new CSVPrinter(writer, CSVFormat.DEFAULT + .withHeader(headers)); + } +} diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java index 50edfac2ea..ee6f055259 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java @@ -34,6 +34,7 @@ import java.util.stream.Collectors; import static edu.unc.lib.boxc.search.api.FacetConstants.MARKED_FOR_DELETION; +import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Matchers.any; @@ -88,7 +89,7 @@ public void exportUnorderedObjectTest() throws Exception { mockChildrenResults(rec1, rec2); var resultPath = csvService.export(asPidList(PARENT1_UUID), agent); - var csvRecords = parseCsv(resultPath); + var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath); assertNumberOfEntries(2, csvRecords); assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One", "file1.txt", "text/plain", false, null); @@ -107,7 +108,7 @@ public void exportPartiallyOrderedObjectTest() throws Exception { mockChildrenResults(rec1, rec2); var resultPath = csvService.export(asPidList(PARENT1_UUID), agent); - var csvRecords = parseCsv(resultPath); + var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath); assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One", "file1.txt", "text/plain", false, 0); assertContainsEntry(csvRecords, CHILD2_UUID, PARENT1_UUID, "File Two", @@ -127,7 +128,7 @@ public void exportOrderedObjectWithDeletedChildTest() throws Exception { mockChildrenResults(rec1, rec2); var resultPath = csvService.export(asPidList(PARENT1_UUID), agent); - var csvRecords = parseCsv(resultPath); + var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath); assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One", "file1.txt", "text/plain", false, 0); assertContainsEntry(csvRecords, CHILD2_UUID, PARENT1_UUID, "File Two", @@ -151,7 +152,7 @@ public void exportMultipleOrderedObjectsTest() throws Exception { .thenReturn(makeResultResponse(rec3)); var resultPath = csvService.export(asPidList(PARENT1_UUID, PARENT2_UUID), agent); - var csvRecords = parseCsv(resultPath); + var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath); assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One", "file1.txt", "text/plain", false, 0); assertContainsEntry(csvRecords, CHILD2_UUID, PARENT1_UUID, "File Two", @@ -253,15 +254,6 @@ private List asPidList(String... ids) { return Arrays.stream(ids).map(PIDs::get).collect(Collectors.toList()); } - private List parseCsv(Path csvPath) throws IOException { - Reader reader = Files.newBufferedReader(csvPath); - return new CSVParser(reader, CSVFormat.DEFAULT - .withFirstRecordAsHeader() - .withHeader(MemberOrderCsvConstants.CSV_HEADERS) - .withTrim()) - .getRecords(); - } - private ContentObjectRecord makeWorkRecord(String uuid, String title) { return makeRecord(uuid, COLLECTION_UUID, ResourceType.Work, title, null, null, null); } 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 37d3fbe6c8..ec993efd81 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 @@ -12,7 +12,11 @@ import java.nio.file.Path; import java.util.List; +import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.CSV_HEADERS; +import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter; +import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -41,11 +45,11 @@ public void setup() throws IOException { @Test public void testGenerate() throws IOException { generateDefaultCsv(null); - var oldRecords = singleUseKeyService.parseCsv(csvPath); + var oldRecords = parseCsv(CSV_HEADERS, csvPath); assertDoesNotContainValue(oldRecords, SingleUseKeyService.ID, UUID_TEST); var key = singleUseKeyService.generate(UUID_TEST); - var newRecords = singleUseKeyService.parseCsv(csvPath); + var newRecords = parseCsv(CSV_HEADERS, csvPath); assertContainsAccessKeyPair(newRecords, UUID_TEST, key); } @@ -53,7 +57,21 @@ public void testGenerate() throws IOException { public void testKeyIsValid() throws IOException { var key = SingleUseKeyService.getKey(); generateDefaultCsv(key); - assertTrue(singleUseKeyService.keyIsValid(key)); + assertTrue(singleUseKeyService.keyIsValid(UUID_TEST, key)); + } + + @Test + public void testKeyIsNotValid() throws IOException { + var key = SingleUseKeyService.getKey(); + generateDefaultCsv(null); + assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key)); + } + + @Test + public void testKeyIsNotValidWrongUUID() throws IOException { + var key = SingleUseKeyService.getKey(); + generateDefaultCsv(key); + assertFalse(singleUseKeyService.keyIsValid("8e0040b2-9951-48a3-9d65-780ae7106951", key)); } @Test @@ -62,7 +80,7 @@ public void testInvalidate() throws IOException { generateDefaultCsv(key); singleUseKeyService.invalidate(key); - var records = singleUseKeyService.parseCsv(csvPath); + var records = parseCsv(CSV_HEADERS, csvPath); assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key); } @@ -86,9 +104,7 @@ private void assertContainsAccessKeyPair(List csvRecords, String id, } private void generateDefaultCsv(String key) throws IOException { - try (var writer = Files.newBufferedWriter(csvPath); - var csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT - .withHeader(SingleUseKeyService.CSV_HEADERS))) { + try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { for (String id : ids) { csvPrinter.printRecord(id, SingleUseKeyService.getKey(), System.currentTimeMillis()); } From 36f2f5c14a3d7b86948f8e45df0d1652a4720364 Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Wed, 20 Dec 2023 16:57:43 -0500 Subject: [PATCH 06/10] BXC-4358 add time logic and clean up imports --- .../processing/SingleUseKeyService.java | 13 +++++------ .../MemberOrderCsvExporterTest.java | 5 ----- .../processing/SingleUseKeyServiceTest.java | 22 ++++++++++++++----- 3 files changed, 21 insertions(+), 19 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 7b2be1d113..0726406dbb 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 @@ -1,17 +1,11 @@ package edu.unc.lib.boxc.web.services.processing; import edu.unc.lib.boxc.model.api.exceptions.RepositoryException; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import java.io.IOException; -import java.io.Reader; -import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.List; import java.util.UUID; import java.util.concurrent.locks.ReentrantLock; @@ -27,6 +21,7 @@ public class SingleUseKeyService { public static final String ACCESS_KEY = "Access Key"; private static final String TIMESTAMP = "Timestamp"; public static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; + public static final long DAY_MILLISECONDS = 86400000; private Path csvPath; /** @@ -55,11 +50,13 @@ public String generate(String id) { * @return true if key is in the CSV, otherwise false * @throws IOException */ - public boolean keyIsValid(String id, String key) throws IOException { + public boolean keyIsValid(String id, String key, long currentMilliseconds) throws IOException { var csvRecords = parseCsv(CSV_HEADERS, csvPath); for (CSVRecord record : csvRecords) { if (accessKeyMatchesUuid(record, id, key)) { - return true; + var startTime = Long.parseLong(record.get(TIMESTAMP)); + long endTime = startTime + DAY_MILLISECONDS; + return currentMilliseconds >= startTime && currentMilliseconds <= endTime; } } return false; diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java index ee6f055259..4743eb4bfa 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/MemberOrderCsvExporterTest.java @@ -16,8 +16,6 @@ import edu.unc.lib.boxc.search.solr.models.DatastreamImpl; import edu.unc.lib.boxc.search.solr.responses.SearchResultResponse; import edu.unc.lib.boxc.search.solr.services.SolrSearchService; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -26,9 +24,6 @@ import org.mockito.Mock; import java.io.IOException; -import java.io.Reader; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; 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 ec993efd81..4b140b7387 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 @@ -1,18 +1,16 @@ package edu.unc.lib.boxc.web.services.processing; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.CSV_HEADERS; +import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.DAY_MILLISECONDS; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -57,21 +55,33 @@ public void testGenerate() throws IOException { public void testKeyIsValid() throws IOException { var key = SingleUseKeyService.getKey(); generateDefaultCsv(key); - assertTrue(singleUseKeyService.keyIsValid(UUID_TEST, key)); + var currentMilliseconds = System.currentTimeMillis(); + assertTrue(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); } @Test public void testKeyIsNotValid() throws IOException { var key = SingleUseKeyService.getKey(); generateDefaultCsv(null); - assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key)); + var currentMilliseconds = System.currentTimeMillis(); + assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); } @Test public void testKeyIsNotValidWrongUUID() throws IOException { var key = SingleUseKeyService.getKey(); generateDefaultCsv(key); - assertFalse(singleUseKeyService.keyIsValid("8e0040b2-9951-48a3-9d65-780ae7106951", key)); + var currentMilliseconds = System.currentTimeMillis(); + assertFalse(singleUseKeyService.keyIsValid( + "8e0040b2-9951-48a3-9d65-780ae7106951", key, currentMilliseconds)); + } + + @Test + public void testKeyIsNotValidCurrentTimeIsMoreThan24hLater() throws IOException { + var key = SingleUseKeyService.getKey(); + generateDefaultCsv(key); + var currentMilliseconds = System.currentTimeMillis() + (2 * DAY_MILLISECONDS); + assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); } @Test From 6203fd8c97bb670e42e15d19401a6d12a5ae6fe8 Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Wed, 20 Dec 2023 17:14:44 -0500 Subject: [PATCH 07/10] BXC-4358 adjusting timestamp in CSV to be expiration timestamp --- .../services/processing/SingleUseKeyService.java | 14 ++++++++------ .../processing/SingleUseKeyServiceTest.java | 11 +++++++---- 2 files changed, 15 insertions(+), 10 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 0726406dbb..a61768bb27 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 @@ -19,7 +19,7 @@ public class SingleUseKeyService { public static final String ID = "UUID"; public static final String ACCESS_KEY = "Access Key"; - private static final String TIMESTAMP = "Timestamp"; + private static final String TIMESTAMP = "Expiration Timestamp"; public static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; public static final long DAY_MILLISECONDS = 86400000; private Path csvPath; @@ -29,12 +29,12 @@ public class SingleUseKeyService { * @param id UUID of the record * @return generated access key */ - public String generate(String id) { + public String generate(String id, long expirationInMilliseconds) { var lock = new ReentrantLock(); var key = getKey(); lock.lock(); try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { - csvPrinter.printRecord(id, key, System.currentTimeMillis()); + csvPrinter.printRecord(id, key, expirationInMilliseconds); } catch (Exception e) { throw new RepositoryException("Failed to write new key to Single Use Key CSV", e); } finally { @@ -54,9 +54,8 @@ public boolean keyIsValid(String id, String key, long currentMilliseconds) throw var csvRecords = parseCsv(CSV_HEADERS, csvPath); for (CSVRecord record : csvRecords) { if (accessKeyMatchesUuid(record, id, key)) { - var startTime = Long.parseLong(record.get(TIMESTAMP)); - long endTime = startTime + DAY_MILLISECONDS; - return currentMilliseconds >= startTime && currentMilliseconds <= endTime; + var expirationTimestamp = Long.parseLong(record.get(TIMESTAMP)); + return currentMilliseconds <= expirationTimestamp; } } return false; @@ -105,6 +104,9 @@ private boolean accessKeyMatchesUuid(CSVRecord record,String uuid, String key) { public static String getKey() { return UUID.randomUUID().toString().replace("-", "") + Long.toHexString(System.nanoTime()); } + public static long getExpirationInMilliseconds() { + return System.currentTimeMillis() + DAY_MILLISECONDS; + } public void setCsvPath(Path csvPath) { this.csvPath = csvPath; 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 4b140b7387..f30166222b 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 @@ -11,6 +11,7 @@ import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.CSV_HEADERS; import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.DAY_MILLISECONDS; +import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.getExpirationInMilliseconds; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -46,7 +47,8 @@ public void testGenerate() throws IOException { var oldRecords = parseCsv(CSV_HEADERS, csvPath); assertDoesNotContainValue(oldRecords, SingleUseKeyService.ID, UUID_TEST); - var key = singleUseKeyService.generate(UUID_TEST); + var expirationTime = getExpirationInMilliseconds(); + var key = singleUseKeyService.generate(UUID_TEST, expirationTime); var newRecords = parseCsv(CSV_HEADERS, csvPath); assertContainsAccessKeyPair(newRecords, UUID_TEST, key); } @@ -80,7 +82,7 @@ public void testKeyIsNotValidWrongUUID() throws IOException { public void testKeyIsNotValidCurrentTimeIsMoreThan24hLater() throws IOException { var key = SingleUseKeyService.getKey(); generateDefaultCsv(key); - var currentMilliseconds = System.currentTimeMillis() + (2 * DAY_MILLISECONDS); + var currentMilliseconds = System.currentTimeMillis() + (3 * DAY_MILLISECONDS); assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); } @@ -114,12 +116,13 @@ private void assertContainsAccessKeyPair(List csvRecords, String id, } private void generateDefaultCsv(String key) throws IOException { + var expiration = getExpirationInMilliseconds(); try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { for (String id : ids) { - csvPrinter.printRecord(id, SingleUseKeyService.getKey(), System.currentTimeMillis()); + csvPrinter.printRecord(id, SingleUseKeyService.getKey(), expiration); } if (key != null) { - csvPrinter.printRecord(UUID_TEST, key, System.currentTimeMillis()); + csvPrinter.printRecord(UUID_TEST, key, expiration); } } } From e18b9d4f04b074f760c47a79424961671e1079ad Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Fri, 5 Jan 2024 15:00:23 -0500 Subject: [PATCH 08/10] BXC-4358 updating logic, adding tests, and correcting some mistakes --- .../processing/SingleUseKeyService.java | 49 ++++------ .../lib/boxc/web/services/utils/CsvUtil.java | 26 +++++ .../processing/SingleUseKeyServiceTest.java | 94 ++++++++++++------- 3 files changed, 108 insertions(+), 61 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 a61768bb27..6305f3e379 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 @@ -10,6 +10,7 @@ import java.util.concurrent.locks.ReentrantLock; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter; +import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createNewCsvPrinter; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; /** @@ -19,19 +20,20 @@ public class SingleUseKeyService { public static final String ID = "UUID"; public static final String ACCESS_KEY = "Access Key"; - private static final String TIMESTAMP = "Expiration Timestamp"; + public static final String TIMESTAMP = "Expiration Timestamp"; public static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP}; public static final long DAY_MILLISECONDS = 86400000; private Path csvPath; + private ReentrantLock lock = new ReentrantLock(); /** * Generates an access key for a particular ID, adds it to the CSV, and returns the key * @param id UUID of the record * @return generated access key */ - public String generate(String id, long expirationInMilliseconds) { - var lock = new ReentrantLock(); + public String generate(String id) { var key = getKey(); + var expirationInMilliseconds = System.currentTimeMillis() + DAY_MILLISECONDS; lock.lock(); try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { csvPrinter.printRecord(id, key, expirationInMilliseconds); @@ -44,16 +46,16 @@ public String generate(String id, long expirationInMilliseconds) { } /** - * Determines if a key is valid by seeing if it is in the CSV, connected to the proper ID - * @param id uuid of the box-c record + * Determines if a key is valid by seeing if it is in the CSV and if the expiration timestamp has not passed * @param key access key for single use link * @return true if key is in the CSV, otherwise false * @throws IOException */ - public boolean keyIsValid(String id, String key, long currentMilliseconds) throws IOException { + public boolean keyIsValid(String key) throws IOException { var csvRecords = parseCsv(CSV_HEADERS, csvPath); + var currentMilliseconds = System.currentTimeMillis(); for (CSVRecord record : csvRecords) { - if (accessKeyMatchesUuid(record, id, key)) { + if (key.equals(record.get(ACCESS_KEY))) { var expirationTimestamp = Long.parseLong(record.get(TIMESTAMP)); return currentMilliseconds <= expirationTimestamp; } @@ -66,47 +68,36 @@ public boolean keyIsValid(String id, String key, long currentMilliseconds) throw * @param key access key of the box-c record */ public void invalidate(String key) { - var lock = new ReentrantLock(); lock.lock(); try { var csvRecords = parseCsv(CSV_HEADERS, csvPath); var updatedRecords = new ArrayList<>(); + var keyExists = false; for (CSVRecord record : csvRecords) { - if (!key.equals(record.get(ACCESS_KEY))) { + if (key.equals(record.get(ACCESS_KEY))) { + keyExists = true; + } else { + // add the rest of the keys to list updatedRecords.add(record); } } - try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { - csvPrinter.flush(); - csvPrinter.printRecords(updatedRecords); - } catch (Exception e) { - throw new IOException("Failed rewrite of Single Use Key CSV"); + + if (keyExists) { + try (var csvPrinter = createNewCsvPrinter(CSV_HEADERS, csvPath)) { + csvPrinter.flush(); + csvPrinter.printRecords(updatedRecords); + } } } catch (IOException e) { throw new RepositoryException("Failed to invalidate key in Single Use Key CSV", e); } finally { lock.unlock(); } - - } - - /** - * Check that the access key is connected to the uuid in question - * @param record CSV entry - * @param uuid uuid of the box-c record - * @param key access key - * @return true if they are in the same CSV line - */ - private boolean accessKeyMatchesUuid(CSVRecord record,String uuid, String key) { - return uuid.equals(record.get(ID)) && key.equals(record.get(ACCESS_KEY)); } public static String getKey() { return UUID.randomUUID().toString().replace("-", "") + Long.toHexString(System.nanoTime()); } - public static long getExpirationInMilliseconds() { - return System.currentTimeMillis() + DAY_MILLISECONDS; - } public void setCsvPath(Path csvPath) { this.csvPath = csvPath; diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java index e73c744eab..289a223a22 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java @@ -9,6 +9,7 @@ import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.List; /** @@ -25,7 +26,32 @@ public static List parseCsv(String[] headers, Path csvPath) throws IO .getRecords(); } + /** + * Creates a CSV printer and determines whether new rows should be appended to existing CSV + * @param headers header values of the CSV + * @param csvPath path of the CSV + * @return + * @throws IOException + */ public static CSVPrinter createCsvPrinter(String[] headers, Path csvPath) throws IOException { + var file = csvPath.toFile(); + if (file.exists()) { + var writer = Files.newBufferedWriter(csvPath, StandardOpenOption.APPEND); + return new CSVPrinter(writer, CSVFormat.DEFAULT + .withHeader(headers)); + } else { + return createNewCsvPrinter(headers, csvPath); + } + } + + /** + * Make a new CSV printer that does not append new rows + * @param headers header values of the CSV + * @param csvPath path of the CSV + * @return + * @throws IOException + */ + public static CSVPrinter createNewCsvPrinter(String[] headers, Path csvPath) throws IOException { var writer = Files.newBufferedWriter(csvPath); return new CSVPrinter(writer, CSVFormat.DEFAULT .withHeader(headers)); 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 f30166222b..598f290bba 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 @@ -11,12 +11,12 @@ import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.CSV_HEADERS; import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.DAY_MILLISECONDS; -import static edu.unc.lib.boxc.web.services.processing.SingleUseKeyService.getExpirationInMilliseconds; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter; import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; /** @@ -31,7 +31,7 @@ public class SingleUseKeyServiceTest { private static final String UUID_2 = "ba70a1ee-fa7c-437f-a979-cc8b16599652"; private static final String UUID_3 = "83c2d7f8-2e6b-4f0b-ab7e-7397969c0682"; private static final String UUID_TEST = "0e33ad0b-7a16-4bfa-b833-6126c262d889"; - private String[] ids = {UUID_1, UUID_2, UUID_3}; + private final String[] ids = {UUID_1, UUID_2, UUID_3}; private SingleUseKeyService singleUseKeyService; @BeforeEach @@ -42,54 +42,80 @@ public void setup() throws IOException { } @Test - public void testGenerate() throws IOException { - generateDefaultCsv(null); - var oldRecords = parseCsv(CSV_HEADERS, csvPath); - assertDoesNotContainValue(oldRecords, SingleUseKeyService.ID, UUID_TEST); + public void testGenerateNoCsvExists() throws IOException { + var key = singleUseKeyService.generate(UUID_TEST); + var newRecords = parseCsv(CSV_HEADERS, csvPath); + var currentMilliseconds = System.currentTimeMillis(); + assertCsvContainsCorrectEntry(newRecords, UUID_TEST, key, currentMilliseconds); + } + + @Test + public void testGenerateMultipleCallsForDifferentIds() throws IOException { + var key1 = singleUseKeyService.generate(UUID_1); + var key2 = singleUseKeyService.generate(UUID_2); + var key3 = singleUseKeyService.generate(UUID_3); - var expirationTime = getExpirationInMilliseconds(); - var key = singleUseKeyService.generate(UUID_TEST, expirationTime); var newRecords = parseCsv(CSV_HEADERS, csvPath); - assertContainsAccessKeyPair(newRecords, UUID_TEST, key); + var currentMilliseconds = System.currentTimeMillis(); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key1, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_2, key2, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_3, key3, currentMilliseconds); } @Test - public void testKeyIsValid() throws IOException { - var key = SingleUseKeyService.getKey(); - generateDefaultCsv(key); + public void testGenerateMultipleCallsForSameId() throws IOException { + var key1 = singleUseKeyService.generate(UUID_1); + var key2 = singleUseKeyService.generate(UUID_1); + var key3 = singleUseKeyService.generate(UUID_1); + + var newRecords = parseCsv(CSV_HEADERS, csvPath); var currentMilliseconds = System.currentTimeMillis(); - assertTrue(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key1, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key2, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key3, currentMilliseconds); + } @Test - public void testKeyIsNotValid() throws IOException { + public void testKeyIsValid() throws IOException { var key = SingleUseKeyService.getKey(); - generateDefaultCsv(null); - var currentMilliseconds = System.currentTimeMillis(); - assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); + var futureExpiration = System.currentTimeMillis() + DAY_MILLISECONDS; + generateDefaultCsv(key, futureExpiration); + assertTrue(singleUseKeyService.keyIsValid(key)); } @Test - public void testKeyIsNotValidWrongUUID() throws IOException { + public void testKeyIsNotValid() throws IOException { var key = SingleUseKeyService.getKey(); - generateDefaultCsv(key); - var currentMilliseconds = System.currentTimeMillis(); - assertFalse(singleUseKeyService.keyIsValid( - "8e0040b2-9951-48a3-9d65-780ae7106951", key, currentMilliseconds)); + var timestamp = System.currentTimeMillis(); + generateDefaultCsv(null, timestamp); + assertFalse(singleUseKeyService.keyIsValid(key)); } @Test public void testKeyIsNotValidCurrentTimeIsMoreThan24hLater() throws IOException { var key = SingleUseKeyService.getKey(); - generateDefaultCsv(key); - var currentMilliseconds = System.currentTimeMillis() + (3 * DAY_MILLISECONDS); - assertFalse(singleUseKeyService.keyIsValid(UUID_TEST, key, currentMilliseconds)); + var pastTimestamp = System.currentTimeMillis() - (2 * DAY_MILLISECONDS); + generateDefaultCsv(key, pastTimestamp); + assertFalse(singleUseKeyService.keyIsValid(key)); } @Test public void testInvalidate() throws IOException { var key = SingleUseKeyService.getKey(); - generateDefaultCsv(key); + var expirationTimestamp = System.currentTimeMillis() + DAY_MILLISECONDS; + generateDefaultCsv(key, expirationTimestamp); + singleUseKeyService.invalidate(key); + + var records = parseCsv(CSV_HEADERS, csvPath); + assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key); + } + + @Test + public void testInvalidateWhenKeyIsNotPresent() throws IOException { + var key = SingleUseKeyService.getKey(); + var expirationTimestamp = System.currentTimeMillis()+ DAY_MILLISECONDS; + generateDefaultCsv(null, expirationTimestamp); singleUseKeyService.invalidate(key); var records = parseCsv(CSV_HEADERS, csvPath); @@ -105,18 +131,22 @@ private void assertDoesNotContainValue(List csvRecords, String column } } - private void assertContainsAccessKeyPair(List csvRecords, String id, String key) { + private void assertCsvContainsCorrectEntry(List csvRecords, + String id, String key, long currentTimestamp) { for (CSVRecord record : csvRecords) { - if (id.equals(record.get(SingleUseKeyService.ID))) { - assertEquals(key, record.get(SingleUseKeyService.ACCESS_KEY)); + if (key.equals(record.get(SingleUseKeyService.ACCESS_KEY))) { + assertEquals(id, record.get(SingleUseKeyService.ID)); + // timestamp must not be null and must be in the future + var expirationTimestamp = record.get(SingleUseKeyService.TIMESTAMP); + assertNotNull(expirationTimestamp); + assertTrue(Long.parseLong(expirationTimestamp) > currentTimestamp); return; } } - fail("No access key pair found"); + fail("Correct entry not found"); } - private void generateDefaultCsv(String key) throws IOException { - var expiration = getExpirationInMilliseconds(); + private void generateDefaultCsv(String key, long expiration) throws IOException { try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { for (String id : ids) { csvPrinter.printRecord(id, SingleUseKeyService.getKey(), expiration); From a2e2e477c416524a407cded9ff7f3be44048b93f Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Fri, 5 Jan 2024 17:13:40 -0500 Subject: [PATCH 09/10] BXC-4358 update tests and fix header problem --- .../processing/SingleUseKeyService.java | 21 +++++---- .../lib/boxc/web/services/utils/CsvUtil.java | 7 ++- .../processing/SingleUseKeyServiceTest.java | 45 ++++++++++++------- 3 files changed, 44 insertions(+), 29 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 6305f3e379..d632101400 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 @@ -33,8 +33,8 @@ public class SingleUseKeyService { */ public String generate(String id) { var key = getKey(); - var expirationInMilliseconds = System.currentTimeMillis() + DAY_MILLISECONDS; lock.lock(); + var expirationInMilliseconds = System.currentTimeMillis() + DAY_MILLISECONDS; try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) { csvPrinter.printRecord(id, key, expirationInMilliseconds); } catch (Exception e) { @@ -49,16 +49,19 @@ public String generate(String id) { * Determines if a key is valid by seeing if it is in the CSV and if the expiration timestamp has not passed * @param key access key for single use link * @return true if key is in the CSV, otherwise false - * @throws IOException */ - public boolean keyIsValid(String key) throws IOException { - var csvRecords = parseCsv(CSV_HEADERS, csvPath); - var currentMilliseconds = System.currentTimeMillis(); - for (CSVRecord record : csvRecords) { - if (key.equals(record.get(ACCESS_KEY))) { - var expirationTimestamp = Long.parseLong(record.get(TIMESTAMP)); - return currentMilliseconds <= expirationTimestamp; + public boolean keyIsValid(String key) { + try { + var csvRecords = parseCsv(CSV_HEADERS, csvPath); + var currentMilliseconds = System.currentTimeMillis(); + for (CSVRecord record : csvRecords) { + if (key.equals(record.get(ACCESS_KEY))) { + var expirationTimestamp = Long.parseLong(record.get(TIMESTAMP)); + return currentMilliseconds <= expirationTimestamp; + } } + } catch (IOException e) { + throw new RepositoryException("Failed to determine if key is valid in Single Use Key CSV", e); } return false; } diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java index 289a223a22..ccbcc474ca 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java @@ -34,11 +34,10 @@ public static List parseCsv(String[] headers, Path csvPath) throws IO * @throws IOException */ public static CSVPrinter createCsvPrinter(String[] headers, Path csvPath) throws IOException { - var file = csvPath.toFile(); - if (file.exists()) { + if (Files.exists(csvPath)) { var writer = Files.newBufferedWriter(csvPath, StandardOpenOption.APPEND); - return new CSVPrinter(writer, CSVFormat.DEFAULT - .withHeader(headers)); + //return new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(headers)); + return new CSVPrinter(writer, CSVFormat.DEFAULT.withSkipHeaderRecord()); } else { return createNewCsvPrinter(headers, csvPath); } 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 598f290bba..5454332fc6 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 @@ -45,8 +45,8 @@ public void setup() throws IOException { public void testGenerateNoCsvExists() throws IOException { var key = singleUseKeyService.generate(UUID_TEST); var newRecords = parseCsv(CSV_HEADERS, csvPath); - var currentMilliseconds = System.currentTimeMillis(); - assertCsvContainsCorrectEntry(newRecords, UUID_TEST, key, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_TEST, key); + assertEquals(1, newRecords.size()); } @Test @@ -56,10 +56,10 @@ public void testGenerateMultipleCallsForDifferentIds() throws IOException { var key3 = singleUseKeyService.generate(UUID_3); var newRecords = parseCsv(CSV_HEADERS, csvPath); - var currentMilliseconds = System.currentTimeMillis(); - assertCsvContainsCorrectEntry(newRecords, UUID_1, key1, currentMilliseconds); - assertCsvContainsCorrectEntry(newRecords, UUID_2, key2, currentMilliseconds); - assertCsvContainsCorrectEntry(newRecords, UUID_3, key3, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key1); + assertCsvContainsCorrectEntry(newRecords, UUID_2, key2); + assertCsvContainsCorrectEntry(newRecords, UUID_3, key3); + assertEquals(3, newRecords.size()); } @Test @@ -69,10 +69,10 @@ public void testGenerateMultipleCallsForSameId() throws IOException { var key3 = singleUseKeyService.generate(UUID_1); var newRecords = parseCsv(CSV_HEADERS, csvPath); - var currentMilliseconds = System.currentTimeMillis(); - assertCsvContainsCorrectEntry(newRecords, UUID_1, key1, currentMilliseconds); - assertCsvContainsCorrectEntry(newRecords, UUID_1, key2, currentMilliseconds); - assertCsvContainsCorrectEntry(newRecords, UUID_1, key3, currentMilliseconds); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key1); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key2); + assertCsvContainsCorrectEntry(newRecords, UUID_1, key3); + assertEquals(3, newRecords.size()); } @@ -109,6 +109,7 @@ public void testInvalidate() throws IOException { var records = parseCsv(CSV_HEADERS, csvPath); assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key); + assertEquals(3, records.size()); } @Test @@ -120,6 +121,22 @@ public void testInvalidateWhenKeyIsNotPresent() throws IOException { var records = parseCsv(CSV_HEADERS, csvPath); assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key); + assertEquals(3, records.size()); + } + + @Test + public void testInvalidateMultipleTimes() throws IOException { + var key = SingleUseKeyService.getKey(); + var expirationTimestamp = System.currentTimeMillis() + DAY_MILLISECONDS; + generateDefaultCsv(key, expirationTimestamp); + var key2 = singleUseKeyService.generate(UUID_TEST); + singleUseKeyService.invalidate(key); + singleUseKeyService.invalidate(key2); + + var records = parseCsv(CSV_HEADERS, csvPath); + assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key); + assertDoesNotContainValue(records, SingleUseKeyService.ACCESS_KEY, key2); + assertEquals(3, records.size()); } private void assertDoesNotContainValue(List csvRecords, String column, String value) { @@ -131,15 +148,11 @@ private void assertDoesNotContainValue(List csvRecords, String column } } - private void assertCsvContainsCorrectEntry(List csvRecords, - String id, String key, long currentTimestamp) { + private void assertCsvContainsCorrectEntry(List csvRecords, String id, String key) { for (CSVRecord record : csvRecords) { if (key.equals(record.get(SingleUseKeyService.ACCESS_KEY))) { assertEquals(id, record.get(SingleUseKeyService.ID)); - // timestamp must not be null and must be in the future - var expirationTimestamp = record.get(SingleUseKeyService.TIMESTAMP); - assertNotNull(expirationTimestamp); - assertTrue(Long.parseLong(expirationTimestamp) > currentTimestamp); + assertNotNull(record.get(SingleUseKeyService.TIMESTAMP)); return; } } From 4f3651111aab8e9084e4f225526503367ca7f5ba Mon Sep 17 00:00:00 2001 From: Sharon Luong Date: Fri, 5 Jan 2024 17:24:58 -0500 Subject: [PATCH 10/10] BXC-4358 add future check back in and remove commented out line --- .../java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java | 1 - .../web/services/processing/SingleUseKeyServiceTest.java | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java index ccbcc474ca..b221d980cf 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/utils/CsvUtil.java @@ -36,7 +36,6 @@ public static List parseCsv(String[] headers, Path csvPath) throws IO public static CSVPrinter createCsvPrinter(String[] headers, Path csvPath) throws IOException { if (Files.exists(csvPath)) { var writer = Files.newBufferedWriter(csvPath, StandardOpenOption.APPEND); - //return new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(headers)); return new CSVPrinter(writer, CSVFormat.DEFAULT.withSkipHeaderRecord()); } else { return createNewCsvPrinter(headers, csvPath); 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 5454332fc6..6a146fec42 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 @@ -152,7 +152,10 @@ private void assertCsvContainsCorrectEntry(List csvRecords, String id for (CSVRecord record : csvRecords) { if (key.equals(record.get(SingleUseKeyService.ACCESS_KEY))) { assertEquals(id, record.get(SingleUseKeyService.ID)); - assertNotNull(record.get(SingleUseKeyService.TIMESTAMP)); + // timestamp must not be null and must be in the future + var expirationTimestamp = record.get(SingleUseKeyService.TIMESTAMP); + assertNotNull(expirationTimestamp); + assertTrue(Long.parseLong(expirationTimestamp) > System.currentTimeMillis()); return; } }