From 3fca9422fe933782a43b8f3325d2a3c44379cf62 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Thu, 5 Dec 2024 11:30:33 +0100 Subject: [PATCH 01/83] feat: add GET all-workspaces for admin user --- .../armadillo/controller/DataController.java | 7 +++++++ .../storage/ArmadilloStorageService.java | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java index e08a3e429..ee906b349 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java @@ -406,6 +406,13 @@ public List getWorkspaces(Principal principal) { () -> storage.listWorkspaces(principal), principal, GET_USER_WORKSPACES, Map.of()); } + @Operation(summary = "Get all workspaces") + @GetMapping(value = "/all-workspaces", produces = APPLICATION_JSON_VALUE) + public Map> getAllUserWorkspaces(Principal principal) { + return auditEventPublisher.audit( + storage::listUserWorkspaces, principal, GET_USER_WORKSPACES, Map.of()); + } + @Operation( summary = "Delete user workspace", responses = { diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index f84fcd7d8..44849ee88 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.io.FilenameUtils; import org.molgenis.armadillo.exceptions.*; @@ -202,6 +203,26 @@ public List listWorkspaces(Principal principal) { .toList(); } + @PreAuthorize("hasAnyRole('ROLE_SU')") + public Map> listUserWorkspaces() { + List availableUsers = + storageService.listBuckets().stream() + .filter((user) -> user.startsWith(USER_PREFIX)) + .toList(); + // How to get username from weird id thingy without principal? + // can we store weird number thingy? + // Do we maybe have it saved already? + // Where does it come from? + return availableUsers.stream() + .collect( + Collectors.toMap( + userFolder -> userFolder, + userFolder -> + storageService.listObjects(userFolder).stream() + .map(ArmadilloStorageService::toWorkspace) + .collect(Collectors.toList()))); + } + public InputStream loadWorkspace(Principal principal, String id) { return storageService.load(getUserBucketName(principal), getWorkspaceObjectName(id)); } From bb10001f179c341c94e99e806ac5eccb81f199e7 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:30:23 +0100 Subject: [PATCH 02/83] chore: updated vue version --- ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index 6d74c1f26..c5190ebb3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,7 @@ "@popperjs/core": "2.11.8", "bootstrap": "5.3.3", "bootstrap-icons": "1.11.3", - "vue": "3.3.8", + "vue": "3.5.13", "vue-router": "^4.0.13" }, "devDependencies": { From aad34853c36b6484f002436cf191d4581bee5d0e Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:31:22 +0100 Subject: [PATCH 03/83] feat: added workspace page --- ui/src/views/Workspaces.vue | 190 ++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 ui/src/views/Workspaces.vue diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue new file mode 100644 index 000000000..9a0a0d65e --- /dev/null +++ b/ui/src/views/Workspaces.vue @@ -0,0 +1,190 @@ + + + From ca5a9036edf4069fc1a79066aa0a2cb636f6561a Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:31:46 +0100 Subject: [PATCH 04/83] feat: added workspace tab --- ui/src/App.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/App.vue b/ui/src/App.vue index 972860329..c814e1b83 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -121,10 +121,11 @@ export default defineComponent({ data() { return { loading: false, - tabs: ["Projects", "Users", "Profiles", "Insight"], + tabs: ["Projects", "Users", "Workspaces", "Profiles", "Insight"], tabIcons: [ "clipboard2-data", "people-fill", + "bi-person-workspace", "shield-shaded", "brilliance", ], From cb607a408ea826bf3e4857d4112b5444c2cb5c40 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 10 Dec 2024 12:49:45 +0100 Subject: [PATCH 05/83] feat: workspace directory now has user email instead of untracable UUID --- .../armadillo/audit/AuditEventPublisher.java | 28 +---- .../audit/AuthenticationAuditListener.java | 9 +- .../info/UserInformationRetriever.java | 33 +++++ .../storage/ArmadilloStorageService.java | 59 +++++++-- .../storage/LocalStorageService.java | 9 +- .../armadillo/storage/StorageService.java | 2 + .../audit/AuditEventPublisherTest.java | 117 ++++++++---------- .../info/UserInformationRetrieverTest.java | 70 +++++++++++ .../storage/ArmadilloStorageServiceTest.java | 11 -- 9 files changed, 218 insertions(+), 120 deletions(-) create mode 100644 armadillo/src/main/java/org/molgenis/armadillo/info/UserInformationRetriever.java create mode 100644 armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java diff --git a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java index 1be6ee2c8..acfb59410 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java @@ -7,6 +7,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import org.molgenis.armadillo.info.UserInformationRetriever; import org.slf4j.MDC; import org.springframework.boot.actuate.audit.listener.AuditApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -14,11 +15,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; @Component @@ -119,31 +115,11 @@ public void audit( Map sessionData = new HashMap<>(data); sessionData.put("sessionId", sessionId); sessionData.put("roles", roles); - var user = getUser(principal); + var user = UserInformationRetriever.getUserIdentifierFromPrincipal(principal); applicationEventPublisher.publishEvent( new AuditApplicationEvent(clock.instant(), user, type, sessionData)); } - static String getUser(Object principal) { - if (principal == null) { - return ANONYMOUS; - } else if (principal instanceof OAuth2AuthenticationToken token) { - return token.getPrincipal().getAttribute(EMAIL); - } else if (principal instanceof JwtAuthenticationToken token) { - return token.getTokenAttributes().get(EMAIL).toString(); - } else if (principal instanceof DefaultOAuth2User user) { - return user.getAttributes().get(EMAIL).toString(); - } else if (principal instanceof Jwt jwt) { - return jwt.getClaims().get(EMAIL).toString(); - } else if (principal instanceof User user) { - return user.getUsername(); - } else if (principal instanceof Principal p) { - return p.getName(); - } else { - return principal.toString(); - } - } - public void audit(Principal principal, String type, Map data) { audit(principal, type, data, MDC.get(MDC_SESSION_ID), getRoles()); } diff --git a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuthenticationAuditListener.java b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuthenticationAuditListener.java index c77a3de4d..88b39da2b 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuthenticationAuditListener.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuthenticationAuditListener.java @@ -1,9 +1,8 @@ package org.molgenis.armadillo.audit; -import static org.molgenis.armadillo.audit.AuditEventPublisher.getUser; - import jakarta.validation.constraints.NotNull; import java.util.Map; +import org.molgenis.armadillo.info.UserInformationRetriever; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener; import org.springframework.security.authentication.event.AbstractAuthenticationEvent; @@ -29,7 +28,8 @@ public void onApplicationEvent(@NotNull AbstractAuthenticationEvent event) { private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { publish( new AuditEvent( - getUser(event.getAuthentication().getPrincipal()), + UserInformationRetriever.getUserIdentifierFromPrincipal( + event.getAuthentication().getPrincipal()), AUTHENTICATION_SUCCESS, Map.of( "details", @@ -41,7 +41,8 @@ private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { private void onLogoutSuccessEvent(LogoutSuccessEvent event) { publish( new AuditEvent( - getUser(event.getAuthentication().getPrincipal()), + UserInformationRetriever.getUserIdentifierFromPrincipal( + event.getAuthentication().getPrincipal()), LOGOUT_SUCCESS, Map.of( "details", diff --git a/armadillo/src/main/java/org/molgenis/armadillo/info/UserInformationRetriever.java b/armadillo/src/main/java/org/molgenis/armadillo/info/UserInformationRetriever.java new file mode 100644 index 000000000..f328181e1 --- /dev/null +++ b/armadillo/src/main/java/org/molgenis/armadillo/info/UserInformationRetriever.java @@ -0,0 +1,33 @@ +package org.molgenis.armadillo.info; + +import java.security.Principal; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +public class UserInformationRetriever { + static final String EMAIL = "email"; + static final String ANONYMOUS = "ANONYMOUS"; + + public static String getUserIdentifierFromPrincipal(Object principal) { + if (principal == null) { + return ANONYMOUS; + } else if (principal instanceof OAuth2AuthenticationToken token) { + return token.getPrincipal().getAttribute(EMAIL); + } else if (principal instanceof JwtAuthenticationToken token) { + return token.getTokenAttributes().get(EMAIL).toString(); + } else if (principal instanceof DefaultOAuth2User user) { + return user.getAttributes().get(EMAIL).toString(); + } else if (principal instanceof Jwt jwt) { + return jwt.getClaims().get(EMAIL).toString(); + } else if (principal instanceof User user) { + return user.getUsername(); + } else if (principal instanceof Principal p) { + return p.getName(); + } else { + return principal.toString(); + } + } +} diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 44849ee88..5b9eb06d0 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -4,6 +4,7 @@ import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import static org.apache.commons.io.FilenameUtils.removeExtension; +import static org.molgenis.armadillo.info.UserInformationRetriever.getUserIdentifierFromPrincipal; import static org.molgenis.armadillo.storage.StorageService.getHumanReadableByteCount; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; @@ -20,6 +21,8 @@ import org.apache.commons.io.FilenameUtils; import org.molgenis.armadillo.exceptions.*; import org.molgenis.armadillo.model.Workspace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; @@ -35,8 +38,11 @@ public class ArmadilloStorageService { public static final String LINK_FILE = ".alf"; public static final String RDS = ".rds"; public static final String SYSTEM = "system"; + public static final String RDATA_EXT = ".RData"; private final StorageService storageService; + private static final Logger LOGGER = LoggerFactory.getLogger(LocalStorageService.class); + public ArmadilloStorageService(StorageService storageService) { this.storageService = storageService; } @@ -209,10 +215,6 @@ public Map> listUserWorkspaces() { storageService.listBuckets().stream() .filter((user) -> user.startsWith(USER_PREFIX)) .toList(); - // How to get username from weird id thingy without principal? - // can we store weird number thingy? - // Do we maybe have it saved already? - // Where does it come from? return availableUsers.stream() .collect( Collectors.toMap( @@ -228,16 +230,16 @@ public InputStream loadWorkspace(Principal principal, String id) { } private static String getWorkspaceObjectName(String id) { - return id + ".RData"; + return id + RDATA_EXT; + } + + private static String getOldUserBucketName(Principal principal) { + return USER_PREFIX + principal.getName(); } private static String getUserBucketName(Principal principal) { - String bucketName = USER_PREFIX + principal.getName(); - if (!bucketName.matches(BUCKET_REGEX)) { - throw new IllegalArgumentException( - "Cannot create valid S3 bucket for username " + principal.getName()); - } - return bucketName; + String userIdentifier = getUserIdentifierFromPrincipal(principal); + return USER_PREFIX + userIdentifier; } private static Workspace toWorkspace(ObjectMetadata item) { @@ -260,12 +262,47 @@ private void trySaveWorkspace(ArmadilloWorkspace workspace, Principal principal, } } + public void moveWorkspacesIfInOldBucket(Principal principal) { + String oldBucketName = getOldUserBucketName(principal); + String newBucketName = getUserBucketName(principal); + // only move workspaces from old bucket to new if there is no new bucket yet, we don't want to + if (storageService.bucketExists(oldBucketName) && !storageService.bucketExists(newBucketName)) { + LOGGER.info( + "Found old workspaces bucket for user, moving workspaces from old directory [{}] to new directory [{}]", + oldBucketName, + newBucketName); + // move all data of old bucket to new one + List existingWorkspaces = storageService.listObjects(oldBucketName); + existingWorkspaces.forEach( + (ws) -> { + // define workspace from ObjectMetaData + String workspaceName = ws.name(); + + if (workspaceName.endsWith(RDATA_EXT)) { + InputStream wsIs = storageService.load(getOldUserBucketName(principal), ws.name()); + ArmadilloWorkspace armadilloWorkspace = new ArmadilloWorkspace(wsIs); + try { + LOGGER.info("Moving workspace: [{}]", ws.name()); + trySaveWorkspace(armadilloWorkspace, principal, ws.name().replace(RDATA_EXT, "")); + LOGGER.info("Workspace: [{}] moved to: [{}]", ws.name(), newBucketName); + } catch (Exception e) { + // Log when we can't migrate workspace + LOGGER.warn( + "Can't migrate workspace: [{}], because: {}", ws.name(), e.getMessage()); + } + } + }); + } + } + public void saveWorkspace(InputStream is, Principal principal, String id) { // Load root dir File drive = new File("/"); long usableSpace = drive.getUsableSpace(); try { + moveWorkspacesIfInOldBucket(principal); ArmadilloWorkspace workspace = storageService.getWorkSpace(is); + long fileSize = workspace.getSize(); if (usableSpace > fileSize * 2L) { trySaveWorkspace(workspace, principal, id); diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java index e5c3c1f9b..6231e85a7 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java @@ -56,8 +56,7 @@ public boolean objectExists(String bucketName, String objectName) { try { // check bucket - Path dir = Paths.get(rootDir, bucketName); - if (!Files.exists(dir)) { + if (!bucketExists(bucketName)) { return false; } // check object @@ -68,6 +67,12 @@ public boolean objectExists(String bucketName, String objectName) { } } + public boolean bucketExists(String bucketName) { + // check bucket + Path dir = Paths.get(rootDir, bucketName); + return Files.exists(dir); + } + @Override public void createBucketIfNotExists(String bucketName) { try { diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java index e3d8c2e9d..c60b4e7c4 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java @@ -13,6 +13,8 @@ public interface StorageService { boolean objectExists(String bucket, String objectName); + boolean bucketExists(String bucket); + List getUnavailableVariables(String bucketName, String objectName, String variables) throws IOException; diff --git a/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java b/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java index c5ca1d9af..1a906d0a5 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java @@ -1,71 +1,56 @@ package org.molgenis.armadillo.audit; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.molgenis.armadillo.audit.AuditEventPublisher.ANONYMOUS; -import static org.molgenis.armadillo.audit.AuditEventPublisher.getUser; - -import java.security.Principal; -import org.junit.jupiter.api.Test; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - class AuditEventPublisherTest { - @Test - void testGetAnonymousUser() { - assertEquals(ANONYMOUS, getUser(null)); - } - - @Test - void testGetOidcUser() { - var principal = mock(OAuth2AuthenticationToken.class, RETURNS_DEEP_STUBS); - when(principal.getPrincipal().getAttribute("email")).thenReturn("henk@molgenis.nl"); - - assertEquals("henk@molgenis.nl", getUser(principal)); - } - - @Test - void testGetBasicAuthUser() { - var principal = mock(Principal.class); - when(principal.getName()).thenReturn("admin"); - - assertEquals("admin", getUser(principal)); - } - - @Test - void testJwtToken() { - var principal = mock(JwtAuthenticationToken.class, RETURNS_DEEP_STUBS); - when(principal.getTokenAttributes().get("email")).thenReturn("tommy@molgenis.nl"); - - assertEquals("tommy@molgenis.nl", getUser(principal)); - } - - @Test - void testJwt() { - var principal = mock(Jwt.class, RETURNS_DEEP_STUBS); - when(principal.getClaims().get("email")).thenReturn("tommy@molgenis.nl"); - assertEquals("tommy@molgenis.nl", getUser(principal)); - } - - @Test - void testAuthenticationOfUser() { - var principal = mock(DefaultOAuth2User.class, RETURNS_DEEP_STUBS); - when(principal.getAttributes().get("email")).thenReturn("bofke@molgenis.nl"); - - assertEquals("bofke@molgenis.nl", getUser(principal)); - } - - @Test - void testLoginBasicAuthUser() { - var principal = mock(User.class); - when(principal.getUsername()).thenReturn("admin"); - - assertEquals("admin", getUser(principal)); - } + // @Test + // void testGetAnonymousUser() { + // assertEquals(ANONYMOUS, getUser(null)); + // } + + // @Test + // void testGetOidcUser() { + // var principal = mock(OAuth2AuthenticationToken.class, RETURNS_DEEP_STUBS); + // when(principal.getPrincipal().getAttribute("email")).thenReturn("henk@molgenis.nl"); + // + // assertEquals("henk@molgenis.nl", getUser(principal)); + // } + + // @Test + // void testGetBasicAuthUser() { + // var principal = mock(Principal.class); + // when(principal.getName()).thenReturn("admin"); + // + // assertEquals("admin", getUser(principal)); + // } + // + // @Test + // void testJwtToken() { + // var principal = mock(JwtAuthenticationToken.class, RETURNS_DEEP_STUBS); + // when(principal.getTokenAttributes().get("email")).thenReturn("tommy@molgenis.nl"); + // + // assertEquals("tommy@molgenis.nl", getUser(principal)); + // } + // + // @Test + // void testJwt() { + // var principal = mock(Jwt.class, RETURNS_DEEP_STUBS); + // when(principal.getClaims().get("email")).thenReturn("tommy@molgenis.nl"); + // assertEquals("tommy@molgenis.nl", getUser(principal)); + // } + // + // @Test + // void testAuthenticationOfUser() { + // var principal = mock(DefaultOAuth2User.class, RETURNS_DEEP_STUBS); + // when(principal.getAttributes().get("email")).thenReturn("bofke@molgenis.nl"); + // + // assertEquals("bofke@molgenis.nl", getUser(principal)); + // } + // + // @Test + // void testLoginBasicAuthUser() { + // var principal = mock(User.class); + // when(principal.getUsername()).thenReturn("admin"); + // + // assertEquals("admin", getUser(principal)); + // } } diff --git a/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java b/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java new file mode 100644 index 000000000..08bd38535 --- /dev/null +++ b/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java @@ -0,0 +1,70 @@ +package org.molgenis.armadillo.info; + +import static org.mockito.Mockito.*; +import static org.molgenis.armadillo.info.UserInformationRetriever.getUserIdentifierFromPrincipal; + +import java.security.Principal; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +@ExtendWith(MockitoExtension.class) +class UserInformationRetrieverTest { + @Test + void testGetAnonymousUser() { + Assertions.assertEquals( + UserInformationRetriever.ANONYMOUS, getUserIdentifierFromPrincipal(null)); + } + + @Test + void testGetOidcUser() { + var principal = mock(OAuth2AuthenticationToken.class, RETURNS_DEEP_STUBS); + when(principal.getPrincipal().getAttribute("email")).thenReturn("henk@molgenis.nl"); + Assertions.assertEquals("henk@molgenis.nl", getUserIdentifierFromPrincipal(principal)); + } + + @Test + void testGetBasicAuthUser() { + var principal = mock(Principal.class); + when(principal.getName()).thenReturn("admin"); + + Assertions.assertEquals("admin", getUserIdentifierFromPrincipal(principal)); + } + + @Test + void testJwtToken() { + var principal = mock(JwtAuthenticationToken.class, RETURNS_DEEP_STUBS); + when(principal.getTokenAttributes().get("email")).thenReturn("tommy@molgenis.nl"); + + Assertions.assertEquals("tommy@molgenis.nl", getUserIdentifierFromPrincipal(principal)); + } + + @Test + void testJwt() { + var principal = mock(Jwt.class, RETURNS_DEEP_STUBS); + when(principal.getClaims().get("email")).thenReturn("tommy@molgenis.nl"); + Assertions.assertEquals("tommy@molgenis.nl", getUserIdentifierFromPrincipal(principal)); + } + + @Test + void testAuthenticationOfUser() { + var principal = mock(DefaultOAuth2User.class, RETURNS_DEEP_STUBS); + when(principal.getAttributes().get("email")).thenReturn("bofke@molgenis.nl"); + + Assertions.assertEquals("bofke@molgenis.nl", getUserIdentifierFromPrincipal(principal)); + } + + @Test + void testLoginBasicAuthUser() { + var principal = mock(User.class); + when(principal.getUsername()).thenReturn("admin"); + + Assertions.assertEquals("admin", getUserIdentifierFromPrincipal(principal)); + } +} diff --git a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java index 4b5a057b5..b0e60a158 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java @@ -530,17 +530,6 @@ void testSaveWorkspaceReturnsErrorWhenBiggerThan2Gbs() { } } - @Test - void testSaveWorkspaceChecksBucketName() { - ArmadilloWorkspace workspaceMock = mock(ArmadilloWorkspace.class); - when(principal.getName()).thenReturn("Henk"); - when(storageService.getWorkSpace(is)).thenReturn(workspaceMock); - when(workspaceMock.getSize()).thenReturn(12345L); - assertThrows( - IllegalArgumentException.class, - () -> armadilloStorage.saveWorkspace(is, principal, "test")); - } - @Test @WithMockUser(roles = "SU") void testResourceExists() { From e31aec2e262bcf7c4fb209c3ea763d708b6f4b54 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 10 Dec 2024 12:59:26 +0100 Subject: [PATCH 06/83] chore: remove moved tests --- .../audit/AuditEventPublisherTest.java | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java diff --git a/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java b/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java deleted file mode 100644 index 1a906d0a5..000000000 --- a/armadillo/src/test/java/org/molgenis/armadillo/audit/AuditEventPublisherTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.molgenis.armadillo.audit; - -class AuditEventPublisherTest { - - // @Test - // void testGetAnonymousUser() { - // assertEquals(ANONYMOUS, getUser(null)); - // } - - // @Test - // void testGetOidcUser() { - // var principal = mock(OAuth2AuthenticationToken.class, RETURNS_DEEP_STUBS); - // when(principal.getPrincipal().getAttribute("email")).thenReturn("henk@molgenis.nl"); - // - // assertEquals("henk@molgenis.nl", getUser(principal)); - // } - - // @Test - // void testGetBasicAuthUser() { - // var principal = mock(Principal.class); - // when(principal.getName()).thenReturn("admin"); - // - // assertEquals("admin", getUser(principal)); - // } - // - // @Test - // void testJwtToken() { - // var principal = mock(JwtAuthenticationToken.class, RETURNS_DEEP_STUBS); - // when(principal.getTokenAttributes().get("email")).thenReturn("tommy@molgenis.nl"); - // - // assertEquals("tommy@molgenis.nl", getUser(principal)); - // } - // - // @Test - // void testJwt() { - // var principal = mock(Jwt.class, RETURNS_DEEP_STUBS); - // when(principal.getClaims().get("email")).thenReturn("tommy@molgenis.nl"); - // assertEquals("tommy@molgenis.nl", getUser(principal)); - // } - // - // @Test - // void testAuthenticationOfUser() { - // var principal = mock(DefaultOAuth2User.class, RETURNS_DEEP_STUBS); - // when(principal.getAttributes().get("email")).thenReturn("bofke@molgenis.nl"); - // - // assertEquals("bofke@molgenis.nl", getUser(principal)); - // } - // - // @Test - // void testLoginBasicAuthUser() { - // var principal = mock(User.class); - // when(principal.getUsername()).thenReturn("admin"); - // - // assertEquals("admin", getUser(principal)); - // } -} From 76ce1ac366bd3d909cf7644deef4a5e0a7287c21 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 10 Dec 2024 14:20:01 +0100 Subject: [PATCH 07/83] test: add tests --- .../armadillo/audit/AuditEventPublisher.java | 1 + .../armadillo/controller/DataController.java | 2 +- .../storage/ArmadilloStorageService.java | 2 +- .../controller/DataControllerTest.java | 18 ++++++++++++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java index acfb59410..f070d7a19 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java @@ -38,6 +38,7 @@ public class AuditEventPublisher implements ApplicationEventPublisherAware { public static final String GET_ASSIGN_METHODS = "GET_ASSIGN_METHODS"; public static final String GET_AGGREGATE_METHODS = "GET_AGGREGATE_METHODS"; public static final String GET_USER_WORKSPACES = "GET_USER_WORKSPACES"; + public static final String GET_ALL_USER_WORKSPACES = "GET_ALL_USER_WORKSPACES"; public static final String DELETE_USER_WORKSPACE = "DELETE_USER_WORKSPACE"; public static final String SAVE_USER_WORKSPACE = "SAVE_USER_WORKSPACE"; public static final String LOAD_USER_WORKSPACE = "LOAD_USER_WORKSPACE"; diff --git a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java index ee906b349..f4adbf60c 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java @@ -410,7 +410,7 @@ public List getWorkspaces(Principal principal) { @GetMapping(value = "/all-workspaces", produces = APPLICATION_JSON_VALUE) public Map> getAllUserWorkspaces(Principal principal) { return auditEventPublisher.audit( - storage::listUserWorkspaces, principal, GET_USER_WORKSPACES, Map.of()); + storage::listAllUserWorkspaces, principal, GET_ALL_USER_WORKSPACES, Map.of()); } @Operation( diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 5b9eb06d0..89a45390d 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -210,7 +210,7 @@ public List listWorkspaces(Principal principal) { } @PreAuthorize("hasAnyRole('ROLE_SU')") - public Map> listUserWorkspaces() { + public Map> listAllUserWorkspaces() { List availableUsers = storageService.listBuckets().stream() .filter((user) -> user.startsWith(USER_PREFIX)) diff --git a/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java b/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java index 1d3c5da66..3517f5bb7 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java @@ -38,7 +38,6 @@ import org.molgenis.armadillo.exceptions.ExpressionException; import org.molgenis.armadillo.exceptions.UnknownProfileException; import org.molgenis.armadillo.exceptions.UnknownVariableException; -import org.molgenis.armadillo.model.Workspace; import org.molgenis.armadillo.service.DSEnvironmentCache; import org.molgenis.armadillo.service.ExpressionRewriter; import org.molgenis.armadillo.storage.ArmadilloLinkFile; @@ -1047,9 +1046,6 @@ void testLoadResourceFails() throws Exception { @Test @WithMockUser(roles = "SU") void testGetWorkspaces() throws Exception { - when(armadilloStorage.listWorkspaces(any(Principal.class))) - .thenReturn(List.of(mock(Workspace.class))); - mockMvc.perform(get("/workspaces").session(session)).andExpect(status().isOk()); auditEventValidator.validateAuditEvent( @@ -1060,6 +1056,20 @@ void testGetWorkspaces() throws Exception { Map.of("sessionId", sessionId, "roles", List.of("ROLE_SU")))); } + @Test + @WithMockUser(roles = "SU") + void testGetAllWorkspaces() throws Exception { + + mockMvc.perform(get("/all-workspaces").session(session)).andExpect(status().isOk()); + + auditEventValidator.validateAuditEvent( + new AuditEvent( + instant, + "user", + "GET_ALL_USER_WORKSPACES", + Map.of("sessionId", sessionId, "roles", List.of("ROLE_SU")))); + } + @Test void testGetMatchedData() { DataController dataController = From d7235a5862c1f8be3af5b9d50f4f6c0e40f106bf Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 10 Dec 2024 15:53:43 +0100 Subject: [PATCH 08/83] test(ArmadilloStorageService): add tests for listAllUserWorkspaces --- .../storage/ArmadilloStorageServiceTest.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java index b0e60a158..ae792ea68 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java @@ -23,6 +23,7 @@ import java.security.Principal; import java.time.Instant; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -764,4 +765,116 @@ void testGetInfo() { FileInfo actual = armadilloStorage.getInfo("gecko", srcObj); assertEquals(info, actual); } + + // Test: User has ROLE_SU, and storage service returns expected data + @Test + @WithMockUser(roles = "SU") // Simulating a user with ROLE_SU + void testListAllUserWorkspaces() { + ObjectMetadata ws1Mock = mock(ObjectMetadata.class); + ObjectMetadata ws2Mock = mock(ObjectMetadata.class); + ObjectMetadata ws3Mock = mock(ObjectMetadata.class); + + when(ws1Mock.lastModified()) + .thenReturn( + ZonedDateTime.ofInstant(Instant.ofEpochMilli(1542654265978L), ZoneId.systemDefault())); + when(ws2Mock.lastModified()) + .thenReturn( + ZonedDateTime.ofInstant(Instant.ofEpochMilli(1542654265978L), ZoneId.systemDefault())); + when(ws3Mock.lastModified()) + .thenReturn( + ZonedDateTime.ofInstant(Instant.ofEpochMilli(1542654265978L), ZoneId.systemDefault())); + + when(ws1Mock.name()).thenReturn("workspace1.RData"); + when(ws2Mock.name()).thenReturn("workspace2.RData"); + when(ws3Mock.name()).thenReturn("workspace3.RData"); + + when(ws1Mock.size()).thenReturn(1234L); + when(ws2Mock.size()).thenReturn(1235L); + when(ws3Mock.size()).thenReturn(1236L); + + // Given + List mockBuckets = Arrays.asList("user-bucket1", "user-bucket2"); + List mockObjects1 = Arrays.asList(ws1Mock, ws2Mock); + List mockObjects2 = singletonList(ws3Mock); + + // Mocking the behavior of storageService + when(storageService.listBuckets()).thenReturn(mockBuckets); + when(storageService.listObjects("user-bucket1")).thenReturn(mockObjects1); + when(storageService.listObjects("user-bucket2")).thenReturn(mockObjects2); + + // When + Map> result = armadilloStorage.listAllUserWorkspaces(); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); // Expecting 2 users/buckets + assertTrue(result.containsKey("user-bucket1")); + assertTrue(result.containsKey("user-bucket2")); + + List user1Workspaces = result.get("user-bucket1"); + assertNotNull(user1Workspaces); + assertEquals(2, user1Workspaces.size()); // Expect 2 workspaces for user1 + + List user2Workspaces = result.get("user-bucket2"); + assertNotNull(user2Workspaces); + assertEquals(1, user2Workspaces.size()); // Expect 1 workspace for user2 + } + + // Test: User without ROLE_SU should not access the method + @Test + @WithMockUser(roles = "RESEARCHER") // Simulating a user without the correct role + void testListUserWorkspacesWithUnauthorizedUser() { + // Given + List mockBuckets = Arrays.asList("user-bucket1"); + + // Mocking the behavior of storageService + when(storageService.listBuckets()).thenReturn(mockBuckets); + + // When & Then: Expecting an access denied exception due to lack of proper role + assertThrows( + AccessDeniedException.class, + () -> { + armadilloStorage.listAllUserWorkspaces(); + }); + } + + // Test: No buckets available + @Test + @WithMockUser(roles = "SU") + void testListUserWorkspacesNoBuckets() { + // Given + List mockBuckets = Collections.emptyList(); + + // Mocking the behavior of storageService + when(storageService.listBuckets()).thenReturn(mockBuckets); + + // When + Map> result = armadilloStorage.listAllUserWorkspaces(); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); // Expecting an empty map + } + + // Test: Single bucket with no workspaces + @Test + @WithMockUser(roles = "SU") + void testListUserWorkspacesNoWorkspacesInBucket() { + // Given + List mockBuckets = Arrays.asList("user-bucket1"); + List mockObjects = new ArrayList<>(); + + // Mocking the behavior of storageService + when(storageService.listBuckets()).thenReturn(mockBuckets); + when(storageService.listObjects("user-bucket1")).thenReturn(mockObjects); + + // When + Map> result = armadilloStorage.listAllUserWorkspaces(); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); // 1 bucket should be present + assertTrue(result.containsKey("user-bucket1")); + assertTrue(result.get("user-bucket1").isEmpty()); // Expecting an empty list for workspaces + } } From c79563001c86bc69f864928f7ed4a18262ce89ed Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 10 Dec 2024 16:17:34 +0100 Subject: [PATCH 09/83] refactor(ArmadilloStorageService): split up move workspace function --- .../storage/ArmadilloStorageService.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 89a45390d..52fe1d628 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -271,30 +271,34 @@ public void moveWorkspacesIfInOldBucket(Principal principal) { "Found old workspaces bucket for user, moving workspaces from old directory [{}] to new directory [{}]", oldBucketName, newBucketName); - // move all data of old bucket to new one List existingWorkspaces = storageService.listObjects(oldBucketName); existingWorkspaces.forEach( (ws) -> { - // define workspace from ObjectMetaData - String workspaceName = ws.name(); - - if (workspaceName.endsWith(RDATA_EXT)) { - InputStream wsIs = storageService.load(getOldUserBucketName(principal), ws.name()); - ArmadilloWorkspace armadilloWorkspace = new ArmadilloWorkspace(wsIs); - try { - LOGGER.info("Moving workspace: [{}]", ws.name()); - trySaveWorkspace(armadilloWorkspace, principal, ws.name().replace(RDATA_EXT, "")); - LOGGER.info("Workspace: [{}] moved to: [{}]", ws.name(), newBucketName); - } catch (Exception e) { - // Log when we can't migrate workspace - LOGGER.warn( - "Can't migrate workspace: [{}], because: {}", ws.name(), e.getMessage()); - } + if (ws.name().endsWith(RDATA_EXT)) { + moveWorkspace(ws, principal, oldBucketName, newBucketName); } }); } } + void moveWorkspace( + ObjectMetadata workspaceMetaData, + Principal principal, + String oldBucketName, + String newBucketName) { + String workspaceName = workspaceMetaData.name(); + InputStream wsIs = storageService.load(oldBucketName, workspaceName); + ArmadilloWorkspace armadilloWorkspace = new ArmadilloWorkspace(wsIs); + try { + LOGGER.info("Moving workspace: [{}]", workspaceName); + trySaveWorkspace(armadilloWorkspace, principal, workspaceName.replace(RDATA_EXT, "")); + LOGGER.info("Workspace: [{}] moved to: [{}]", workspaceName, newBucketName); + } catch (Exception e) { + // Log when we can't migrate workspace + LOGGER.warn("Can't migrate workspace: [{}], because: {}", workspaceName, e.getMessage()); + } + } + public void saveWorkspace(InputStream is, Principal principal, String id) { // Load root dir File drive = new File("/"); From 59bd184b7553de54a29d65bf9481c72dfe0180c2 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Thu, 12 Dec 2024 11:51:08 +0100 Subject: [PATCH 10/83] refactor: make more suitable for testing --- .../storage/ArmadilloStorageService.java | 26 +++--------------- .../storage/LocalStorageService.java | 27 +++++++++++++++++++ .../armadillo/storage/StorageService.java | 7 +++++ 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 52fe1d628..0b5b8f702 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -229,7 +229,7 @@ public InputStream loadWorkspace(Principal principal, String id) { return storageService.load(getUserBucketName(principal), getWorkspaceObjectName(id)); } - private static String getWorkspaceObjectName(String id) { + static String getWorkspaceObjectName(String id) { return id + RDATA_EXT; } @@ -237,7 +237,7 @@ private static String getOldUserBucketName(Principal principal) { return USER_PREFIX + principal.getName(); } - private static String getUserBucketName(Principal principal) { + static String getUserBucketName(Principal principal) { String userIdentifier = getUserIdentifierFromPrincipal(principal); return USER_PREFIX + userIdentifier; } @@ -274,31 +274,13 @@ public void moveWorkspacesIfInOldBucket(Principal principal) { List existingWorkspaces = storageService.listObjects(oldBucketName); existingWorkspaces.forEach( (ws) -> { - if (ws.name().endsWith(RDATA_EXT)) { - moveWorkspace(ws, principal, oldBucketName, newBucketName); + if (ws.name().toLowerCase().endsWith(RDATA_EXT.toLowerCase())) { + storageService.moveWorkspace(ws, principal, oldBucketName, newBucketName); } }); } } - void moveWorkspace( - ObjectMetadata workspaceMetaData, - Principal principal, - String oldBucketName, - String newBucketName) { - String workspaceName = workspaceMetaData.name(); - InputStream wsIs = storageService.load(oldBucketName, workspaceName); - ArmadilloWorkspace armadilloWorkspace = new ArmadilloWorkspace(wsIs); - try { - LOGGER.info("Moving workspace: [{}]", workspaceName); - trySaveWorkspace(armadilloWorkspace, principal, workspaceName.replace(RDATA_EXT, "")); - LOGGER.info("Workspace: [{}] moved to: [{}]", workspaceName, newBucketName); - } catch (Exception e) { - // Log when we can't migrate workspace - LOGGER.warn("Can't migrate workspace: [{}], because: {}", workspaceName, e.getMessage()); - } - } - public void saveWorkspace(InputStream is, Principal principal, String id) { // Load root dir File drive = new File("/"); diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java index 6231e85a7..f240c3e9d 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java @@ -3,11 +3,13 @@ import static java.lang.String.format; import static java.util.Collections.emptyList; import static org.molgenis.armadillo.storage.ArmadilloStorageService.*; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.Principal; import java.util.*; import org.molgenis.armadillo.exceptions.IllegalPathException; import org.molgenis.armadillo.exceptions.StorageException; @@ -217,6 +219,31 @@ public ArmadilloWorkspace getWorkSpace(InputStream is) { return new ArmadilloWorkspace(is); } + public void moveWorkspace( + ObjectMetadata workspaceMetaData, + Principal principal, + String oldBucketName, + String newBucketName) { + String workspaceName = workspaceMetaData.name(); + InputStream wsIs = load(oldBucketName, workspaceName); + ArmadilloWorkspace armadilloWorkspace = new ArmadilloWorkspace(wsIs); + try { + LOGGER.info("Moving workspace: [{}]", workspaceName); + + save( + armadilloWorkspace.createInputStream(), + getUserBucketName(principal), + getWorkspaceObjectName(workspaceName.replace(RDATA_EXT, "")), + APPLICATION_OCTET_STREAM); + + // trySaveWorkspace(armadilloWorkspace, principal, workspaceName.replace(RDATA_EXT, "")); + LOGGER.info("Workspace: [{}] moved to: [{}]", workspaceName, newBucketName); + } catch (Exception e) { + // Log when we can't migrate workspace + LOGGER.warn("Can't migrate workspace: [{}], because: {}", workspaceName, e.getMessage()); + } + } + private FileInfo getFileInfoForLinkFile( String bucketName, String objectName, String fileSizeWithUnit) throws FileNotFoundException { ArmadilloLinkFile linkFile = getArmadilloLinkFileFromName(bucketName, objectName); diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java index c60b4e7c4..a4720695f 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; +import java.security.Principal; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.List; @@ -58,4 +59,10 @@ static String getHumanReadableByteCount(long bytes) { } ArmadilloWorkspace getWorkSpace(InputStream is); + + void moveWorkspace( + ObjectMetadata workspaceMetaData, + Principal principal, + String oldBucketName, + String newBucketName); } From 7776b493fe2f14fd9886b77313b404b75751b26a Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Thu, 12 Dec 2024 11:51:38 +0100 Subject: [PATCH 11/83] test(ArmadilloStorageService): add tests --- .../storage/ArmadilloStorageServiceTest.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java index ae792ea68..0a113bcaa 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java @@ -33,6 +33,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.molgenis.armadillo.exceptions.*; +import org.molgenis.armadillo.info.UserInformationRetriever; import org.molgenis.armadillo.model.Workspace; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -51,6 +52,10 @@ class ArmadilloStorageServiceTest { final String SHARED_GECKO = "shared-gecko"; final String SHARED_DIABETES = "shared-diabetes"; final String METADATA_FILE = "metadata.json"; + private static final String USER_ID = "very-random-id"; + private static final String USER_EMAIL = "user@email.com"; + private static final String OLD_BUCKET = "user-very-random-id"; + private static final String NEW_BUCKET = "user-user@email.com"; @MockBean StorageService storageService; @Mock Principal principal; @@ -877,4 +882,99 @@ void testListUserWorkspacesNoWorkspacesInBucket() { assertTrue(result.containsKey("user-bucket1")); assertTrue(result.get("user-bucket1").isEmpty()); // Expecting an empty list for workspaces } + + // Test case 1: old bucket exists, new bucket doesn't exist, and there are valid workspaces to + // move + @Test + void + testMoveWorkspacesIfInOldBucket_WhenOldBucketExistsNewBucketDoesNotExist_AndWorkspacesToMove() { + when(storageService.bucketExists(OLD_BUCKET)).thenReturn(true); + when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); + when(storageService.listObjects(OLD_BUCKET)).thenReturn(List.of(item)); + when(principal.getName()).thenReturn(USER_ID); + when(item.name()).thenReturn("workspace1.RData"); + + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + armadilloStorage.moveWorkspacesIfInOldBucket(principal); + verify(storageService, times(1)) + .moveWorkspace(eq(item), eq(principal), eq(OLD_BUCKET), eq(NEW_BUCKET)); + } + } + + // Test case 2: old bucket doesn't exist, so no action is taken + @Test + void testMoveWorkspacesIfInOldBucket_WhenOldBucketDoesNotExist() { + when(storageService.bucketExists(OLD_BUCKET)).thenReturn(false); + when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); + + armadilloStorage.moveWorkspacesIfInOldBucket(principal); + + verify(storageService, never()).listObjects(any()); + } + + // Test case 3: new bucket already exists, so no workspaces should be moved + @Test + void testMoveWorkspacesIfInOldBucket_WhenNewBucketExists() { + when(storageService.bucketExists(OLD_BUCKET)).thenReturn(true); + when(storageService.bucketExists(NEW_BUCKET)).thenReturn(true); + + armadilloStorage.moveWorkspacesIfInOldBucket(principal); + + verify(storageService, never()).listObjects(any()); + verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + } + + // Test case 4: old bucket exists, new bucket does not exist, but no workspaces to move + @Test + void testMoveWorkspacesIfInOldBucket_WhenOldBucketExistsButNoWorkspacesToMove() { + when(storageService.bucketExists(OLD_BUCKET)).thenReturn(true); + when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); + when(storageService.listObjects(OLD_BUCKET)).thenReturn(Collections.emptyList()); + + armadilloStorage.moveWorkspacesIfInOldBucket(principal); + + verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + } + + // Test case 5: old bucket exists, new bucket does not exist, but workspace with wrong extension + // (not RDATA) + @Test + void testMoveWorkspacesIfInOldBucket_WhenOldBucketExistsButNonRDataFiles() { + when(storageService.bucketExists(OLD_BUCKET)).thenReturn(true); + when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); + when(storageService.listObjects(OLD_BUCKET)).thenReturn(List.of(item)); + when(principal.getName()).thenReturn(USER_ID); + when(item.name()).thenReturn("workspace1.txt"); + + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + armadilloStorage.moveWorkspacesIfInOldBucket(principal); + verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + } + } + + // Test case 6: handle exception (e.g., if storageService throws an exception) + @Test + void testMoveWorkspacesIfInOldBucket_WhenStorageServiceFails() { + when(storageService.bucketExists(OLD_BUCKET)).thenReturn(true); + when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); + when(principal.getName()).thenReturn(USER_ID); + when(storageService.listObjects(OLD_BUCKET)).thenThrow(new RuntimeException("Service failure")); + + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + assertThrows( + RuntimeException.class, () -> armadilloStorage.moveWorkspacesIfInOldBucket(principal)); + } + } } From 2a3daadaa13e43858a7354179aa08cf7450e8dac Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Thu, 12 Dec 2024 12:48:42 +0100 Subject: [PATCH 12/83] refactor: cleanup code --- .../org/molgenis/armadillo/storage/LocalStorageService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java index f240c3e9d..d6a821b0c 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java @@ -229,14 +229,11 @@ public void moveWorkspace( ArmadilloWorkspace armadilloWorkspace = new ArmadilloWorkspace(wsIs); try { LOGGER.info("Moving workspace: [{}]", workspaceName); - save( armadilloWorkspace.createInputStream(), getUserBucketName(principal), getWorkspaceObjectName(workspaceName.replace(RDATA_EXT, "")), APPLICATION_OCTET_STREAM); - - // trySaveWorkspace(armadilloWorkspace, principal, workspaceName.replace(RDATA_EXT, "")); LOGGER.info("Workspace: [{}] moved to: [{}]", workspaceName, newBucketName); } catch (Exception e) { // Log when we can't migrate workspace From 308cd65341bd5f36095343c2c59ec6c9d86dcf44 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Thu, 12 Dec 2024 12:55:45 +0100 Subject: [PATCH 13/83] test(UserInformationRetriever): add tests --- .../armadillo/info/UserInformationRetrieverTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java b/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java index 08bd38535..7bfae8e77 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/info/UserInformationRetrieverTest.java @@ -37,6 +37,14 @@ void testGetBasicAuthUser() { Assertions.assertEquals("admin", getUserIdentifierFromPrincipal(principal)); } + @Test + void testGetObject() { + var principal = mock(Object.class); + when(principal.toString()).thenReturn("object"); + + Assertions.assertEquals("object", getUserIdentifierFromPrincipal(principal)); + } + @Test void testJwtToken() { var principal = mock(JwtAuthenticationToken.class, RETURNS_DEEP_STUBS); From 3e109929eb7b240e739a69e105f35ee0967f9589 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 13 Dec 2024 12:36:52 +0100 Subject: [PATCH 14/83] feat: delete workspace for user --- .../armadillo/audit/AuditEventPublisher.java | 1 + .../armadillo/controller/DataController.java | 20 +++++++++++++++++++ .../storage/ArmadilloStorageService.java | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java index f070d7a19..0766035f4 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/audit/AuditEventPublisher.java @@ -93,6 +93,7 @@ public class AuditEventPublisher implements ApplicationEventPublisherAware { public static final String MESSAGE = "message"; public static final String TABLE = "table"; public static final String ID = "id"; + public static final String USER = "user"; static final String ANONYMOUS = "ANONYMOUS"; public static final String MDC_SESSION_ID = "sessionID"; private ApplicationEventPublisher applicationEventPublisher; diff --git a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java index f4adbf60c..1c5561966 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java @@ -437,6 +437,26 @@ public void removeWorkspace( Map.of(ID, id)); } + @DeleteMapping(value = "/workspaces/{user}/{id}") + @ResponseStatus(OK) + public void removeUserWorkspace( + @PathVariable + @Pattern( + regexp = WORKSPACE_ID_FORMAT_REGEX, + message = "Please use only letters, numbers, dashes or underscores") + String user, + String id, + Principal principal) { + auditEventPublisher.audit( + () -> { + storage.removeWorkspaceByStringUserId(user, id); + return null; + }, + principal, + DELETE_USER_WORKSPACE, + Map.of(ID, id, USER, user)); + } + @Operation(summary = "Save user workspace") @PostMapping(value = "/workspaces/{id}", produces = TEXT_PLAIN_VALUE) @ResponseStatus(CREATED) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 0b5b8f702..b6eadd52c 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -307,6 +307,10 @@ public void removeWorkspace(Principal principal, String id) { storageService.delete(getUserBucketName(principal), getWorkspaceObjectName(id)); } + public void removeWorkspaceByStringUserId(String userId, String id) { + storageService.delete(USER_PREFIX + userId, getWorkspaceObjectName(id)); + } + public void saveSystemFile(InputStream is, String name, MediaType mediaType) { storageService.save(is, SYSTEM, name, mediaType); } From f386c65d66d836298487460227a885778b7af761 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 13 Dec 2024 15:34:40 +0100 Subject: [PATCH 15/83] chore: remove invalid validation --- .../armadillo/controller/DataController.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java index 1c5561966..18fd87411 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java @@ -420,13 +420,7 @@ public Map> getAllUserWorkspaces(Principal principal) { }) @DeleteMapping(value = "/workspaces/{id}") @ResponseStatus(OK) - public void removeWorkspace( - @PathVariable - @Pattern( - regexp = WORKSPACE_ID_FORMAT_REGEX, - message = "Please use only letters, numbers, dashes or underscores") - String id, - Principal principal) { + public void removeWorkspace(@PathVariable String id, Principal principal) { auditEventPublisher.audit( () -> { storage.removeWorkspace(principal, id); @@ -440,13 +434,7 @@ public void removeWorkspace( @DeleteMapping(value = "/workspaces/{user}/{id}") @ResponseStatus(OK) public void removeUserWorkspace( - @PathVariable - @Pattern( - regexp = WORKSPACE_ID_FORMAT_REGEX, - message = "Please use only letters, numbers, dashes or underscores") - String user, - String id, - Principal principal) { + @PathVariable String user, @PathVariable String id, Principal principal) { auditEventPublisher.audit( () -> { storage.removeWorkspaceByStringUserId(user, id); From f87610edc4e602d2527ce5a570fca86618d6bd43 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 7 Jan 2025 15:20:45 +0100 Subject: [PATCH 16/83] chore: replace @ in userfoldername with __at__ to make sure we dont run into issues --- .../org/molgenis/armadillo/storage/ArmadilloStorageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index b6eadd52c..5a6f6f156 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -238,7 +238,7 @@ private static String getOldUserBucketName(Principal principal) { } static String getUserBucketName(Principal principal) { - String userIdentifier = getUserIdentifierFromPrincipal(principal); + String userIdentifier = getUserIdentifierFromPrincipal(principal).replace("@", "__at__"); return USER_PREFIX + userIdentifier; } From 0c9e30eee59b6fc98acc136e6aadb42945de3384 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 7 Jan 2025 15:43:59 +0100 Subject: [PATCH 17/83] chore: change statuscode in delete endpoint and if email address is provided convert @ to __at__ to match file system --- .../org/molgenis/armadillo/controller/DataController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java index 18fd87411..e68d73c20 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java @@ -432,12 +432,16 @@ public void removeWorkspace(@PathVariable String id, Principal principal) { } @DeleteMapping(value = "/workspaces/{user}/{id}") - @ResponseStatus(OK) + @ResponseStatus(NO_CONTENT) public void removeUserWorkspace( @PathVariable String user, @PathVariable String id, Principal principal) { + if (user.contains("@")) { + user = user.replace("@", "__at__"); + } + String finalUser = user; auditEventPublisher.audit( () -> { - storage.removeWorkspaceByStringUserId(user, id); + storage.removeWorkspaceByStringUserId(finalUser, id); return null; }, principal, From a5c8dffe80656dd6dadb6eb95a90569df0682454 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Thu, 9 Jan 2025 14:52:33 +0100 Subject: [PATCH 18/83] feat: write file with migration status in migrated workspace user bucket --- .../storage/ArmadilloStorageService.java | 37 ++++++++++++++++++- .../storage/LocalStorageService.java | 11 ++++-- .../armadillo/storage/StorageService.java | 2 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 5a6f6f156..89aa7dec4 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -10,9 +10,12 @@ import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.Principal; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -266,6 +269,7 @@ public void moveWorkspacesIfInOldBucket(Principal principal) { String oldBucketName = getOldUserBucketName(principal); String newBucketName = getUserBucketName(principal); // only move workspaces from old bucket to new if there is no new bucket yet, we don't want to + List migrationStatus = new ArrayList<>(); if (storageService.bucketExists(oldBucketName) && !storageService.bucketExists(newBucketName)) { LOGGER.info( "Found old workspaces bucket for user, moving workspaces from old directory [{}] to new directory [{}]", @@ -274,10 +278,41 @@ public void moveWorkspacesIfInOldBucket(Principal principal) { List existingWorkspaces = storageService.listObjects(oldBucketName); existingWorkspaces.forEach( (ws) -> { + String message = ""; if (ws.name().toLowerCase().endsWith(RDATA_EXT.toLowerCase())) { - storageService.moveWorkspace(ws, principal, oldBucketName, newBucketName); + try { + storageService.moveWorkspace(ws, principal, oldBucketName, newBucketName); + message = + format( + "Successfully migrated workspace [%s] from [%s] to [%s]", + ws.name(), oldBucketName, newBucketName); + } catch (StorageException e) { + message = + format( + "Can't migrate workspace [%s] from [%s] to [%s], because [%s]. Workspace needs to be moved manually.", + ws.name(), oldBucketName, newBucketName, e.getMessage()); + } finally { + migrationStatus.add(message); + } } }); + try { + writeMigrationFile(migrationStatus, newBucketName); + } catch (FileNotFoundException e) { + LOGGER.warn("Can't write migration status file for user [{}].", newBucketName); + } + } + } + + private void writeMigrationFile(List migrationStatus, String bucketName) + throws FileNotFoundException { + Path bucketPath = + Paths.get(storageService.getRootDir(), bucketName).toAbsolutePath().normalize(); + Path path = Paths.get(bucketPath + "/migration-status.txt"); + try { + Files.write(path, migrationStatus, StandardCharsets.UTF_8); + } catch (IOException e) { + LOGGER.warn("Cannot write migration file to [{}] because: [{}]", path, e.getMessage()); } } diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java index d6a821b0c..85232342f 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/LocalStorageService.java @@ -26,7 +26,11 @@ public class LocalStorageService implements StorageService { private static final Logger LOGGER = LoggerFactory.getLogger(LocalStorageService.class); - final String rootDir; + public final String rootDir; + + public String getRootDir() { + return rootDir; + } public LocalStorageService(@Value("${" + ROOT_DIR_PROPERTY + "}") String rootDir) { var dir = new File(rootDir); @@ -231,13 +235,14 @@ public void moveWorkspace( LOGGER.info("Moving workspace: [{}]", workspaceName); save( armadilloWorkspace.createInputStream(), - getUserBucketName(principal), - getWorkspaceObjectName(workspaceName.replace(RDATA_EXT, "")), + newBucketName, + workspaceName, APPLICATION_OCTET_STREAM); LOGGER.info("Workspace: [{}] moved to: [{}]", workspaceName, newBucketName); } catch (Exception e) { // Log when we can't migrate workspace LOGGER.warn("Can't migrate workspace: [{}], because: {}", workspaceName, e.getMessage()); + throw new StorageException(e); } } diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java index a4720695f..ca9315f99 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/StorageService.java @@ -12,6 +12,8 @@ import org.springframework.http.MediaType; public interface StorageService { + public String getRootDir(); + boolean objectExists(String bucket, String objectName); boolean bucketExists(String bucket); From d9584cd05e9b76cdc9ec4bc67d160a54ee577243 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 10 Jan 2025 09:50:31 +0100 Subject: [PATCH 19/83] test(ArmadilloStorageService): add tests for migration status file --- .../storage/ArmadilloStorageService.java | 2 +- .../storage/ArmadilloStorageServiceTest.java | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 89aa7dec4..29975f6a5 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -304,7 +304,7 @@ public void moveWorkspacesIfInOldBucket(Principal principal) { } } - private void writeMigrationFile(List migrationStatus, String bucketName) + void writeMigrationFile(List migrationStatus, String bucketName) throws FileNotFoundException { Path bucketPath = Paths.get(storageService.getRootDir(), bucketName).toAbsolutePath().normalize(); diff --git a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java index 0a113bcaa..6f6a78741 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.Principal; import java.time.Instant; import java.time.ZoneId; @@ -977,4 +978,108 @@ void testMoveWorkspacesIfInOldBucket_WhenStorageServiceFails() { RuntimeException.class, () -> armadilloStorage.moveWorkspacesIfInOldBucket(principal)); } } + + @Test + public void testWriteMigrationFile_success() throws IOException { + // Prepare test data + List migrationStatus = List.of("migration1", "migration2", "migration3"); + String bucketName = "testBucket"; + + // Mock behavior of storageService to return a valid root directory + when(storageService.getRootDir()).thenReturn("/mock/root"); + + // Use Mockito to mock static method Files.write() + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + // Call the method + armadilloStorage.writeMigrationFile(migrationStatus, bucketName); + + // Create the expected path + Path expectedPath = Paths.get("/mock/root", bucketName, "migration-status.txt"); + + // Verify that Files.write was called with the correct parameters + mockedFiles.verify( + () -> + Files.write( + eq(expectedPath), + eq(migrationStatus), + eq(java.nio.charset.StandardCharsets.UTF_8))); + } + } + + @Test + public void testWriteMigrationFile_writeFailure() throws IOException { + // Prepare test data + List migrationStatus = List.of("migration1", "migration2", "migration3"); + String bucketName = "testBucket"; + + // Mock behavior of storageService to return a valid root directory + when(storageService.getRootDir()).thenReturn("/mock/root"); + + // Use Mockito to mock static method Files.write() + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + // Simulate IOException when trying to write the file + mockedFiles + .when(() -> Files.write(any(Path.class), anyList(), any())) + .thenThrow(new IOException("Permission denied")); + + // Call the method + try { + armadilloStorage.writeMigrationFile(migrationStatus, bucketName); + } catch (IOException e) { + assertTrue(e.getMessage().contains("Permission denied")); + } + } + } + + @Test + public void testWriteMigrationFile_emptyMigrationStatus() throws IOException { + // Prepare test data + List migrationStatus = List.of(); // Empty list + String bucketName = "testBucket"; + + // Mock behavior of storageService to return a valid root directory + when(storageService.getRootDir()).thenReturn("/mock/root"); + + // Use Mockito to mock static method Files.write() + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + // Call the method with empty migration status + armadilloStorage.writeMigrationFile(migrationStatus, bucketName); + + // Create the expected path + Path expectedPath = Paths.get("/mock/root", bucketName, "migration-status.txt"); + + // Verify that Files.write is still called with an empty list + mockedFiles.verify( + () -> + Files.write( + eq(expectedPath), + eq(migrationStatus), + eq(java.nio.charset.StandardCharsets.UTF_8))); + } + } + + @Test + public void testWriteMigrationFile_invalidBucket() throws IOException { + // Prepare test data + List migrationStatus = List.of("migration1", "migration2"); + String bucketName = "invalidBucket"; + + // Mock behavior of storageService to return a valid root directory + when(storageService.getRootDir()).thenReturn("/mock/root"); + + // Simulate an invalid bucket by making the path creation fail + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + // Simulate a situation where creating the path causes an exception + mockedFiles + .when(() -> Files.write(any(Path.class), anyList(), any())) + .thenThrow(new IOException("Invalid bucket path")); + + // Call the method + try { + armadilloStorage.writeMigrationFile(migrationStatus, bucketName); + } catch (IOException e) { + assertTrue(e.getMessage().contains("Invalid bucket path")); + } + } + } } From 24ad092ae21f8e3f48ce9163adcc78b2a32490da Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 10 Jan 2025 14:04:42 +0100 Subject: [PATCH 20/83] test: fix tests --- .../storage/ArmadilloStorageServiceTest.java | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java index 6f6a78741..78c6d459f 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/storage/ArmadilloStorageServiceTest.java @@ -56,7 +56,7 @@ class ArmadilloStorageServiceTest { private static final String USER_ID = "very-random-id"; private static final String USER_EMAIL = "user@email.com"; private static final String OLD_BUCKET = "user-very-random-id"; - private static final String NEW_BUCKET = "user-user@email.com"; + private static final String NEW_BUCKET = "user-user__at__email.com"; @MockBean StorageService storageService; @Mock Principal principal; @@ -521,19 +521,31 @@ void testSaveWorkspaceReturnsErrorWhenTooBig() { ArmadilloWorkspace workspaceMock = mock(ArmadilloWorkspace.class); when(storageService.getWorkSpace(is)).thenReturn(workspaceMock); when(workspaceMock.getSize()).thenReturn(123456789123456789L); - assertThrows( - StorageException.class, () -> armadilloStorage.saveWorkspace(is, principal, "test")); + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + assertThrows( + StorageException.class, () -> armadilloStorage.saveWorkspace(is, principal, "test")); + } } @Test void testSaveWorkspaceReturnsErrorWhenBiggerThan2Gbs() { when(storageService.getWorkSpace(is)) .thenThrow(new StorageException(ArmadilloWorkspace.WORKSPACE_TOO_BIG_ERROR)); - try { - armadilloStorage.saveWorkspace(is, principal, "test"); - } catch (StorageException e) { - assertEquals( - "Unable to save workspace. Maximum supported workspace size is 2GB", e.getMessage()); + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + try { + armadilloStorage.saveWorkspace(is, principal, "test"); + } catch (StorageException e) { + assertEquals( + "Unable to save workspace. Maximum supported workspace size is 2GB", e.getMessage()); + } } } @@ -894,15 +906,18 @@ void testListUserWorkspacesNoWorkspacesInBucket() { when(storageService.listObjects(OLD_BUCKET)).thenReturn(List.of(item)); when(principal.getName()).thenReturn(USER_ID); when(item.name()).thenReturn("workspace1.RData"); + when(storageService.getRootDir()).thenReturn("/mock/root"); - try (MockedStatic infoRetriever = - Mockito.mockStatic(UserInformationRetriever.class)) { - infoRetriever - .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) - .thenReturn(USER_EMAIL); - armadilloStorage.moveWorkspacesIfInOldBucket(principal); - verify(storageService, times(1)) - .moveWorkspace(eq(item), eq(principal), eq(OLD_BUCKET), eq(NEW_BUCKET)); + try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + armadilloStorage.moveWorkspacesIfInOldBucket(principal); + verify(storageService, times(1)) + .moveWorkspace(eq(item), eq(principal), eq(OLD_BUCKET), eq(NEW_BUCKET)); + } } } @@ -912,9 +927,15 @@ void testMoveWorkspacesIfInOldBucket_WhenOldBucketDoesNotExist() { when(storageService.bucketExists(OLD_BUCKET)).thenReturn(false); when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); - armadilloStorage.moveWorkspacesIfInOldBucket(principal); + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + armadilloStorage.moveWorkspacesIfInOldBucket(principal); - verify(storageService, never()).listObjects(any()); + verify(storageService, never()).listObjects(any()); + } } // Test case 3: new bucket already exists, so no workspaces should be moved @@ -923,10 +944,16 @@ void testMoveWorkspacesIfInOldBucket_WhenNewBucketExists() { when(storageService.bucketExists(OLD_BUCKET)).thenReturn(true); when(storageService.bucketExists(NEW_BUCKET)).thenReturn(true); - armadilloStorage.moveWorkspacesIfInOldBucket(principal); + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + armadilloStorage.moveWorkspacesIfInOldBucket(principal); - verify(storageService, never()).listObjects(any()); - verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + verify(storageService, never()).listObjects(any()); + verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + } } // Test case 4: old bucket exists, new bucket does not exist, but no workspaces to move @@ -936,9 +963,15 @@ void testMoveWorkspacesIfInOldBucket_WhenOldBucketExistsButNoWorkspacesToMove() when(storageService.bucketExists(NEW_BUCKET)).thenReturn(false); when(storageService.listObjects(OLD_BUCKET)).thenReturn(Collections.emptyList()); - armadilloStorage.moveWorkspacesIfInOldBucket(principal); + try (MockedStatic infoRetriever = + Mockito.mockStatic(UserInformationRetriever.class)) { + infoRetriever + .when(() -> UserInformationRetriever.getUserIdentifierFromPrincipal(principal)) + .thenReturn(USER_EMAIL); + armadilloStorage.moveWorkspacesIfInOldBucket(principal); - verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + verify(storageService, never()).moveWorkspace(any(), any(), any(), any()); + } } // Test case 5: old bucket exists, new bucket does not exist, but workspace with wrong extension @@ -950,6 +983,7 @@ void testMoveWorkspacesIfInOldBucket_WhenOldBucketExistsButNonRDataFiles() { when(storageService.listObjects(OLD_BUCKET)).thenReturn(List.of(item)); when(principal.getName()).thenReturn(USER_ID); when(item.name()).thenReturn("workspace1.txt"); + when(storageService.getRootDir()).thenReturn("/mock/root"); try (MockedStatic infoRetriever = Mockito.mockStatic(UserInformationRetriever.class)) { From 74f5c8947932e80b5d499479cb2cf1532119d7a9 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 10 Jan 2025 15:17:46 +0100 Subject: [PATCH 21/83] test(DataController): add tests --- .../armadillo/controller/DataController.java | 14 +++++--- .../controller/DataControllerTest.java | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java index e68d73c20..17157c9db 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/controller/DataController.java @@ -435,10 +435,7 @@ public void removeWorkspace(@PathVariable String id, Principal principal) { @ResponseStatus(NO_CONTENT) public void removeUserWorkspace( @PathVariable String user, @PathVariable String id, Principal principal) { - if (user.contains("@")) { - user = user.replace("@", "__at__"); - } - String finalUser = user; + String finalUser = getSafeUsernameForFileSystem(user); auditEventPublisher.audit( () -> { storage.removeWorkspaceByStringUserId(finalUser, id); @@ -532,6 +529,15 @@ protected List getLinkedVariables(ArmadilloLinkFile linkFile, String var : variableList.stream().filter(allowedVariables::contains).toList(); } + String getSafeUsernameForFileSystem(String user) { + // replaces the @ in email addresses because when we use it as name of a folder, not all + // filesystems might like it + if (user.contains("@")) { + user = user.replace("@", "__at__"); + } + return user; + } + private CompletableFuture> loadTableFromLinkFile( String project, String objectName, diff --git a/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java b/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java index 3517f5bb7..8c073c4de 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java @@ -50,6 +50,7 @@ import org.obiba.datashield.r.expr.v2.ParseException; import org.rosuda.REngine.REXPDouble; import org.rosuda.REngine.REXPRaw; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -86,6 +87,7 @@ class DataControllerTest extends ArmadilloControllerTestBase { @MockBean private DSEnvironmentCache environments; @Mock private RockResult rexp; @Mock private DSEnvironment assignEnvironment; + @Autowired private DataController dataController; @Test @WithMockUser @@ -360,6 +362,31 @@ void testDeleteWorkspace() throws Exception { Map.of("sessionId", sessionId, "roles", List.of("ROLE_USER"), "id", "test"))); } + @Test + @WithMockUser(roles = "SU", username = "admin") + void testDeleteWorkspaceOfUser() throws Exception { + mockMvc + .perform(delete("/workspaces/henk@email.com/test").session(session)) + .andExpect(status().isNoContent()); + + verify(armadilloStorage).removeWorkspaceByStringUserId("henk__at__email.com", "test"); + + auditEventValidator.validateAuditEvent( + new AuditEvent( + instant, + "admin", + "DELETE_USER_WORKSPACE", + Map.of( + "sessionId", + sessionId, + "roles", + List.of("ROLE_SU"), + "id", + "test", + "user", + "henk@email.com"))); + } + @Test @WithMockUser(username = "henk") void testSaveWorkspace() throws Exception { @@ -1086,4 +1113,10 @@ void testGetMatchedData() { expected.put("RESOURCE", "Blaat"); assertEquals(matchedData, expected); } + + @Test + void testGetSafeUserNameForFile() { + String user = "username@email.com"; + assertEquals("username__at__email.com", dataController.getSafeUsernameForFileSystem(user)); + } } From 1035d0de8c479450bb31dbd938eb88fcb1b7b3e7 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 10 Jan 2025 15:29:25 +0100 Subject: [PATCH 22/83] fix: make delete workspace for specified user SU only --- .../org/molgenis/armadillo/storage/ArmadilloStorageService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java index 29975f6a5..58a356429 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/storage/ArmadilloStorageService.java @@ -342,6 +342,7 @@ public void removeWorkspace(Principal principal, String id) { storageService.delete(getUserBucketName(principal), getWorkspaceObjectName(id)); } + @PreAuthorize("hasAnyRole('ROLE_SU')") public void removeWorkspaceByStringUserId(String userId, String id) { storageService.delete(USER_PREFIX + userId, getWorkspaceObjectName(id)); } From b8ebf58a30f8847d37383a0113ee6cd2574eba98 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Mon, 13 Jan 2025 11:59:11 +0100 Subject: [PATCH 23/83] test: add tests for moveWorkspace method --- .../storage/LocalStorageServiceTest.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/armadillo/src/test/java/org/molgenis/armadillo/storage/LocalStorageServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/storage/LocalStorageServiceTest.java index 742bc4858..f6057ecb3 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/storage/LocalStorageServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/storage/LocalStorageServiceTest.java @@ -14,6 +14,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.Principal; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -22,6 +23,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.molgenis.armadillo.exceptions.IllegalPathException; @@ -34,6 +36,11 @@ class LocalStorageServiceTest { public static final String SOME_OBJECT_PATH = "object/some/path"; // n.b. can be subfolders you see? public static final String SOME_PROJECT = "project"; + public static final String WORKSPACE_NAME = "workspace1"; + + @Mock Principal principal; + + @Mock ObjectMetadata workspaceMetaData; @BeforeEach void beforeEach() throws IOException { @@ -366,4 +373,51 @@ void testGetHumanReadableByteCountGb() { String size = getHumanReadableByteCount(12345678910L); assertEquals("11.5 GB", size); } + + @Test + void testMoveWorkspace() throws IOException { + // Setup test data: Create a workspace in the old bucket + String oldBucketName = "old-bucket"; + String newBucketName = "new-bucket"; + + localStorageService.createBucketIfNotExists(oldBucketName); + + // Create a workspace in the old bucket + localStorageService.save( + new ByteArrayInputStream("workspace content".getBytes()), + oldBucketName, + WORKSPACE_NAME, + MediaType.APPLICATION_OCTET_STREAM); + + // Initialize the mock ObjectMetadata and define its behavior + workspaceMetaData = mock(ObjectMetadata.class); // Ensure workspaceMetaData is not null + when(workspaceMetaData.name()).thenReturn(WORKSPACE_NAME); // Mock name() method + + // Call the moveWorkspace method + localStorageService.moveWorkspace(workspaceMetaData, principal, oldBucketName, newBucketName); + + // Verify both workspaces are there (we don't want to remove the old one in case not everything + // is moved) + assertTrue(localStorageService.objectExists(newBucketName, WORKSPACE_NAME)); + assertTrue(localStorageService.objectExists(oldBucketName, WORKSPACE_NAME)); + } + + @Test + void testMoveWorkspaceFileDoesNotExistInOldBucket() { + // Setup test data: Create workspace in old bucket + String oldBucketName = "old-bucket"; + String newBucketName = "new-bucket"; + + localStorageService.createBucketIfNotExists(newBucketName); + + // Initialize the mock ObjectMetadata and define its behavior + workspaceMetaData = mock(ObjectMetadata.class); // Ensure workspaceMetaData is not null + when(workspaceMetaData.name()).thenReturn(WORKSPACE_NAME); // Mock name() method + + assertThrows( + StorageException.class, + () -> + localStorageService.moveWorkspace( + workspaceMetaData, principal, oldBucketName, newBucketName)); + } } From 2f38ea85a3960f33fae0e8ac49ae33b63de9acb0 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:15:41 +0100 Subject: [PATCH 24/83] feat: added api connection for listing workspaces --- ui/src/api/api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index 14f6207a0..90ac20e14 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -332,3 +332,7 @@ export async function getFreeDiskSpace(): Promise { return Number(data.measurements[0].value); }); } + +export async function getWorkspaceDetails() { + return get(`/all-workspaces`); +} From 8a942ec11a553ca4332c388e967ac09e2a2d9592 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:29:02 +0100 Subject: [PATCH 25/83] tried to add type for workspaces --- ui/src/types/api.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/src/types/api.d.ts b/ui/src/types/api.d.ts index 65c88dab4..bf399a9bc 100644 --- a/ui/src/types/api.d.ts +++ b/ui/src/types/api.d.ts @@ -116,3 +116,9 @@ export type Metric = { }; export type Metrics = Dictionary; + +export type Workspace = { + name: string; + size: number; + lastModified: Date; +}; From 33758a1a1ec1b792f01f1d24dbf4036050f87783 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:29:30 +0100 Subject: [PATCH 26/83] feat: added function to get workspaces --- ui/src/views/Workspaces.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index 9a0a0d65e..a0b86658f 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -72,6 +72,9 @@ import { } from "vue"; import { useRoute, useRouter } from "vue-router"; import DataPreviewTable from "@/components/DataPreviewTable.vue"; +import { Workspace } from "@/types/api"; +import { getWorkspaceDetails } from "@/api/api"; +import { processErrorMessages } from "@/helpers/errorProcessing"; export default defineComponent({ name: "WorkspaceExplorer", @@ -90,19 +93,27 @@ export default defineComponent({ const router = useRouter(); const route = useRoute(); const previewParam = ref(); + const workspaces: Ref = ref([]); onMounted(() => { console.log(workspaceComponent); watch( () => workspaceComponent.value?.selectedItem, (newVal) => { - console.log(newVal); if (newVal != undefined) { emit("selectUser", newVal); selectedUser.value = newVal; } } ); + loadWorkspaces(); }); + const loadWorkspaces = async () => { + workspaces.value = await getWorkspaceDetails().catch((error: string) => { + errorMessage.value = processErrorMessages(error, "workspaces", router); + return []; + }); + console.log("Updated workspace value:", workspaces.value); + }; return { route, router, From 826a5f1d72c8a956915df2a83a57f35dabcad6c5 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:02:17 +0100 Subject: [PATCH 27/83] chore: added workspace endpoint to routing --- ui/vite.config.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/vite.config.js b/ui/vite.config.js index 0e93732c2..c3e678302 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -95,6 +95,13 @@ export default defineConfig({ port: 8080, }, }, + "^/all-workspaces": { + target: { + protocol: "http:", + host: "localhost", + port: 8080, + }, + }, }, }, }); From 1667f7cc742e9b9564609f8d7ad03574cd0fb9ac Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:16:55 +0100 Subject: [PATCH 28/83] feat: view user workspaces --- ui/src/components/DataPreviewTable.vue | 39 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/ui/src/components/DataPreviewTable.vue b/ui/src/components/DataPreviewTable.vue index ff524ee9a..f93c74267 100644 --- a/ui/src/components/DataPreviewTable.vue +++ b/ui/src/components/DataPreviewTable.vue @@ -6,9 +6,12 @@ - + {{ key }} + + {{ toCapitalizedWords(key) }} + @@ -16,7 +19,11 @@ - + {{ value.toString().slice(0, tableHeader[index].length - 2) }}.. @@ -31,29 +38,38 @@ From 65b77e0adb82edc26cbc7c1ade226357c74387f4 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:17:58 +0100 Subject: [PATCH 29/83] fix: correctly detect date as not being an integer --- ui/src/helpers/utils.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index 5b9405092..b6bf70e92 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -80,8 +80,13 @@ export function isInt(itemToCheck: number) { export function isIntArray(listOfItems: StringArray) { let itemIsIntArray = true; listOfItems.forEach((item) => { - const numberToCheck = parseFloat(item); - if (!isInt(numberToCheck)) { + if (!isDate(item)) { + const numberToCheck = parseFloat(item); + if (!isInt(numberToCheck)) { + itemIsIntArray = false; + return; + } + } else { itemIsIntArray = false; return; } @@ -89,6 +94,10 @@ export function isIntArray(listOfItems: StringArray) { return itemIsIntArray; } +export function isDate(item: string) { + return new Date(item) !== "NaN"; +} + export function transformTable(table: { [key: string]: string }[]) { let transformed: { [key: string]: StringArray } = {}; table.forEach((row) => { From c7295b1541870c8b231fd0efd22c9c76621651b5 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:18:36 +0100 Subject: [PATCH 30/83] test: date should not be identified as an integer --- ui/tests/unit/helpers/utils.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/tests/unit/helpers/utils.spec.ts b/ui/tests/unit/helpers/utils.spec.ts index 336529657..52dd64566 100644 --- a/ui/tests/unit/helpers/utils.spec.ts +++ b/ui/tests/unit/helpers/utils.spec.ts @@ -121,7 +121,7 @@ describe("utils", () => { it("should return false for actual float", () => { const actual = isInt(3.01); expect(actual).toBe(false); - }); + }) }); describe("isIntArray", () => { @@ -137,6 +137,14 @@ describe("utils", () => { const actual = isIntArray(["1", "1", "2"]); expect(actual).toBe(true); }); + it("should return false for string array with dates", () => { + const actual = isIntArray(["2024-12-05T12:27:49.107+01:00", "2024-12-05T12:27:49.107+01:00", "2024-12-05T12:27:49.107+01:00"]); + expect(actual).toBe(false); + }); + it("should return false for string array with strings", () => { + const actual = isIntArray(["test1", "test2", "test3"]); + expect(actual).toBe(false); + }); }); describe("transformTable", () => { From b2fda822f2ae024ffcb62d13ffedd19129b14281 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:19:39 +0100 Subject: [PATCH 31/83] changed return type --- ui/src/api/api.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index 90ac20e14..624ecd051 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -154,7 +154,6 @@ async function getMetrics(): Promise { if (data.hasOwnProperty("names")) { return data.names; } else { - console.log("No names found in the data"); return []; } }) @@ -333,6 +332,6 @@ export async function getFreeDiskSpace(): Promise { }); } -export async function getWorkspaceDetails() { - return get(`/all-workspaces`); +export async function getWorkspaceDetails(): Promise { + return get("/all-workspaces"); } From 41fbd6323396eb1fc95059431865dd47a760d0f6 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:20:29 +0100 Subject: [PATCH 32/83] workspace modified date changed to string --- ui/src/types/api.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/types/api.d.ts b/ui/src/types/api.d.ts index bf399a9bc..20bd4474d 100644 --- a/ui/src/types/api.d.ts +++ b/ui/src/types/api.d.ts @@ -120,5 +120,5 @@ export type Metrics = Dictionary; export type Workspace = { name: string; size: number; - lastModified: Date; + lastModified: string; }; From f183a55a83b0c559b8ec9466512e1cc901c0d792 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:21:04 +0100 Subject: [PATCH 33/83] added workspaces tab --- ui/src/router.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/src/router.ts b/ui/src/router.ts index c0d53d987..ef5c64a34 100644 --- a/ui/src/router.ts +++ b/ui/src/router.ts @@ -5,6 +5,7 @@ import Users from "@/views/Users.vue"; import Profiles from "@/views/Profiles.vue"; import Login from "@/views/Login.vue"; import Insight from "./views/Insight.vue"; +import Workspaces from "./views/Workspaces.vue"; const routes = [ { @@ -16,6 +17,11 @@ const routes = [ name: "users", component: Users, }, + { + path: "/workspaces", + name: "workspaces", + component: Workspaces, + }, { path: "/projects", name: "projects", From 19f91189e6dc61e0ea38206cf99effc377b7f777 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:21:46 +0100 Subject: [PATCH 34/83] pass workspace data per active user --- ui/src/views/Workspaces.vue | 80 ++++--------------------------------- 1 file changed, 7 insertions(+), 73 deletions(-) diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index a0b86658f..40fecb6cc 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -43,11 +43,12 @@ >
- +
@@ -95,7 +96,6 @@ export default defineComponent({ const previewParam = ref(); const workspaces: Ref = ref([]); onMounted(() => { - console.log(workspaceComponent); watch( () => workspaceComponent.value?.selectedItem, (newVal) => { @@ -112,7 +112,6 @@ export default defineComponent({ errorMessage.value = processErrorMessages(error, "workspaces", router); return []; }); - console.log("Updated workspace value:", workspaces.value); }; return { route, @@ -120,76 +119,11 @@ export default defineComponent({ errorMessage, previewParam, selectedUser, + workspaces, }; }, data() { return { - workspaces: { - "user-1bd26c22-4a9a-480a-80e7-6acbed34bc05": [ - { - name: "cohort_2:test_workspace_2", - size: 172681, - lastModified: "2024-12-05T12:27:49.107+01:00", - }, - { - name: "cohort_1:test_workspace_2", - size: 174014, - lastModified: "2024-12-05T12:27:48.877+01:00", - }, - { - name: "cohort_2:test_workspace_1", - size: 172681, - lastModified: "2024-12-05T12:27:37.2+01:00", - }, - { - name: "cohort_2:test_workspace_3", - size: 172681, - lastModified: "2024-12-05T12:27:52.65+01:00", - }, - { - name: "cohort_1:test_workspace_3", - size: 174014, - lastModified: "2024-12-05T12:27:52.418+01:00", - }, - { - name: "cohort_1:test_workspace_1", - size: 174014, - lastModified: "2024-12-05T12:27:36.963+01:00", - }, - ], - "user-e6de84fa-d08d-43e4-9558-bb9fba1528b9": [ - { - name: "cohort_2:test_workspace_2", - size: 172681, - lastModified: "2024-12-05T12:27:49.107+01:00", - }, - { - name: "cohort_1:test_workspace_2", - size: 174014, - lastModified: "2024-12-05T12:27:48.877+01:00", - }, - { - name: "cohort_2:test_workspace_1", - size: 172681, - lastModified: "2024-12-05T12:27:37.2+01:00", - }, - { - name: "cohort_2:test_workspace_3", - size: 172681, - lastModified: "2024-12-05T12:27:52.65+01:00", - }, - { - name: "cohort_1:test_workspace_3", - size: 174014, - lastModified: "2024-12-05T12:27:52.418+01:00", - }, - { - name: "cohort_1:test_workspace_1", - size: 174014, - lastModified: "2024-12-05T12:27:36.963+01:00", - }, - ], - }, loading: false, }; }, From 87977617337fed279c83c6d544771c4bddea44eb Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:09:31 +0100 Subject: [PATCH 35/83] format workspace data --- ui/src/views/Workspaces.vue | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index 40fecb6cc..c0d6cf25b 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -45,7 +45,7 @@
@@ -62,7 +62,7 @@ import ListGroup from "@/components/ListGroup.vue"; import LoadingSpinner from "@/components/LoadingSpinner.vue"; import FeedbackMessage from "@/components/FeedbackMessage.vue"; import {} from "@/api/api"; -import {} from "@/helpers/utils"; +import { convertBytes } from "@/helpers/utils"; import { defineComponent, onMounted, @@ -131,5 +131,19 @@ export default defineComponent({ deleteUserWorkspace() {}, showSelectedUser() {}, }, + computed: { + formattedWorkspaces() { + console.log(typeof this.workspaces); + return Object.entries(this.workspaces).reduce((result, [userId, workspaces]) => { + result[userId] = workspaces.map(workspace => ({ + name: workspace.name, + size: convertBytes(workspace.size), + lastModified: Date(workspace.lastModified), + })); + return result; + }, {}); + } +} +, }); From a0b01b668929d85cbcf9658bf4b5892409e48342 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:09:56 +0100 Subject: [PATCH 36/83] added connection for delete workspaces --- ui/src/api/api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index 624ecd051..068223861 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -335,3 +335,7 @@ export async function getFreeDiskSpace(): Promise { export async function getWorkspaceDetails(): Promise { return get("/all-workspaces"); } + +export async function deleteUserWorkspace(idWorkspace: string) { + return delete_("/workspaces", idWorkspace); +} From 8d96b13ac4c886ccfa72b8f9aab4250eb6191f6d Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:12:06 +0100 Subject: [PATCH 37/83] fix: correctly detect dates in iso format --- ui/src/helpers/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index b6bf70e92..9971fecea 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -94,8 +94,9 @@ export function isIntArray(listOfItems: StringArray) { return itemIsIntArray; } -export function isDate(item: string) { - return new Date(item) !== "NaN"; +function isDate(item: string) { + const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2}|Z)$/; + return iso8601Regex.test(item); } export function transformTable(table: { [key: string]: string }[]) { From 90ecac795d5c7ce0518fa3acf8d060cbb30ff0ef Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:50:01 +0100 Subject: [PATCH 38/83] moved to correct file, added type for object of workspaces --- ui/src/types/api.d.ts | 6 ------ ui/src/types/types.d.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/src/types/api.d.ts b/ui/src/types/api.d.ts index 20bd4474d..65c88dab4 100644 --- a/ui/src/types/api.d.ts +++ b/ui/src/types/api.d.ts @@ -116,9 +116,3 @@ export type Metric = { }; export type Metrics = Dictionary; - -export type Workspace = { - name: string; - size: number; - lastModified: string; -}; diff --git a/ui/src/types/types.d.ts b/ui/src/types/types.d.ts index 2ac2cee17..3e9fbfe9e 100644 --- a/ui/src/types/types.d.ts +++ b/ui/src/types/types.d.ts @@ -130,3 +130,11 @@ export type ViewEditorData = { srcFolder: string; formValidated: boolean; }; + +export type Workspace = { + name: string; + size: number; + lastModified: string; +}; + +export type Workspaces = Record; From 48bf3f0b504ec2b29865fc679a37510d2821ef14 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:50:33 +0100 Subject: [PATCH 39/83] corrected type of object returned by workspace api call --- ui/src/api/api.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index 068223861..03e261e5e 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -21,6 +21,7 @@ import { ObjectWithStringKey, StringArray, ListOfObjectsWithStringKey, + Workspaces } from "@/types/types"; import { APISettings } from "./config"; @@ -332,10 +333,10 @@ export async function getFreeDiskSpace(): Promise { }); } -export async function getWorkspaceDetails(): Promise { +export async function getWorkspaceDetails(): Promise { return get("/all-workspaces"); } -export async function deleteUserWorkspace(idWorkspace: string) { - return delete_("/workspaces", idWorkspace); +export async function deleteUserWorkspace(deletePath: string) { + return delete_("/workspaces", deletePath); } From edd5e17db81e75f4122f308874dc1412d6f963eb Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:50:58 +0100 Subject: [PATCH 40/83] function to delete all workspaces per user --- ui/src/views/Workspaces.vue | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index c0d6cf25b..e5477e02c 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -73,9 +73,9 @@ import { } from "vue"; import { useRoute, useRouter } from "vue-router"; import DataPreviewTable from "@/components/DataPreviewTable.vue"; -import { Workspace } from "@/types/api"; -import { getWorkspaceDetails } from "@/api/api"; +import { getWorkspaceDetails, deleteUserWorkspace } from "@/api/api"; import { processErrorMessages } from "@/helpers/errorProcessing"; +import { Workspaces } from "@/types/types"; export default defineComponent({ name: "WorkspaceExplorer", @@ -94,7 +94,7 @@ export default defineComponent({ const router = useRouter(); const route = useRoute(); const previewParam = ref(); - const workspaces: Ref = ref([]); + const workspaces: Ref = ref([]); onMounted(() => { watch( () => workspaceComponent.value?.selectedItem, @@ -128,7 +128,13 @@ export default defineComponent({ }; }, methods: { - deleteUserWorkspace() {}, + deleteAllWorkspaces(workspaces: Workspaces, selectedUser: string) { + const userWorkspaces = workspaces[selectedUser]; + userWorkspaces.forEach((workspace) => { + const deletepath = selectedUser.replace("user-", "") + workspace.name + deleteUserWorkspace(deletepath); + }) + }, showSelectedUser() {}, }, computed: { From 80a7ed0d4475b406825ce8a006d7ffc35bbd2f4f Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:46:05 +0100 Subject: [PATCH 41/83] added type for formatted workspaces --- ui/src/types/types.d.ts | 8 ++++++++ ui/src/views/Workspaces.vue | 15 +++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ui/src/types/types.d.ts b/ui/src/types/types.d.ts index 3e9fbfe9e..3962880c7 100644 --- a/ui/src/types/types.d.ts +++ b/ui/src/types/types.d.ts @@ -138,3 +138,11 @@ export type Workspace = { }; export type Workspaces = Record; + +export type FormattedWorkspace = { + name: string; + size: string; + lastModified: Date; +}; + +export type FormattedWorkspaces = Record; diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index e5477e02c..fc0dfce94 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -24,7 +24,7 @@ @@ -75,7 +75,7 @@ import { useRoute, useRouter } from "vue-router"; import DataPreviewTable from "@/components/DataPreviewTable.vue"; import { getWorkspaceDetails, deleteUserWorkspace } from "@/api/api"; import { processErrorMessages } from "@/helpers/errorProcessing"; -import { Workspaces } from "@/types/types"; +import { FormattedWorkspaces, Workspace, Workspaces } from "@/types/types"; export default defineComponent({ name: "WorkspaceExplorer", @@ -94,7 +94,7 @@ export default defineComponent({ const router = useRouter(); const route = useRoute(); const previewParam = ref(); - const workspaces: Ref = ref([]); + const workspaces: Ref = ref({}); onMounted(() => { watch( () => workspaceComponent.value?.selectedItem, @@ -110,7 +110,7 @@ export default defineComponent({ const loadWorkspaces = async () => { workspaces.value = await getWorkspaceDetails().catch((error: string) => { errorMessage.value = processErrorMessages(error, "workspaces", router); - return []; + return {}; }); }; return { @@ -139,12 +139,11 @@ export default defineComponent({ }, computed: { formattedWorkspaces() { - console.log(typeof this.workspaces); - return Object.entries(this.workspaces).reduce((result, [userId, workspaces]) => { - result[userId] = workspaces.map(workspace => ({ + return Object.entries(this.workspaces).reduce((result: FormattedWorkspaces, [userId, workspaces]) => { + result[userId] = workspaces.map((workspace: Workspace) => ({ name: workspace.name, size: convertBytes(workspace.size), - lastModified: Date(workspace.lastModified), + lastModified: new Date(workspace.lastModified), })); return result; }, {}); From ad33c95f6aa3072c43648f25931a9a9f8880fb93 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:43:33 +0100 Subject: [PATCH 42/83] corrected delete workspace api function --- ui/src/api/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index 03e261e5e..4db5cae8c 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -337,6 +337,6 @@ export async function getWorkspaceDetails(): Promise { return get("/all-workspaces"); } -export async function deleteUserWorkspace(deletePath: string) { - return delete_("/workspaces", deletePath); +export async function deleteUserWorkspace(user: string, workspace: string) { + return delete_("/workspaces" ,`${user}/${workspace}`) } From ea4c41f57191e6206ac0f0077cb3d8e45c385798 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:43:50 +0100 Subject: [PATCH 43/83] refactored delete workspace --- ui/src/views/Workspaces.vue | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index fc0dfce94..3cd14d960 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -24,7 +24,7 @@ @@ -128,11 +128,15 @@ export default defineComponent({ }; }, methods: { - deleteAllWorkspaces(workspaces: Workspaces, selectedUser: string) { - const userWorkspaces = workspaces[selectedUser]; + deleteWorkspace(workspaceName: string) { + deleteUserWorkspace(this.selectedUser.replace("user-", ""), workspaceName).catch( error => { + this.errorMessage = error + }) + }, + deleteAllWorkspaces() { + const userWorkspaces = this.workspaces[this.selectedUser]; userWorkspaces.forEach((workspace) => { - const deletepath = selectedUser.replace("user-", "") + workspace.name - deleteUserWorkspace(deletepath); + this.deleteWorkspace(workspace.name) }) }, showSelectedUser() {}, From 3687fd34f1fdd919ef823487a0660ff1f5159aee Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:44:08 +0100 Subject: [PATCH 44/83] added delete to 8080 --- ui/vite.config.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/vite.config.js b/ui/vite.config.js index c3e678302..e5a3ed435 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -102,6 +102,13 @@ export default defineConfig({ port: 8080, }, }, + "^/workspaces": { + target: { + protocol: "http:", + host: "localhost", + port: 8080, + }, + } }, }, }); From 0f9cedda1da54f22ae0a5004fa715b630d9cce05 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Fri, 24 Jan 2025 14:51:13 +0100 Subject: [PATCH 45/83] chore(ui): show icon for workspace tab --- ui/src/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/App.vue b/ui/src/App.vue index c814e1b83..39ada91f9 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -125,7 +125,7 @@ export default defineComponent({ tabIcons: [ "clipboard2-data", "people-fill", - "bi-person-workspace", + "person-workspace", "shield-shaded", "brilliance", ], From 15d0574dbdf3be02a34fcb8f4577bb0732512cfb Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Mon, 27 Jan 2025 13:30:08 +0100 Subject: [PATCH 46/83] chore: add checkboxes to delete workspaces and fix confirmation dialog --- ui/src/components/ConfirmationDialog.vue | 7 +- ui/src/components/DataPreviewTable.vue | 3 + ui/src/components/ListGroup.vue | 3 + ui/src/helpers/utils.ts | 3 +- ui/src/types/types.d.ts | 1 + ui/src/views/Workspaces.vue | 133 +++++++++++++++++------ 6 files changed, 115 insertions(+), 35 deletions(-) diff --git a/ui/src/components/ConfirmationDialog.vue b/ui/src/components/ConfirmationDialog.vue index 40de58913..2a4dde563 100644 --- a/ui/src/components/ConfirmationDialog.vue +++ b/ui/src/components/ConfirmationDialog.vue @@ -6,7 +6,8 @@ @clear="$emit('cancel')" >
- Are you sure you want to {{ action }} {{ recordType }} [{{ record }}]? + Are you sure you want to {{ action }} {{ recordType }} [{{ record }}] + {{ additionalMessage }}?
{{ extraInfo }} @@ -42,6 +43,10 @@ export default { record: String, action: String, recordType: String, + additionalMessage: { + type: String, + default: "", + }, extraInfo: { type: String, default: "", diff --git a/ui/src/components/DataPreviewTable.vue b/ui/src/components/DataPreviewTable.vue index f93c74267..458528938 100644 --- a/ui/src/components/DataPreviewTable.vue +++ b/ui/src/components/DataPreviewTable.vue @@ -6,6 +6,7 @@ + {{ key }} @@ -17,6 +18,8 @@ + + , required: true }, rowIcon: { type: String, required: true }, @@ -61,6 +62,8 @@ export default { }, methods: { toggleSelectedItem(newItem: string) { + console.log("listgroup", newItem); + this.$emit("selectItem", newItem); if (this.selectedItem !== newItem) { this.selectedItem = newItem; } else { diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index 9971fecea..3be305866 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -95,7 +95,8 @@ export function isIntArray(listOfItems: StringArray) { } function isDate(item: string) { - const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2}|Z)$/; + const iso8601Regex = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2}|Z)$/; return iso8601Regex.test(item); } diff --git a/ui/src/types/types.d.ts b/ui/src/types/types.d.ts index 3962880c7..59d7a71f5 100644 --- a/ui/src/types/types.d.ts +++ b/ui/src/types/types.d.ts @@ -135,6 +135,7 @@ export type Workspace = { name: string; size: number; lastModified: string; + checked?: boolean; }; export type Workspaces = Record; diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index 3cd14d960..1af9dfa47 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -6,15 +6,16 @@ --> + recordType="workspaces" + :additionalMessage="`for user [${selectedUser}]`" + @proceed="() => {}" + @cancel="clearIsDeleteTriggered" + >
@@ -24,7 +25,7 @@ @@ -33,6 +34,7 @@
- +
+ + + + + +
@@ -99,7 +128,7 @@ export default defineComponent({ watch( () => workspaceComponent.value?.selectedItem, (newVal) => { - if (newVal != undefined) { + if (newVal != undefined && newVal !== "") { emit("selectUser", newVal); selectedUser.value = newVal; } @@ -125,34 +154,72 @@ export default defineComponent({ data() { return { loading: false, + isDeleteTriggered: false, + userWorkspaces: [] as Workspace[], }; }, methods: { + getIndexOfWorkspace(selectedWorkspaceName: string) { + return this.userWorkspaces.findIndex((workspace) => { + return workspace.name === selectedWorkspaceName; + }); + }, + setWorkspaces(user: string) { + this.resetWorkspacesToDelete(); + this.userWorkspaces = this.workspaces[user].map((ws) => { + ws["checked"] = false; + return ws; + }); + }, + setIsDeleteTriggered() { + this.isDeleteTriggered = true; + }, + clearIsDeleteTriggered() { + this.isDeleteTriggered = false; + }, + resetWorkspacesToDelete() { + this.workspacesToDelete = []; + }, + toggleWorkspacesToDelete(selectedWS: Workspace) { + const selectedWorkspaceName = selectedWS.name; + const index = this.getIndexOfWorkspace(selectedWorkspaceName); + this.userWorkspaces[index]["checked"] = true; + }, deleteWorkspace(workspaceName: string) { - deleteUserWorkspace(this.selectedUser.replace("user-", ""), workspaceName).catch( error => { - this.errorMessage = error - }) + deleteUserWorkspace( + this.selectedUser.replace("user-", ""), + workspaceName + ).catch((error) => { + this.errorMessage = error; + }); }, deleteAllWorkspaces() { const userWorkspaces = this.workspaces[this.selectedUser]; userWorkspaces.forEach((workspace) => { - this.deleteWorkspace(workspace.name) - }) - }, + this.deleteWorkspace(workspace.name); + }); + }, showSelectedUser() {}, }, computed: { - formattedWorkspaces() { - return Object.entries(this.workspaces).reduce((result: FormattedWorkspaces, [userId, workspaces]) => { - result[userId] = workspaces.map((workspace: Workspace) => ({ - name: workspace.name, - size: convertBytes(workspace.size), - lastModified: new Date(workspace.lastModified), - })); - return result; - }, {}); - } -} -, + workspacesToDelete() { + return this.userWorkspaces + .filter((ws) => ws.checked) + .map((ws) => ws.name); + }, + formattedWorkspaces() { + return Object.entries(this.workspaces).reduce( + (result: FormattedWorkspaces, [userId, workspaces]) => { + result[userId] = workspaces.map((workspace: Workspace) => ({ + name: workspace.name, + size: convertBytes(workspace.size), + lastModified: new Date(workspace.lastModified), + })); + return result; + }, + {} + ); + }, + }, }); From 49afc5852aa64d6ec41ac4b702911d486eeb7c86 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Mon, 27 Jan 2025 14:33:44 +0100 Subject: [PATCH 47/83] chore: cleanup things --- ui/src/api/api.ts | 4 ++-- ui/src/components/ListGroup.vue | 1 - ui/src/views/Workspaces.vue | 14 +------------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index 4db5cae8c..21d6b5fe3 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -21,7 +21,7 @@ import { ObjectWithStringKey, StringArray, ListOfObjectsWithStringKey, - Workspaces + Workspaces, } from "@/types/types"; import { APISettings } from "./config"; @@ -338,5 +338,5 @@ export async function getWorkspaceDetails(): Promise { } export async function deleteUserWorkspace(user: string, workspace: string) { - return delete_("/workspaces" ,`${user}/${workspace}`) + return delete_("/workspaces", `${user}/${workspace}`); } diff --git a/ui/src/components/ListGroup.vue b/ui/src/components/ListGroup.vue index 514ba90f6..708773460 100644 --- a/ui/src/components/ListGroup.vue +++ b/ui/src/components/ListGroup.vue @@ -62,7 +62,6 @@ export default { }, methods: { toggleSelectedItem(newItem: string) { - console.log("listgroup", newItem); this.$emit("selectItem", newItem); if (this.selectedItem !== newItem) { this.selectedItem = newItem; diff --git a/ui/src/views/Workspaces.vue b/ui/src/views/Workspaces.vue index 1af9dfa47..f0fd5ee7c 100644 --- a/ui/src/views/Workspaces.vue +++ b/ui/src/views/Workspaces.vue @@ -62,11 +62,10 @@