From d7c81a2ba06ced1c6cc8ac25b357896b4ab78817 Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Wed, 24 Jun 2026 09:28:44 -0400 Subject: [PATCH 1/2] feat: Add GDPR endpoint to openvsx admin API Adds a GDPR endpoint to the OpenVSX API that allows for admin level tokens to request for users to be forgotten. Rather than deleting users, this request will scrub all PII from the existing user records to erase any potential leak of private information. This is done to best retain things like reviews and other user actions within the system while abiding by GDPR requirements. These deletion requests do not remove or modify any of the internal logging that already exists as there is a business need to retain that type of data, which will have it's own retention policy that will eventually purge those records. Assisted-by: Claude Sonnet 4.6 --- .../org/eclipse/openvsx/admin/AdminAPI.java | 54 ++++++ .../eclipse/openvsx/admin/AdminService.java | 103 ++++++++++ .../ExtensionVersionRepository.java | 2 + .../repositories/RepositoryService.java | 8 + .../repositories/UserDataRepository.java | 2 + .../eclipse/openvsx/admin/AdminAPITest.java | 183 ++++++++++++++++++ .../RepositoryServiceSmokeTest.java | 2 + 7 files changed, 354 insertions(+) diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java index 48cb30a2e..78485850a 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java @@ -1020,6 +1020,60 @@ public ResponseEntity revokePublisherTokens( } } + @PostMapping( + path = "/api/publisher/{provider}/{authId}/delete", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @CrossOrigin + @Operation(summary = "Forget a user in response to a data-protection erasure request") + @MutatingOperation + @ApiResponse( + responseCode = "200", + description = "A success message is returned in JSON format", + content = @Content(schema = @Schema(implementation = ResultJson.class)) + ) + @ApiResponse( + responseCode = "403", + description = "An administration token is required", + content = @Content(schema = @Schema(implementation = ResultJson.class)) + ) + @ApiResponse( + responseCode = "404", + description = "User not found", + content = @Content() + ) + public ResponseEntity forgetUser( + @PathVariable @Parameter(description = "Authentication provider", example = "github") String provider, + @PathVariable @Parameter(description = "Provider-specific user id") String authId, + @RequestParam(value = "token") @Parameter(description = "A personal access token") String tokenValue + ) { + try { + var adminUser = admins.checkAdminUser(tokenValue); + var result = admins.forgetUser(provider, authId, adminUser); + return ResponseEntity.ok(result); + } catch (ErrorResultException exc) { + return exc.toResponseEntity(); + } + } + + @PostMapping( + path = "/publisher/{provider}/{authId}/delete", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @MutatingOperation + public ResponseEntity forgetUser( + @PathVariable String provider, + @PathVariable String authId + ) { + try { + var adminUser = admins.checkAdminUser(); + var result = admins.forgetUser(provider, authId, adminUser); + return ResponseEntity.ok(result); + } catch (ErrorResultException exc) { + return exc.toResponseEntity(); + } + } + @GetMapping( path = "/settings", produces = MediaType.APPLICATION_JSON_VALUE diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java index 825ed52c2..48cc0c0c0 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java @@ -514,6 +514,109 @@ public ResultJson revokePublisherTokens(String provider, String loginName, UserD return result; } + /** + * Forget a user in line with a data-protection (GDPR) erasure request. + * + *

The user record is anonymised in place rather than deleted, so that retained content + * (extension reviews, security scan and file decisions, and audit logs) keeps referring to a + * row that no longer holds any personal data. Extensions in namespaces where the user was the + * sole member are unpublished but kept in the database and storage, so people who have already + * installed them are unaffected. + * + * @param provider the authentication provider the user belongs to + * @param authId the provider-specific identifier of the user to forget + * @param admin the administrator performing the erasure + */ + @Transactional(rollbackOn = ErrorResultException.class) + public ResultJson forgetUser(String provider, String authId, UserData admin) { + var user = repositories.findUserByProviderAndAuthId(provider, authId); + if (user == null) { + throw new ErrorResultException(userNotFoundMessage(provider + "/" + authId), HttpStatus.NOT_FOUND); + } + + // Send a DELETE request to the Eclipse publisher agreement API. Guarded so that + // instances without Eclipse integration are unaffected. + if (eclipse.isActive() && user.getEclipsePersonId() != null) { + eclipse.revokePublisherAgreement(user, admin); + } + + // Handle namespace memberships. Where the user is the sole member, unpublish the + // namespace's extensions (deactivate every version, which deactivates the extension and + // drops it from search) but keep them in the database for existing installs. Where other + // members remain, only the membership is removed and the extensions stay active. + var unpublishedExtensionCount = 0; + var removedMembershipCount = 0; + for (var membership : repositories.findMemberships(user).toList()) { + var namespace = membership.getNamespace(); + var soleMember = repositories.findMemberships(namespace).toList().size() <= 1; + if (soleMember) { + for (var extension : repositories.findActiveExtensions(namespace).toList()) { + var deactivated = false; + for (var version : repositories.findVersions(extension)) { + if (version.isActive()) { + version.setActive(false); + deactivated = true; + } + } + if (deactivated) { + extensions.updateExtension(extension); + unpublishedExtensionCount++; + } + } + } + + users.removeNamespaceMember(namespace, user); + removedMembershipCount++; + search.updateSearchEntries(repositories.findActiveExtensions(namespace).toList()); + } + + // Remove customer memberships. The customer and its rate-limit tokens are organisation-level + // and shared, so they are retained. + var removedCustomerMembershipCount = 0; + for (var customerMembership : repositories.findCustomerMemberships(user).toList()) { + entityManager.remove(customerMembership); + removedCustomerMembershipCount++; + } + + // Personal access tokens. Delete tokens that no retained extension version references; + // scrub and deactivate the rest so retained versions still resolve a publisher. + var deletedTokenCount = 0; + var scrubbedTokenCount = 0; + for (var token : repositories.findAccessTokens(user).toList()) { + if (repositories.countVersionsByAccessToken(token) == 0) { + entityManager.remove(token); + deletedTokenCount++; + } else { + token.setActive(false); + token.setValue(null); + token.setDescription(null); + scrubbedTokenCount++; + } + } + + // Anonymise the user record in place. Reviews, scan and file decisions, and audit logs + // keep referencing this row, which no longer holds any personal data. + var tombstoneLogin = "deleted-user-" + user.getId(); + user.setLoginName(tombstoneLogin); + user.setFullName(null); + user.setEmail(null); + user.setAvatarUrl(null); + user.setAuthId(null); + user.setProviderUrl(null); + user.setEclipsePersonId(null); + user.setEclipseToken(null); + user.setRole(null); + + // The success message deliberately contains no personal data, only the tombstone id and counts. + var result = ResultJson.success("Forgot user " + tombstoneLogin + + ": unpublished " + unpublishedExtensionCount + " extensions, removed " + + removedMembershipCount + " namespace memberships, removed " + + removedCustomerMembershipCount + " customer memberships, deleted " + + deletedTokenCount + " tokens, scrubbed " + scrubbedTokenCount + " tokens."); + logs.logAction(admin, result); + return result; + } + public UserData checkAdminUser() { return checkAdminUser(users.findLoggedInUser()); } diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java index 8bbe592e1..1ee0023b1 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java @@ -55,6 +55,8 @@ Streamable findByVersionAndExtensionNameIgnoreCaseAndExtension Streamable findByPublishedWithAndActive(PersonalAccessToken publishedWith, boolean active); + long countByPublishedWith(PersonalAccessToken publishedWith); + Streamable findByPublishedWithUserAndActive(UserData user, boolean active); Streamable findAll(); diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index 027c8bfb3..966566a43 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -346,6 +346,10 @@ public Streamable findVersionsByAccessToken(PersonalAccessToke return extensionVersionRepo.findByPublishedWithAndActive(publishedWith, active); } + public long countVersionsByAccessToken(PersonalAccessToken publishedWith) { + return extensionVersionRepo.countByPublishedWith(publishedWith); + } + public Streamable findVersionsByUser(UserData user, boolean active) { return extensionVersionRepo.findByPublishedWithUserAndActive(user, active); } @@ -426,6 +430,10 @@ public UserData findUserByLoginName(String provider, String loginName) { return userDataRepo.findByProviderAndLoginName(provider, loginName); } + public UserData findUserByProviderAndAuthId(String provider, String authId) { + return userDataRepo.findByProviderAndAuthId(provider, authId); + } + public long countUsers() { return userDataRepo.count(); } diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/UserDataRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/UserDataRepository.java index 52f957203..92409b474 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/UserDataRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/UserDataRepository.java @@ -19,6 +19,8 @@ public interface UserDataRepository extends Repository { UserData findByProviderAndLoginName(String provider, String loginName); + UserData findByProviderAndAuthId(String provider, String authId); + Page findByLoginNameStartingWith(String loginNameStart, Pageable page); long count(); diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java index 8e33347dc..97e9ae755 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -56,6 +56,7 @@ import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.AdminStatistics; +import org.eclipse.openvsx.entities.CustomerMembership; import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.entities.ExtensionReview; import org.eclipse.openvsx.entities.ExtensionVersion; @@ -1040,6 +1041,188 @@ void testRevokeAccessTokens() throws Exception { .andExpect(content().json(successJson("Deactivated 2 tokens of user github/test."))); } + @Test + void testForgetUserNotLoggedIn() throws Exception { + mockMvc.perform(post("/admin/publisher/{provider}/{authId}/delete", "github", "12345") + .with(csrf().asHeader())) + .andExpect(status().isForbidden()); + } + + @Test + void testForgetUserNotAdmin() throws Exception { + mockNormalUser(); + mockMvc.perform(post("/admin/publisher/{provider}/{authId}/delete", "github", "12345") + .with(user("test_user")) + .with(csrf().asHeader())) + .andExpect(status().isForbidden()); + } + + @Test + void testForgetUserWithInvalidToken() throws Exception { + var token = mockNonAdminToken(); + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + .andExpect(status().isForbidden()); + } + + @Test + void testForgetUserNotFound() throws Exception { + var token = mockAdminToken(); + Mockito.when(repositories.findUserByProviderAndAuthId("github", "12345")).thenReturn(null); + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + .andExpect(status().isNotFound()); + } + + @Test + void testForgetUserWithToken() throws Exception { + var token = mockAdminToken(); + var user = mockForgettableUser(); + + // The user is the sole member of a namespace, so its extension must be unpublished but retained. + var namespace = new Namespace(); + namespace.setName("janedoe-ns"); + var membership = new NamespaceMembership(); + membership.setNamespace(namespace); + membership.setUser(user); + membership.setRole(NamespaceMembership.ROLE_OWNER); + Mockito.when(repositories.findMemberships(user)).thenReturn(Streamable.of(membership)); + Mockito.when(repositories.findMemberships(namespace)).thenReturn(Streamable.of(membership)); + Mockito.when(repositories.findMembership(user, namespace)).thenReturn(membership); + + var extension = new Extension(); + extension.setName("ext"); + extension.setNamespace(namespace); + extension.setActive(true); + var version = new ExtensionVersion(); + version.setExtension(extension); + version.setActive(true); + extension.getVersions().add(version); + Mockito.when(repositories.findActiveExtensions(namespace)).thenReturn(Streamable.of(extension)); + Mockito.when(repositories.findVersions(extension)).thenReturn(Streamable.of(version)); + + var customerMembership = new CustomerMembership(); + customerMembership.setUser(user); + Mockito.when(repositories.findCustomerMemberships(user)).thenReturn(Streamable.of(customerMembership)); + + // A token not referenced by any retained version must be deleted outright. + var unreferenced = new PersonalAccessToken(); + unreferenced.setUser(user); + unreferenced.setActive(true); + Mockito.when(repositories.findAccessTokens(user)).thenReturn(Streamable.of(unreferenced)); + Mockito.when(repositories.countVersionsByAccessToken(unreferenced)).thenReturn(0L); + + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + .andExpect(status().isOk()) + .andExpect(content().json(successJson("Forgot user deleted-user-7: unpublished 1 extensions, removed 1 namespace memberships, removed 1 customer memberships, deleted 1 tokens, scrubbed 0 tokens."))); + + // The extension and its version are deactivated but kept in the database. + assertThat(version.isActive()).isFalse(); + assertThat(extension.isActive()).isFalse(); + Mockito.verify(entityManager, Mockito.never()).remove(extension); + Mockito.verify(entityManager, Mockito.never()).remove(version); + + // Memberships, the customer membership, and the unreferenced token are removed. + Mockito.verify(entityManager).remove(membership); + Mockito.verify(entityManager).remove(customerMembership); + Mockito.verify(entityManager).remove(unreferenced); + + // The user record is anonymised in place rather than deleted. + Mockito.verify(entityManager, Mockito.never()).remove(user); + assertThat(user.getLoginName()).isEqualTo("deleted-user-7"); + assertThat(user.getFullName()).isNull(); + assertThat(user.getEmail()).isNull(); + assertThat(user.getAvatarUrl()).isNull(); + assertThat(user.getAuthId()).isNull(); + assertThat(user.getProviderUrl()).isNull(); + assertThat(user.getRole()).isNull(); + } + + @Test + void testForgetUserSharedNamespaceKeepsExtensions() throws Exception { + mockAdminUser(); + var user = mockForgettableUser(); + + // A namespace with another member must keep its extensions active. + var namespace = new Namespace(); + namespace.setName("shared-ns"); + var membership = new NamespaceMembership(); + membership.setNamespace(namespace); + membership.setUser(user); + membership.setRole(NamespaceMembership.ROLE_OWNER); + var otherMembership = new NamespaceMembership(); + otherMembership.setNamespace(namespace); + otherMembership.setUser(new UserData()); + otherMembership.setRole(NamespaceMembership.ROLE_OWNER); + Mockito.when(repositories.findMemberships(user)).thenReturn(Streamable.of(membership)); + Mockito.when(repositories.findMemberships(namespace)).thenReturn(Streamable.of(membership, otherMembership)); + Mockito.when(repositories.findMembership(user, namespace)).thenReturn(membership); + + var extension = new Extension(); + extension.setName("ext"); + extension.setNamespace(namespace); + extension.setActive(true); + var version = new ExtensionVersion(); + version.setExtension(extension); + version.setActive(true); + extension.getVersions().add(version); + Mockito.when(repositories.findActiveExtensions(namespace)).thenReturn(Streamable.of(extension)); + + Mockito.when(repositories.findCustomerMemberships(user)).thenReturn(Streamable.empty()); + Mockito.when(repositories.findAccessTokens(user)).thenReturn(Streamable.empty()); + + mockMvc.perform(post("/admin/publisher/{provider}/{authId}/delete", "github", "12345") + .with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN")))) + .with(csrf().asHeader())) + .andExpect(status().isOk()) + .andExpect(content().json(successJson("Forgot user deleted-user-7: unpublished 0 extensions, removed 1 namespace memberships, removed 0 customer memberships, deleted 0 tokens, scrubbed 0 tokens."))); + + // The shared extension and version stay active; only the membership is removed. + assertThat(version.isActive()).isTrue(); + assertThat(extension.isActive()).isTrue(); + Mockito.verify(entityManager).remove(membership); + Mockito.verify(entityManager, Mockito.never()).remove(extension); + } + + @Test + void testForgetUserScrubsReferencedToken() throws Exception { + var token = mockAdminToken(); + var user = mockForgettableUser(); + Mockito.when(repositories.findMemberships(user)).thenReturn(Streamable.empty()); + Mockito.when(repositories.findCustomerMemberships(user)).thenReturn(Streamable.empty()); + + // A token still referenced by a retained version must be scrubbed and kept, not deleted. + var referenced = new PersonalAccessToken(); + referenced.setUser(user); + referenced.setActive(true); + referenced.setValue("secret-value"); + referenced.setDescription("my token"); + Mockito.when(repositories.findAccessTokens(user)).thenReturn(Streamable.of(referenced)); + Mockito.when(repositories.countVersionsByAccessToken(referenced)).thenReturn(2L); + + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + .andExpect(status().isOk()) + .andExpect(content().json(successJson("Forgot user deleted-user-7: unpublished 0 extensions, removed 0 namespace memberships, removed 0 customer memberships, deleted 0 tokens, scrubbed 1 tokens."))); + + Mockito.verify(entityManager, Mockito.never()).remove(referenced); + assertThat(referenced.isActive()).isFalse(); + assertThat(referenced.getValue()).isNull(); + assertThat(referenced.getDescription()).isNull(); + } + + private UserData mockForgettableUser() { + var user = new UserData(); + user.setId(7); + user.setProvider("github"); + user.setAuthId("12345"); + user.setLoginName("janedoe"); + user.setFullName("Jane Doe"); + user.setEmail("jane@example.com"); + user.setAvatarUrl("https://example.com/avatar.png"); + user.setProviderUrl("https://github.com/janedoe"); + user.setRole(UserData.Role.PRIVILEGED); + Mockito.when(repositories.findUserByProviderAndAuthId("github", "12345")).thenReturn(user); + return user; + } + @Test void testReportUnsupportedMediaType() throws Exception { var token = mockNonAdminToken(); diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index 60c5a0dd1..0fed89ea8 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -232,11 +232,13 @@ void testExecuteQueries() { () -> repositories.findPersistedLogsAfter(NOW), () -> repositories.findTargetPlatformVersions("version", "extensionName", "namespaceName"), () -> repositories.findUserByLoginName("provider", "loginName"), + () -> repositories.findUserByProviderAndAuthId("provider", "authId"), () -> repositories.searchUsers("search", "role", Pageable.ofSize(25)), () -> repositories.findVersion("version", "targetPlatform", extension), () -> repositories.findVersion("version", "targetPlatform", "extensionName", "namespace"), () -> repositories.findVersions(extension), () -> repositories.findVersionsByAccessToken(personalAccessToken, true), + () -> repositories.countVersionsByAccessToken(personalAccessToken), () -> repositories.getMaxExtensionDownloadCount(), () -> repositories.getOldestExtensionTimestamp(), () -> repositories.findExtensions(LONG_LIST), From 9ac59231adeae461f08036d10413ae403a289df3 Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Tue, 21 Jul 2026 14:13:40 -0400 Subject: [PATCH 2/2] fix: Switch GDPR to use username instead of authId for ease of use --- .../java/org/eclipse/openvsx/admin/AdminAPI.java | 6 +++--- .../org/eclipse/openvsx/admin/AdminService.java | 8 ++++---- .../openvsx/repositories/RepositoryService.java | 4 ---- .../org/eclipse/openvsx/admin/AdminAPITest.java | 15 ++++++++------- .../repositories/RepositoryServiceSmokeTest.java | 1 - 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java index 78485850a..883979b8f 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java @@ -1021,7 +1021,7 @@ public ResponseEntity revokePublisherTokens( } @PostMapping( - path = "/api/publisher/{provider}/{authId}/delete", + path = "/api/publisher/{provider}/{username}/delete", produces = MediaType.APPLICATION_JSON_VALUE ) @CrossOrigin @@ -1044,12 +1044,12 @@ public ResponseEntity revokePublisherTokens( ) public ResponseEntity forgetUser( @PathVariable @Parameter(description = "Authentication provider", example = "github") String provider, - @PathVariable @Parameter(description = "Provider-specific user id") String authId, + @PathVariable @Parameter(description = "Provider-specific username") String username, @RequestParam(value = "token") @Parameter(description = "A personal access token") String tokenValue ) { try { var adminUser = admins.checkAdminUser(tokenValue); - var result = admins.forgetUser(provider, authId, adminUser); + var result = admins.forgetUser(provider, username, adminUser); return ResponseEntity.ok(result); } catch (ErrorResultException exc) { return exc.toResponseEntity(); diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java index 48cc0c0c0..3ac265508 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java @@ -524,14 +524,14 @@ public ResultJson revokePublisherTokens(String provider, String loginName, UserD * installed them are unaffected. * * @param provider the authentication provider the user belongs to - * @param authId the provider-specific identifier of the user to forget + * @param username the provider-specific username of the user to forget * @param admin the administrator performing the erasure */ @Transactional(rollbackOn = ErrorResultException.class) - public ResultJson forgetUser(String provider, String authId, UserData admin) { - var user = repositories.findUserByProviderAndAuthId(provider, authId); + public ResultJson forgetUser(String provider, String username, UserData admin) { + var user = repositories.findUserByLoginName(provider, username); if (user == null) { - throw new ErrorResultException(userNotFoundMessage(provider + "/" + authId), HttpStatus.NOT_FOUND); + throw new ErrorResultException(userNotFoundMessage(provider + "/" + username), HttpStatus.NOT_FOUND); } // Send a DELETE request to the Eclipse publisher agreement API. Guarded so that diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index 966566a43..1b3bed6ff 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -430,10 +430,6 @@ public UserData findUserByLoginName(String provider, String loginName) { return userDataRepo.findByProviderAndLoginName(provider, loginName); } - public UserData findUserByProviderAndAuthId(String provider, String authId) { - return userDataRepo.findByProviderAndAuthId(provider, authId); - } - public long countUsers() { return userDataRepo.count(); } diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java index 97e9ae755..5fa803940 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -1060,15 +1060,16 @@ void testForgetUserNotAdmin() throws Exception { @Test void testForgetUserWithInvalidToken() throws Exception { var token = mockNonAdminToken(); - mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={username}", "github", "12345", token.getValue())) .andExpect(status().isForbidden()); } @Test void testForgetUserNotFound() throws Exception { var token = mockAdminToken(); - Mockito.when(repositories.findUserByProviderAndAuthId("github", "12345")).thenReturn(null); - mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + var loginName = "test_admin"; + Mockito.when(repositories.findUserByLoginName("github", loginName)).thenReturn(null); + mockMvc.perform(post("/admin/api/publisher/{provider}/{username}/delete?token={token}", "github", loginName, token.getValue())) .andExpect(status().isNotFound()); } @@ -1110,7 +1111,7 @@ void testForgetUserWithToken() throws Exception { Mockito.when(repositories.findAccessTokens(user)).thenReturn(Streamable.of(unreferenced)); Mockito.when(repositories.countVersionsByAccessToken(unreferenced)).thenReturn(0L); - mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "janedoe", token.getValue())) .andExpect(status().isOk()) .andExpect(content().json(successJson("Forgot user deleted-user-7: unpublished 1 extensions, removed 1 namespace memberships, removed 1 customer memberships, deleted 1 tokens, scrubbed 0 tokens."))); @@ -1169,7 +1170,7 @@ void testForgetUserSharedNamespaceKeepsExtensions() throws Exception { Mockito.when(repositories.findCustomerMemberships(user)).thenReturn(Streamable.empty()); Mockito.when(repositories.findAccessTokens(user)).thenReturn(Streamable.empty()); - mockMvc.perform(post("/admin/publisher/{provider}/{authId}/delete", "github", "12345") + mockMvc.perform(post("/admin/publisher/{provider}/{authId}/delete", "github", "janedoe") .with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN")))) .with(csrf().asHeader())) .andExpect(status().isOk()) @@ -1198,7 +1199,7 @@ void testForgetUserScrubsReferencedToken() throws Exception { Mockito.when(repositories.findAccessTokens(user)).thenReturn(Streamable.of(referenced)); Mockito.when(repositories.countVersionsByAccessToken(referenced)).thenReturn(2L); - mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "12345", token.getValue())) + mockMvc.perform(post("/admin/api/publisher/{provider}/{authId}/delete?token={token}", "github", "janedoe", token.getValue())) .andExpect(status().isOk()) .andExpect(content().json(successJson("Forgot user deleted-user-7: unpublished 0 extensions, removed 0 namespace memberships, removed 0 customer memberships, deleted 0 tokens, scrubbed 1 tokens."))); @@ -1219,7 +1220,7 @@ private UserData mockForgettableUser() { user.setAvatarUrl("https://example.com/avatar.png"); user.setProviderUrl("https://github.com/janedoe"); user.setRole(UserData.Role.PRIVILEGED); - Mockito.when(repositories.findUserByProviderAndAuthId("github", "12345")).thenReturn(user); + Mockito.when(repositories.findUserByLoginName("github", "janedoe")).thenReturn(user); return user; } diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index 0fed89ea8..b82fdd934 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -232,7 +232,6 @@ void testExecuteQueries() { () -> repositories.findPersistedLogsAfter(NOW), () -> repositories.findTargetPlatformVersions("version", "extensionName", "namespaceName"), () -> repositories.findUserByLoginName("provider", "loginName"), - () -> repositories.findUserByProviderAndAuthId("provider", "authId"), () -> repositories.searchUsers("search", "role", Pageable.ofSize(25)), () -> repositories.findVersion("version", "targetPlatform", extension), () -> repositories.findVersion("version", "targetPlatform", "extensionName", "namespace"),