Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: #751 show all workspaces to admin #820

Merged
merged 88 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
3fca942
feat: add GET all-workspaces for admin user
marikaris Dec 5, 2024
bb10001
chore: updated vue version
timcadman Dec 6, 2024
aad3485
feat: added workspace page
timcadman Dec 6, 2024
ca5a903
feat: added workspace tab
timcadman Dec 6, 2024
cb607a4
feat: workspace directory now has user email instead of untracable UUID
marikaris Dec 10, 2024
e31aec2
chore: remove moved tests
marikaris Dec 10, 2024
76ce1ac
test: add tests
marikaris Dec 10, 2024
d7235a5
test(ArmadilloStorageService): add tests for listAllUserWorkspaces
marikaris Dec 10, 2024
c795630
refactor(ArmadilloStorageService): split up move workspace function
marikaris Dec 10, 2024
59bd184
refactor: make more suitable for testing
marikaris Dec 12, 2024
7776b49
test(ArmadilloStorageService): add tests
marikaris Dec 12, 2024
2a3daad
refactor: cleanup code
marikaris Dec 12, 2024
308cd65
test(UserInformationRetriever): add tests
marikaris Dec 12, 2024
3e10992
feat: delete workspace for user
marikaris Dec 13, 2024
f386c65
chore: remove invalid validation
marikaris Dec 13, 2024
f87610e
chore: replace @ in userfoldername with __at__ to make sure we dont r…
marikaris Jan 7, 2025
0c9e30e
chore: change statuscode in delete endpoint and if email address is p…
marikaris Jan 7, 2025
a5c8dff
feat: write file with migration status in migrated workspace user bucket
marikaris Jan 9, 2025
d9584cd
test(ArmadilloStorageService): add tests for migration status file
marikaris Jan 10, 2025
24ad092
test: fix tests
marikaris Jan 10, 2025
74f5c89
test(DataController): add tests
marikaris Jan 10, 2025
1035d0d
fix: make delete workspace for specified user SU only
marikaris Jan 10, 2025
b8ebf58
test: add tests for moveWorkspace method
marikaris Jan 13, 2025
5d45661
Merge branch 'master' into feat/#751-show-all-workspaces-to-admin
marikaris Jan 13, 2025
2f38ea8
feat: added api connection for listing workspaces
timcadman Jan 14, 2025
8a942ec
tried to add type for workspaces
timcadman Jan 14, 2025
33758a1
feat: added function to get workspaces
timcadman Jan 14, 2025
826a5f1
chore: added workspace endpoint to routing
timcadman Jan 20, 2025
1667f7c
feat: view user workspaces
timcadman Jan 20, 2025
65b77e0
fix: correctly detect date as not being an integer
timcadman Jan 20, 2025
c7295b1
test: date should not be identified as an integer
timcadman Jan 20, 2025
b2fda82
changed return type
timcadman Jan 20, 2025
41fbd63
workspace modified date changed to string
timcadman Jan 20, 2025
f183a55
added workspaces tab
timcadman Jan 20, 2025
19f9118
pass workspace data per active user
timcadman Jan 20, 2025
8797761
format workspace data
timcadman Jan 21, 2025
a0b01b6
added connection for delete workspaces
timcadman Jan 21, 2025
8d96b13
fix: correctly detect dates in iso format
timcadman Jan 21, 2025
90ecac7
moved to correct file, added type for object of workspaces
timcadman Jan 23, 2025
48bf3f0
corrected type of object returned by workspace api call
timcadman Jan 23, 2025
edd5e17
function to delete all workspaces per user
timcadman Jan 23, 2025
80a7ed0
added type for formatted workspaces
timcadman Jan 24, 2025
ad33c95
corrected delete workspace api function
timcadman Jan 24, 2025
ea4c41f
refactored delete workspace
timcadman Jan 24, 2025
3687fd3
added delete to 8080
timcadman Jan 24, 2025
0f9cedd
chore(ui): show icon for workspace tab
marikaris Jan 24, 2025
15d0574
chore: add checkboxes to delete workspaces and fix confirmation dialog
marikaris Jan 27, 2025
49afc58
chore: cleanup things
marikaris Jan 27, 2025
5f8918e
collect errors and display them
timcadman Jan 27, 2025
d9ac63f
try: add 'all workspaces' summary
timcadman Jan 27, 2025
c09e2e7
Merge branch 'master' into feat/#751-show-all-workspaces-to-admin
marikaris Jan 28, 2025
7470391
refactor: refactored all workspaces function
timcadman Jan 28, 2025
3306a58
sort users alphabetically
timcadman Jan 28, 2025
74702d0
updated workspace type to include user
timcadman Jan 28, 2025
49f4544
Display all users and only show used column
timcadman Jan 28, 2025
34af9c9
fix: don't select two workspaces when checking in the all-workspaces …
timcadman Jan 30, 2025
735bb78
chore: make table of workspaces sortable
marikaris Jan 31, 2025
c818323
fix booboo when isAscending is undefined should pretend like its true
marikaris Jan 31, 2025
63de548
only return workspaces and migration-status file in all-workspaces en…
marikaris Jan 31, 2025
c9d11c9
sort workspaces on workspace name
marikaris Feb 3, 2025
5cb17e2
cleanup UI for workspaces
marikaris Feb 3, 2025
6cbdc45
feat: get migration status endpoint
marikaris Feb 7, 2025
006f908
fix code smells
marikaris Feb 7, 2025
a26ecd9
fix typing
marikaris Feb 7, 2025
903d498
change regex to hopefully appease sonar
marikaris Feb 7, 2025
8f9acca
refactor: rewrite regex part
marikaris Feb 7, 2025
665d3b6
fix filename
marikaris Feb 7, 2025
b998ad4
hopefully appease sonar with regex
marikaris Feb 7, 2025
dbe0170
test: fix testdata
marikaris Feb 7, 2025
0e9638d
test: increase coverage
marikaris Feb 7, 2025
c3b99ec
make regex less greedy
marikaris Feb 7, 2025
f5e4584
refactor: get rid of impossible regex
marikaris Feb 7, 2025
37fe0c4
feat: endpoint for copying file from one workspace folder to another
marikaris Feb 11, 2025
f74a3df
refactor: prefix migration file endpoint with 'workspaces'
marikaris Feb 13, 2025
e82db3b
refactor: rename directories in API output of migration file endpoint…
marikaris Feb 13, 2025
443d43c
feat: show migration file information and copy when unsuccesful
marikaris Feb 13, 2025
e187794
feat: add endpoint for deleting entire user workspace directory
marikaris Feb 13, 2025
e2d03be
feat: make delete workspace button when migration succesful functional
marikaris Feb 13, 2025
04ac4dd
refactor: instead of copy move workspaces and remove all endpoints to…
marikaris Feb 14, 2025
f4f4220
test: add test for delete workspace directory endpoint
marikaris Feb 14, 2025
42a1749
feat: delete user workspace directory
marikaris Feb 17, 2025
d6aa547
Merge branch 'master' into feat/#751-show-all-workspaces-to-admin
marikaris Feb 17, 2025
b7be024
chore: fix typing
marikaris Feb 17, 2025
a7fdd82
test: add first test for workspaces
marikaris Feb 17, 2025
4e3abce
return 404 when object doensnt exist when deleting
marikaris Feb 18, 2025
f162b00
Merge branch 'master' into feat/#751-show-all-workspaces-to-admin
marikaris Feb 18, 2025
0efa460
refactor: rename static method to minimise code changes
marikaris Feb 18, 2025
f5cd54a
Merge branch 'master' into feat/#751-show-all-workspaces-to-admin
marikaris Feb 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@
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;
import org.springframework.context.ApplicationEventPublisherAware;
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
Expand All @@ -42,7 +38,12 @@ 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 GET_MIGRATION_STATUS = "GET_MIGRATION_STATUS";
public static final String DELETE_USER_WORKSPACE = "DELETE_USER_WORKSPACE";
public static final String DELETE_USER_WORKSPACE_DIRECTORY = "DELETE_USER_WORKSPACE_DIRECTORY";
public static final String USER_WORKSPACE_DIRECTORY = "USER_WORKSPACE_DIRECTORY";
public static final String COPY_USER_WORKSPACE = "COPY_USER_WORKSPACE";
public static final String SAVE_USER_WORKSPACE = "SAVE_USER_WORKSPACE";
public static final String LOAD_USER_WORKSPACE = "LOAD_USER_WORKSPACE";
public static final String PERMISSIONS_LIST = "PERMISSIONS_LIST";
Expand Down Expand Up @@ -96,6 +97,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;
Expand All @@ -119,31 +121,11 @@ public void audit(
Map<String, Object> 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<String, Object> data) {
audit(principal, type, data, MDC.get(MDC_SESSION_ID), getRoles());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -29,7 +28,8 @@ public void onApplicationEvent(@NotNull AbstractAuthenticationEvent event) {
private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
publish(
new AuditEvent(
getUser(event.getAuthentication().getPrincipal()),
UserInformationRetriever.getUserIdentifierFromPrincipal(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waarom heb je deze nieuwe functie niet ook gewoon getUser genoemd? Dat had flink wat code changes verborgen ;-)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goede vraag. Ga ik aanpassen haha

event.getAuthentication().getPrincipal()),
AUTHENTICATION_SUCCESS,
Map.of(
"details",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.molgenis.armadillo.command.ArmadilloCommandDTO;
import org.molgenis.armadillo.command.Commands;
import org.molgenis.armadillo.exceptions.ExpressionException;
import org.molgenis.armadillo.exceptions.StorageException;
import org.molgenis.armadillo.exceptions.UnknownObjectException;
import org.molgenis.armadillo.exceptions.UnknownVariableException;
import org.molgenis.armadillo.model.Workspace;
Expand All @@ -46,6 +47,7 @@
import org.molgenis.r.model.RPackage;
import org.obiba.datashield.core.DSMethod;
import org.rosuda.REngine.REXPMismatchException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -406,20 +408,21 @@ public List<Workspace> 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<String, List<Workspace>> getAllUserWorkspaces(Principal principal) {
return auditEventPublisher.audit(
storage::listAllUserWorkspaces, principal, GET_ALL_USER_WORKSPACES, Map.of());
}

@Operation(
summary = "Delete user workspace",
responses = {
@ApiResponse(responseCode = "200", description = "Workspace was removed or did not exist.")
})
@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);
Expand All @@ -430,6 +433,58 @@ public void removeWorkspace(
Map.of(ID, id));
}

@Operation(
summary = "Delete workspace of specific user",
responses = {
@ApiResponse(responseCode = "204", description = "Workspace was removed."),
@ApiResponse(responseCode = "404", description = "Workspace does not exist."),
@ApiResponse(responseCode = "401", description = "Permission denied.")
})
@DeleteMapping(value = "/workspaces/{user}/{id}")
@ResponseStatus(NO_CONTENT)
public void removeUserWorkspace(
@PathVariable String user, @PathVariable String id, Principal principal) {
String finalUser = getSafeUsernameForFileSystem(user);
auditEventPublisher.audit(
() -> {
try {
storage.removeWorkspaceByStringUserId(finalUser, id);
return null;
} catch (StorageException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
}
},
principal,
DELETE_USER_WORKSPACE,
Map.of(ID, id, USER, user));
}

@Operation(
summary = "Delete workspace directory of specific user",
responses = {
@ApiResponse(responseCode = "204", description = "Workspace directory was removed."),
@ApiResponse(responseCode = "404", description = "Workspace directory does not exist."),
@ApiResponse(responseCode = "401", description = "Permission denied.")
})
@DeleteMapping(value = "/workspaces/directory/{userDirectory}")
@ResponseStatus(NO_CONTENT)
public void removeUserWorkspacesDirectory(
@PathVariable String userDirectory, Principal principal) {
String finalUserDirectory = getSafeUsernameForFileSystem(userDirectory);
auditEventPublisher.audit(
() -> {
try {
storage.deleteDirectory(finalUserDirectory);
return null;
} catch (StorageException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
}
},
principal,
DELETE_USER_WORKSPACE_DIRECTORY,
Map.of(USER_WORKSPACE_DIRECTORY, finalUserDirectory));
}

@Operation(summary = "Save user workspace")
@PostMapping(value = "/workspaces/{id}", produces = TEXT_PLAIN_VALUE)
@ResponseStatus(CREATED)
Expand Down Expand Up @@ -513,6 +568,15 @@ protected List<String> getLinkedVariables(ArmadilloLinkFile linkFile, String var
: variableList.stream().filter(allowedVariables::contains).toList();
}

public static 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<ResponseEntity<Void>> loadTableFromLinkFile(
String project,
String objectName,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading