From c74ac3e18e18ad757a523a0e5ba01abe259b84be Mon Sep 17 00:00:00 2001 From: Aart van Baren Date: Tue, 30 Dec 2025 13:17:24 +0200 Subject: [PATCH 1/9] Access token expiry # Conflicts: # server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java # server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java --- server/src/dev/resources/application.yml | 6 +++ ...PersonalAccessTokensJobRequestHandler.java | 33 ++++++++++++ ...nalAccessTokenExpiryJobRequestHandler.java | 37 +++++++++++++ ...PersonalAccessTokensJobRequestHandler.java | 36 +++++++++++++ ...hedulePersonalAccessTokenJobsListener.java | 54 +++++++++++++++++++ .../openvsx/entities/PersonalAccessToken.java | 11 ++-- .../org/eclipse/openvsx/mail/MailService.java | 30 +++++++++++ .../PersonalAccessTokenRepository.java | 8 +++ .../repositories/RepositoryService.java | 8 +++ .../access-token-expiry-notification.html | 14 +++++ .../RepositoryServiceSmokeTest.java | 2 + 11 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java create mode 100644 server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java create mode 100644 server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java create mode 100644 server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java create mode 100644 server/src/main/resources/mail-templates/access-token-expiry-notification.html diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index e98cc1272..5f7bf220b 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -33,6 +33,9 @@ spring: baseline-description: JobRunr tables jpa: open-in-view: false + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect hibernate: ddl-auto: none session: @@ -167,6 +170,9 @@ ovsx: revoked-access-tokens: subject: 'Open VSX Access Tokens Revoked' template: 'revoked-access-tokens.html' + access-token-expiry: + subject: 'Open VSX Access Token Expiry Notification' + template: 'access-token-expiry-notification.html' # dynamic tier-based rate limiting configuration rate-limit: enabled: false diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java new file mode 100644 index 000000000..5ebd4b906 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java @@ -0,0 +1,33 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.access_token; + +import org.eclipse.openvsx.entities.PersonalAccessToken; +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.springframework.stereotype.Component; + +@Component +public class ExpirePersonalAccessTokensJobRequestHandler implements JobRequestHandler { + + private final RepositoryService repositories; + + public ExpirePersonalAccessTokensJobRequestHandler(RepositoryService repositories) { + this.repositories = repositories; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + var timestamp = TimeUtil.getCurrentUTC().minusDays(PersonalAccessToken.EXPIRY_DAYS); + repositories.expireAccessTokens(timestamp); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java new file mode 100644 index 000000000..22bc0acf0 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java @@ -0,0 +1,37 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.access_token; + +import org.eclipse.openvsx.entities.PersonalAccessToken; +import org.eclipse.openvsx.mail.MailService; +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.springframework.stereotype.Component; + +@Component +public class NotifyPersonalAccessTokenExpiryJobRequestHandler implements JobRequestHandler { + + private final MailService mail; + private final RepositoryService repositories; + + public NotifyPersonalAccessTokenExpiryJobRequestHandler(MailService mail, RepositoryService repositories) { + this.mail = mail; + this.repositories = repositories; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + var timestamp = TimeUtil.getCurrentUTC().minusDays(PersonalAccessToken.EXPIRY_DAYS - 7); + var tokens = repositories.findAccessTokensCreatedBefore(timestamp); + tokens.forEach(mail::scheduleAccessTokenExpiryNotification); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java new file mode 100644 index 000000000..3e1037fe6 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java @@ -0,0 +1,36 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.access_token; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.jobrunr.scheduling.cron.Cron; +import org.springframework.stereotype.Component; + +import java.time.ZoneId; + +@Component +public class ScheduleExpirePersonalAccessTokensJobRequestHandler implements JobRequestHandler { + + private final JobRequestScheduler scheduler; + + public ScheduleExpirePersonalAccessTokensJobRequestHandler(JobRequestScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + var zone = ZoneId.of("UTC"); + var expireSchedule = Cron.daily(1, 38); + var expireJobRequest = new HandlerJobRequest<>(ExpirePersonalAccessTokensJobRequestHandler.class); + scheduler.scheduleRecurrently("ExpirePersonalAccessTokens", expireSchedule, zone, expireJobRequest); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java b/server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java new file mode 100644 index 000000000..3c86140e1 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java @@ -0,0 +1,54 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.access_token; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.jobrunr.scheduling.cron.Cron; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.util.UUID; + +@Component +public class SchedulePersonalAccessTokenJobsListener { + + private final JobRequestScheduler scheduler; + + @Value("${ovsx.access-token-expire.delay:0}") + int delay; + + public SchedulePersonalAccessTokenJobsListener(JobRequestScheduler scheduler) { + this.scheduler = scheduler; + } + + @EventListener + public void applicationStarted(ApplicationStartedEvent event) { + var jobIdText = "ScheduleExpirePersonalAccessTokens"; + var jobId = UUID.nameUUIDFromBytes(jobIdText.getBytes(StandardCharsets.UTF_8)); + var scheduleExpireJobRequest = new HandlerJobRequest<>(ScheduleExpirePersonalAccessTokensJobRequestHandler.class); + if(delay > 0) { + scheduler.schedule(jobId, TimeUtil.getCurrentUTC().plusDays(delay), scheduleExpireJobRequest); + } else { + scheduler.enqueue(jobId, scheduleExpireJobRequest); + } + + var zone = ZoneId.of("UTC"); + var notifySchedule = Cron.daily(2, 8); + var notifyJobRequest = new HandlerJobRequest<>(NotifyPersonalAccessTokenExpiryJobRequestHandler.class); + scheduler.scheduleRecurrently("NotifyPersonalAccessTokenExpiry", notifySchedule, zone, notifyJobRequest); + } + +} diff --git a/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java b/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java index a1ef42c62..de8120262 100644 --- a/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java +++ b/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java @@ -9,20 +9,21 @@ ********************************************************************************/ package org.eclipse.openvsx.entities; +import jakarta.persistence.*; +import org.eclipse.openvsx.json.AccessTokenJson; +import org.eclipse.openvsx.util.TimeUtil; + import java.io.Serial; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; -import jakarta.persistence.*; - -import org.eclipse.openvsx.json.AccessTokenJson; -import org.eclipse.openvsx.util.TimeUtil; - @Entity @Table(uniqueConstraints = { @UniqueConstraint(columnNames = "value") }) public class PersonalAccessToken implements Serializable { + public static final int EXPIRY_DAYS = 90; + @Serial private static final long serialVersionUID = 1L; diff --git a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java index 223918e65..5cf0bfb2f 100644 --- a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java +++ b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java @@ -9,6 +9,7 @@ * ****************************************************************************** */ package org.eclipse.openvsx.mail; +import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; @@ -20,6 +21,8 @@ import java.util.Map; +import static org.eclipse.openvsx.entities.PersonalAccessToken.EXPIRY_DAYS; + @Component public class MailService { private final Logger logger = LoggerFactory.getLogger(MailService.class); @@ -33,11 +36,38 @@ public class MailService { @Value("${ovsx.mail.revoked-access-tokens.template:}") String revokedAccessTokensTemplate; + @Value("${ovsx.mail.access-token-expiry.subject:}") + String accessTokenExpirySubject; + + @Value("${ovsx.mail.access-token-expiry.template:}") + String accessTokenExpiryTemplate; + public MailService(@Autowired(required = false) JavaMailSender sender, JobRequestScheduler scheduler) { this.disabled = sender == null; this.scheduler = scheduler; } + public void scheduleAccessTokenExpiryNotification(PersonalAccessToken token) { + if(disabled) { + return; + } + + var user = token.getUser(); + var variables = Map.of( + "name", user.getFullName(), + "tokenName", token.getDescription(), + "expiryDate", token.getCreatedTimestamp().plusDays(EXPIRY_DAYS) + ); + var jobRequest = new SendMailJobRequest( + user.getEmail(), + accessTokenExpirySubject, + accessTokenExpiryTemplate, + variables + ); + + scheduler.enqueue(jobRequest); + } + public void scheduleRevokedAccessTokensMail(UserData user) { if (disabled) { return; diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java index 52dc993b5..d0cd6a626 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java @@ -16,6 +16,8 @@ import org.springframework.data.repository.Repository; import org.springframework.data.util.Streamable; +import java.time.LocalDateTime; + public interface PersonalAccessTokenRepository extends Repository { Streamable findAll(); @@ -35,4 +37,10 @@ public interface PersonalAccessTokenRepository extends Repository findByCreatedTimestampLessThanEqualAndActiveTrue(LocalDateTime timestamp); + + @Modifying + @Query("update PersonalAccessToken t set t.active = false where t.createdTimestamp <= ?1 and t.active = true") + void expireAccessTokens(LocalDateTime timestamp); } \ No newline at end of file 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 fda5b565b..e29154eed 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -715,6 +715,14 @@ public boolean isDeleteAllVersions(String namespaceName, String extensionName, L return extensionVersionJooqRepo.isDeleteAllVersions(namespaceName, extensionName, targetVersions, user); } + public Streamable findAccessTokensCreatedBefore(LocalDateTime timestamp) { + return tokenRepo.findByCreatedTimestampLessThanEqualAndActiveTrue(timestamp); + } + + public void expireAccessTokens(LocalDateTime timestamp) { + tokenRepo.expireAccessTokens(timestamp); + } + public List findSimilarExtensionsByLevenshtein( String extensionName, String namespaceName, diff --git a/server/src/main/resources/mail-templates/access-token-expiry-notification.html b/server/src/main/resources/mail-templates/access-token-expiry-notification.html new file mode 100644 index 000000000..bbda3e3e8 --- /dev/null +++ b/server/src/main/resources/mail-templates/access-token-expiry-notification.html @@ -0,0 +1,14 @@ + + + + + + +

Hi John Doe,

+

Your access token Test token will expire on 14-11-2025.

+

+ Regards,
+ The Open VSX Team +

+ + \ No newline at end of file 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 f0b433269..49bf00f9c 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -282,6 +282,8 @@ void testExecuteQueries() { () -> repositories.findLatestVersion(userData, "namespaceName", "extensionName"), () -> repositories.isDeleteAllVersions("namespaceName", "extensionName", Collections.emptyList(), userData), () -> repositories.deactivateAccessTokens(userData), + () -> repositories.expireAccessTokens(NOW), + () -> repositories.findAccessTokensCreatedBefore(NOW), () -> repositories.findSimilarExtensionsByLevenshtein("extensionName", "namespaceName", "displayName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findSimilarNamespacesByLevenshtein("namespaceName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findExtensionScans(extVersion), From 5fe433868b72b46c02f19507b38adf1732cacafe Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 2 Mar 2026 14:23:14 +0100 Subject: [PATCH 2/9] adjust mail template --- .../mail-templates/access-token-expiry-notification.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/main/resources/mail-templates/access-token-expiry-notification.html b/server/src/main/resources/mail-templates/access-token-expiry-notification.html index bbda3e3e8..cf1a5bc24 100644 --- a/server/src/main/resources/mail-templates/access-token-expiry-notification.html +++ b/server/src/main/resources/mail-templates/access-token-expiry-notification.html @@ -5,10 +5,13 @@

Hi John Doe,

-

Your access token Test token will expire on 14-11-2025.

+

+ Your access token Test token will expire on 14-11-2025. + Please generate a new token to continue accessing the service. +

Regards,
The Open VSX Team

- \ No newline at end of file + From ea5c7b3fe9eddd020c0aeb1ced453a9e17f8d899 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 2 Mar 2026 14:38:09 +0100 Subject: [PATCH 3/9] fix merge conflicts --- .../openvsx/repositories/RepositoryService.java | 16 ++++++++-------- .../repositories/RepositoryServiceSmokeTest.java | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) 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 e29154eed..f5c0c6bb9 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -390,6 +390,10 @@ public PersonalAccessToken findAccessToken(long id) { return tokenRepo.findById(id); } + public Streamable findAccessTokensCreatedBefore(LocalDateTime timestamp) { + return tokenRepo.findByCreatedTimestampLessThanEqualAndActiveTrue(timestamp); + } + public Streamable findAllPersistedLogs() { return persistedLogRepo.findByOrderByTimestampAsc(); } @@ -647,6 +651,10 @@ public int deactivateAccessTokens(UserData user) { return tokenRepo.updateActiveSetFalse(user); } + public void expireAccessTokens(LocalDateTime timestamp) { + tokenRepo.expireAccessTokens(timestamp); + } + public List findActiveExtensionNames(Namespace namespace) { return extensionJooqRepo.findActiveExtensionNames(namespace); } @@ -715,14 +723,6 @@ public boolean isDeleteAllVersions(String namespaceName, String extensionName, L return extensionVersionJooqRepo.isDeleteAllVersions(namespaceName, extensionName, targetVersions, user); } - public Streamable findAccessTokensCreatedBefore(LocalDateTime timestamp) { - return tokenRepo.findByCreatedTimestampLessThanEqualAndActiveTrue(timestamp); - } - - public void expireAccessTokens(LocalDateTime timestamp) { - tokenRepo.expireAccessTokens(timestamp); - } - public List findSimilarExtensionsByLevenshtein( String extensionName, String namespaceName, 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 49bf00f9c..1c533af26 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; import java.lang.reflect.Modifier; From 9cf694ef78b6e84faacdea62058c776250b91155 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 2 Mar 2026 14:40:17 +0100 Subject: [PATCH 4/9] make MailService fail-safe --- .../org/eclipse/openvsx/mail/MailService.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java index 5cf0bfb2f..a30ccfe02 100644 --- a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java +++ b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java @@ -48,14 +48,25 @@ public MailService(@Autowired(required = false) JavaMailSender sender, JobReques } public void scheduleAccessTokenExpiryNotification(PersonalAccessToken token) { - if(disabled) { + if (disabled) { return; } var user = token.getUser(); + + if (user.getEmail() == null) { + logger.warn("Could not send mail to user '{}' due to expired access token notification: email not known", user.getLoginName()); + return; + } + + // the fullName might be null + var name = user.getFullName() == null ? user.getLoginName() : user.getFullName(); + // the token description might be null as well + var tokenName = token.getDescription() != null ? token.getDescription() : ""; + var variables = Map.of( - "name", user.getFullName(), - "tokenName", token.getDescription(), + "name", name, + "tokenName", tokenName, "expiryDate", token.getCreatedTimestamp().plusDays(EXPIRY_DAYS) ); var jobRequest = new SendMailJobRequest( @@ -74,7 +85,7 @@ public void scheduleRevokedAccessTokensMail(UserData user) { } if (user.getEmail() == null) { - logger.info("Could not schedule mail to user '{}' due to revoked access tokens: email not known", user.getLoginName()); + logger.warn("Could not send mail to user '{}' due to revoked access token: email not known", user.getLoginName()); return; } From 31875a56d9a00115b4ef8837029d68bff0e164c9 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Wed, 4 Mar 2026 13:28:36 +0100 Subject: [PATCH 5/9] refactor token related operations into an AccessTokenService, rename existing TokenService to EclipseTokenService, cleanup tests --- .../eclipse/openvsx/LocalRegistryService.java | 14 ++- .../java/org/eclipse/openvsx/UserAPI.java | 8 +- .../java/org/eclipse/openvsx/UserService.java | 58 ----------- .../accesstoken/AccessTokenService.java | 96 +++++++++++++++++++ ...PersonalAccessTokensJobRequestHandler.java | 2 +- ...nalAccessTokenExpiryJobRequestHandler.java | 2 +- ...PersonalAccessTokensJobRequestHandler.java | 2 +- ...hedulePersonalAccessTokenJobsListener.java | 2 +- .../eclipse/openvsx/admin/AdminService.java | 6 +- .../openvsx/eclipse/EclipseService.java | 7 +- ...nService.java => EclipseTokenService.java} | 10 +- .../ExtensionControlService.java | 4 +- .../openvsx/mirror/DataMirrorService.java | 6 +- .../mirror/MirrorExtensionService.java | 17 ++-- .../openvsx/ratelimit/IdentityService.java | 10 +- .../openvsx/security/OAuth2UserServices.java | 10 +- .../openvsx/LocalRegistryServiceTest.java | 5 + .../openvsx/MockTransactionTemplate.java | 2 +- .../org/eclipse/openvsx/RegistryAPITest.java | 31 +++--- .../java/org/eclipse/openvsx/UserAPITest.java | 31 ++++-- .../adapter/LocalVSCodeServiceTest.java | 7 +- .../openvsx/adapter/VSCodeAPITest.java | 21 ++-- .../adapter/VSCodeIdUpdateServiceTest.java | 22 ++--- .../eclipse/openvsx/admin/AdminAPITest.java | 36 ++++--- .../openvsx/eclipse/EclipseServiceTest.java | 5 +- .../eclipse/openvsx/scanning/ScannerTest.java | 1 - .../search/ElasticSearchServiceTest.java | 1 - .../openvsx/util/CollectionUtilTest.java | 3 +- .../openvsx/web/SitemapControllerTest.java | 9 +- 29 files changed, 252 insertions(+), 176 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java rename server/src/main/java/org/eclipse/openvsx/{access_token => accesstoken}/ExpirePersonalAccessTokensJobRequestHandler.java (96%) rename server/src/main/java/org/eclipse/openvsx/{access_token => accesstoken}/NotifyPersonalAccessTokenExpiryJobRequestHandler.java (97%) rename server/src/main/java/org/eclipse/openvsx/{access_token => accesstoken}/ScheduleExpirePersonalAccessTokensJobRequestHandler.java (97%) rename server/src/main/java/org/eclipse/openvsx/{access_token => accesstoken}/SchedulePersonalAccessTokenJobsListener.java (98%) rename server/src/main/java/org/eclipse/openvsx/eclipse/{TokenService.java => EclipseTokenService.java} (96%) diff --git a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java index 7ef3ea8b8..8ad2419f5 100644 --- a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java @@ -13,6 +13,7 @@ import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; @@ -59,6 +60,7 @@ public class LocalRegistryService implements IExtensionRegistry { private final ExtensionService extensions; private final VersionService versions; private final UserService users; + private final AccessTokenService tokens; private final SearchUtilService search; private final ExtensionValidator validator; private final StorageUtilService storageUtil; @@ -73,6 +75,7 @@ public LocalRegistryService( ExtensionService extensions, VersionService versions, UserService users, + AccessTokenService tokens, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, @@ -86,6 +89,7 @@ public LocalRegistryService( this.extensions = extensions; this.versions = versions; this.users = users; + this.tokens = tokens; this.search = search; this.validator = validator; this.storageUtil = storageUtil; @@ -579,7 +583,7 @@ private Map> getMemberships(Collection createAccessToken(@RequestParam(required return new ResponseEntity<>(HttpStatus.FORBIDDEN); } - return new ResponseEntity<>(users.createAccessToken(user, description), HttpStatus.CREATED); + return new ResponseEntity<>(tokens.createAccessToken(user, description), HttpStatus.CREATED); } @PostMapping( @@ -189,7 +193,7 @@ public ResponseEntity deleteAccessToken(@PathVariable long id) { } try { - return ResponseEntity.ok(users.deleteAccessToken(user, id)); + return ResponseEntity.ok(tokens.deleteAccessToken(user, id)); } catch(NotFoundException e) { return new ResponseEntity<>(ResultJson.error("Token does not exist."), HttpStatus.NOT_FOUND); } diff --git a/server/src/main/java/org/eclipse/openvsx/UserService.java b/server/src/main/java/org/eclipse/openvsx/UserService.java index c1d4c80df..81c748998 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserService.java +++ b/server/src/main/java/org/eclipse/openvsx/UserService.java @@ -22,9 +22,7 @@ import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.entities.Namespace; import org.eclipse.openvsx.entities.NamespaceMembership; -import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; -import org.eclipse.openvsx.json.AccessTokenJson; import org.eclipse.openvsx.json.NamespaceDetailsJson; import org.eclipse.openvsx.json.ResultJson; import org.eclipse.openvsx.repositories.RepositoryService; @@ -33,7 +31,6 @@ import org.eclipse.openvsx.storage.StorageUtilService; import org.eclipse.openvsx.util.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.CacheEvict; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -47,7 +44,6 @@ import java.util.stream.Collectors; import static org.eclipse.openvsx.cache.CacheService.CACHE_NAMESPACE_DETAILS_JSON; -import static org.eclipse.openvsx.util.UrlUtil.createApiUrl; @Component public class UserService { @@ -60,9 +56,6 @@ public class UserService { private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AttributesConfig attributesConfig; - @Value("${ovsx.token-prefix:}") - String tokenPrefix; - public UserService( EntityManager entityManager, RepositoryService repositories, @@ -93,24 +86,6 @@ public UserData findLoggedInUser() { return null; } - @Transactional - public PersonalAccessToken useAccessToken(String tokenValue) { - var token = repositories.findAccessToken(tokenValue); - if (token == null || !token.isActive()) { - return null; - } - token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); - return token; - } - - public String generateTokenValue() { - String value; - do { - value = tokenPrefix + UUID.randomUUID(); - } while (repositories.hasAccessToken(value)); - return value; - } - public boolean hasPublishPermission(UserData user, Namespace namespace) { if (UserData.ROLE_PRIVILEGED.equals(user.getRole())) { // Privileged users can publish to every namespace. @@ -255,39 +230,6 @@ public ResultJson updateNamespaceDetailsLogo(String namespaceName, MultipartFile return ResultJson.success("Updated logo for namespace " + namespace.getName()); } - @Transactional - public AccessTokenJson createAccessToken(UserData user, String description) { - var token = new PersonalAccessToken(); - token.setUser(user); - token.setValue(generateTokenValue()); - token.setActive(true); - token.setCreatedTimestamp(TimeUtil.getCurrentUTC()); - token.setDescription(description); - entityManager.persist(token); - var json = token.toAccessTokenJson(); - // Include the token value after creation so the user can copy it - json.setValue(token.getValue()); - json.setDeleteTokenUrl(createApiUrl(UrlUtil.getBaseUrl(), "user", "token", "delete", Long.toString(token.getId()))); - - return json; - } - - @Transactional - public ResultJson deleteAccessToken(UserData user, long id) { - var token = repositories.findAccessToken(id); - if (token == null || !token.isActive()) { - throw new NotFoundException(); - } - - user = entityManager.merge(user); - if(!token.getUser().equals(user)) { - throw new NotFoundException(); - } - - token.setActive(false); - return ResultJson.success("Deleted access token for user " + user.getLoginName() + "."); - } - public boolean canLogin() { return !getLoginProviders().isEmpty(); } diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java new file mode 100644 index 000000000..c1a41c6e7 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java @@ -0,0 +1,96 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.accesstoken; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.eclipse.openvsx.entities.PersonalAccessToken; +import org.eclipse.openvsx.entities.UserData; +import org.eclipse.openvsx.json.AccessTokenJson; +import org.eclipse.openvsx.json.ResultJson; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.NotFoundException; +import org.eclipse.openvsx.util.TimeUtil; +import org.eclipse.openvsx.util.UrlUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +import static org.eclipse.openvsx.util.UrlUtil.createApiUrl; + +@Service +public class AccessTokenService { + private final EntityManager entityManager; + private final RepositoryService repositories; + + @Value("${ovsx.token-prefix:}") + String tokenPrefix; + + public AccessTokenService(EntityManager entityManager, RepositoryService repositories) { + this.entityManager = entityManager; + this.repositories = repositories; + } + + @Transactional + public AccessTokenJson createAccessToken(UserData user, String description) { + var token = new PersonalAccessToken(); + token.setUser(user); + token.setValue(generateTokenValue()); + token.setActive(true); + token.setCreatedTimestamp(TimeUtil.getCurrentUTC()); + token.setDescription(description); + entityManager.persist(token); + var json = token.toAccessTokenJson(); + // Include the token value after creation so the user can copy it + json.setValue(token.getValue()); + json.setDeleteTokenUrl(createApiUrl(UrlUtil.getBaseUrl(), "user", "token", "delete", Long.toString(token.getId()))); + + return json; + } + + // public to be accessible from tests + public String generateTokenValue() { + String value; + do { + value = tokenPrefix + UUID.randomUUID(); + } while (repositories.hasAccessToken(value)); + return value; + } + + @Transactional + public ResultJson deleteAccessToken(UserData user, long id) { + var token = repositories.findAccessToken(id); + if (token == null || !token.isActive()) { + throw new NotFoundException(); + } + + user = entityManager.merge(user); + if(!token.getUser().equals(user)) { + throw new NotFoundException(); + } + + token.setActive(false); + return ResultJson.success("Deleted access token for user " + user.getLoginName() + "."); + } + + @Transactional + public PersonalAccessToken useAccessToken(String tokenValue) { + var token = repositories.findAccessToken(tokenValue); + if (token == null || !token.isActive()) { + return null; + } + token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); + return token; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensJobRequestHandler.java similarity index 96% rename from server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java rename to server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensJobRequestHandler.java index 5ebd4b906..ea0c360b5 100644 --- a/server/src/main/java/org/eclipse/openvsx/access_token/ExpirePersonalAccessTokensJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensJobRequestHandler.java @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 * ****************************************************************************** */ -package org.eclipse.openvsx.access_token; +package org.eclipse.openvsx.accesstoken; import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.migration.HandlerJobRequest; diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java similarity index 97% rename from server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java rename to server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java index 22bc0acf0..e4a9ccf5c 100644 --- a/server/src/main/java/org/eclipse/openvsx/access_token/NotifyPersonalAccessTokenExpiryJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 * ****************************************************************************** */ -package org.eclipse.openvsx.access_token; +package org.eclipse.openvsx.accesstoken; import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.mail.MailService; diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java similarity index 97% rename from server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java rename to server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java index 3e1037fe6..770e555fb 100644 --- a/server/src/main/java/org/eclipse/openvsx/access_token/ScheduleExpirePersonalAccessTokensJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 * ****************************************************************************** */ -package org.eclipse.openvsx.access_token; +package org.eclipse.openvsx.accesstoken; import org.eclipse.openvsx.migration.HandlerJobRequest; import org.jobrunr.jobs.lambdas.JobRequestHandler; diff --git a/server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java similarity index 98% rename from server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java rename to server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java index 3c86140e1..54cf3f2dc 100644 --- a/server/src/main/java/org/eclipse/openvsx/access_token/SchedulePersonalAccessTokenJobsListener.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 * ****************************************************************************** */ -package org.eclipse.openvsx.access_token; +package org.eclipse.openvsx.accesstoken; import org.eclipse.openvsx.migration.HandlerJobRequest; import org.eclipse.openvsx.util.TimeUtil; 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 f9f15c4ef..20e2e99e2 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java @@ -15,6 +15,7 @@ import org.eclipse.openvsx.ExtensionService; import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; @@ -46,6 +47,7 @@ public class AdminService { private final ExtensionService extensions; private final EntityManager entityManager; private final UserService users; + private final AccessTokenService tokens; private final ExtensionValidator validator; private final SearchUtilService search; private final EclipseService eclipse; @@ -61,6 +63,7 @@ public AdminService( ExtensionService extensions, EntityManager entityManager, UserService users, + AccessTokenService tokens, ExtensionValidator validator, SearchUtilService search, EclipseService eclipse, @@ -75,6 +78,7 @@ public AdminService( this.extensions = extensions; this.entityManager = entityManager; this.users = users; + this.tokens = tokens; this.validator = validator; this.search = search; this.eclipse = eclipse; @@ -476,7 +480,7 @@ public UserData checkAdminUser() { public UserData checkAdminUser(String tokenValue) { var user = Optional.of(tokenValue) - .map(users::useAccessToken) + .map(tokens::useAccessToken) .map(PersonalAccessToken::getUser) .orElse(null); diff --git a/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java index 90d16bbb7..3eaf04fdb 100644 --- a/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java +++ b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -43,7 +44,7 @@ import java.util.Map; import java.util.regex.Pattern; -@Component +@Service public class EclipseService { private static final String VAR_PERSON_ID = "personId"; @@ -61,7 +62,7 @@ public class EclipseService { protected final Logger logger = LoggerFactory.getLogger(EclipseService.class); - private final TokenService tokens; + private final EclipseTokenService tokens; private final ExtensionService extensions; private final EntityManager entityManager; private final RestTemplate restTemplate; @@ -77,7 +78,7 @@ public class EclipseService { List publisherAgreementAllowedVersions; public EclipseService( - TokenService tokens, + EclipseTokenService tokens, ExtensionService extensions, EntityManager entityManager, RestTemplate restTemplate diff --git a/server/src/main/java/org/eclipse/openvsx/eclipse/TokenService.java b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseTokenService.java similarity index 96% rename from server/src/main/java/org/eclipse/openvsx/eclipse/TokenService.java rename to server/src/main/java/org/eclipse/openvsx/eclipse/EclipseTokenService.java index c9200d149..37c49b642 100644 --- a/server/src/main/java/org/eclipse/openvsx/eclipse/TokenService.java +++ b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseTokenService.java @@ -26,9 +26,9 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -37,16 +37,16 @@ import java.util.List; import java.util.Optional; -@Component -public class TokenService { +@Service +public class EclipseTokenService { - protected final Logger logger = LoggerFactory.getLogger(TokenService.class); + protected final Logger logger = LoggerFactory.getLogger(EclipseTokenService.class); private final TransactionTemplate transactions; private final EntityManager entityManager; private final ClientRegistrationRepository clientRegistrationRepository; - public TokenService( + public EclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, @Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository diff --git a/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java b/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java index 996cf78bb..3480b7dec 100644 --- a/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java +++ b/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java @@ -31,7 +31,7 @@ import org.springframework.stereotype.Component; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; @@ -124,7 +124,7 @@ public void updateExtension(ExtensionId extensionId, boolean deprecated, Extensi } public JsonNode getExtensionControlJson() throws IOException { - var url = new URL("https://github.com/open-vsx/publish-extensions/raw/master/extension-control/extensions.json"); + var url = URI.create("https://github.com/open-vsx/publish-extensions/raw/master/extension-control/extensions.json").toURL(); return new ObjectMapper().readValue(url, JsonNode.class); } diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java index aa8a95bfe..8a9c9c990 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java @@ -17,6 +17,7 @@ import org.eclipse.openvsx.LocalRegistryService; import org.eclipse.openvsx.UpstreamRegistryService; import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.admin.AdminService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.ExtensionJson; @@ -47,6 +48,7 @@ public class DataMirrorService { private final RepositoryService repositories; private final EntityManager entityManager; private final UserService users; + private final AccessTokenService tokens; private final ExtensionService extensions; private final UpstreamRegistryService upstream; private final LocalRegistryService local; @@ -68,6 +70,7 @@ public DataMirrorService( RepositoryService repositories, EntityManager entityManager, UserService users, + AccessTokenService tokens, ExtensionService extensions, UpstreamRegistryService upstream, LocalRegistryService local, @@ -81,6 +84,7 @@ public DataMirrorService( this.repositories = repositories; this.entityManager = entityManager; this.users = users; + this.tokens = tokens; this.extensions = extensions; this.upstream = upstream; this.local = local; @@ -169,7 +173,7 @@ public UserData getOrAddUser(UserJson json) { public String getOrAddAccessTokenValue(UserData user, String description) { var token = repositories.findAccessToken(user, description); return token == null - ? users.createAccessToken(user, description).getValue() + ? tokens.createAccessToken(user, description).getValue() : token.getValue(); } diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java b/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java index 4066ceae0..9fdee1abf 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java @@ -11,7 +11,7 @@ import org.eclipse.openvsx.ExtensionService; import org.eclipse.openvsx.UpstreamRegistryService; -import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.json.ExtensionJson; import org.eclipse.openvsx.json.UserJson; @@ -32,7 +32,6 @@ import java.time.LocalDate; import java.util.*; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.zip.ZipFile; import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD_SIG; @@ -48,7 +47,7 @@ public class MirrorExtensionService { private final UpstreamRegistryService upstream; private final RestTemplate backgroundRestTemplate; private final RestTemplate backgroundNonRedirectingRestTemplate; - private final UserService users; + private final AccessTokenService tokens; private final ExtensionService extensions; private final ExtensionVersionIntegrityService integrityService; @@ -58,7 +57,7 @@ public MirrorExtensionService( UpstreamRegistryService upstream, RestTemplate backgroundRestTemplate, RestTemplate backgroundNonRedirectingRestTemplate, - UserService users, + AccessTokenService tokens, ExtensionService extensions, ExtensionVersionIntegrityService integrityService ) { @@ -67,7 +66,7 @@ public MirrorExtensionService( this.upstream = upstream; this.backgroundRestTemplate = backgroundRestTemplate; this.backgroundNonRedirectingRestTemplate = backgroundNonRedirectingRestTemplate; - this.users = users; + this.tokens = tokens; this.extensions = extensions; this.integrityService = integrityService; } @@ -141,7 +140,7 @@ private void mirrorExtensionVersions(String namespaceName, String extensionName, versions.stream() .filter(version -> targetVersions.stream().noneMatch(extVersion -> extVersion.getVersion().equals(version))) .map(version -> upstream.getExtension(namespaceName, extensionName, targetPlatform, version)) - .collect(Collectors.toList()) + .toList() ); } toAdd.sort(Comparator.comparing(extensionJson -> TimeUtil.fromUTCString(extensionJson.getTimestamp()))); @@ -175,8 +174,8 @@ private void mirrorExtensionVersion(ExtensionJson json) { throw new AssertionError("Expected location header from redirected vsix url"); } - var tokens = vsixLocation.getPath().split("/"); - var filename = tokens[tokens.length-1]; + var segments = vsixLocation.getPath().split("/"); + var filename = segments[segments.length-1]; if (!filename.endsWith(".vsix")) { throw new AssertionError("Invalid vsix filename from redirected vsix url"); } @@ -206,7 +205,7 @@ private void mirrorExtensionVersion(ExtensionJson json) { var description = "MirrorExtensionVersion"; var accessTokenValue = data.getOrAddAccessTokenValue(user, description); - var token = users.useAccessToken(accessTokenValue); + var token = tokens.useAccessToken(accessTokenValue); extensions.mirrorVersion(extensionFile, signatureName, token, filename, json.getTimestamp()); logger.atDebug() .setMessage("completed mirroring of extension version: {}") diff --git a/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java b/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java index f6e0e13bb..8f6464a33 100644 --- a/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java +++ b/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java @@ -14,7 +14,7 @@ import com.giffing.bucket4j.spring.boot.starter.context.ExpressionParams; import jakarta.servlet.http.HttpServletRequest; -import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.ratelimit.config.RateLimitConfig; import org.eclipse.openvsx.ratelimit.config.RateLimitProperties; import org.slf4j.Logger; @@ -39,7 +39,7 @@ public class IdentityService { private final TierService tierService; private final CustomerService customerService; - private final UserService userService; + private final AccessTokenService tokenService; private final RateLimitProperties rateLimitProperties; public IdentityService( @@ -47,14 +47,14 @@ public IdentityService( ConfigurableBeanFactory beanFactory, TierService tierService, CustomerService customerService, - UserService userService, + AccessTokenService tokenService, RateLimitProperties rateLimitProperties ) { this.expressionParser = expressionParser; this.beanFactory = beanFactory; this.tierService = tierService; this.customerService = customerService; - this.userService = userService; + this.tokenService = tokenService; this.rateLimitProperties = rateLimitProperties; } @@ -67,7 +67,7 @@ public ResolvedIdentity resolveIdentity(HttpServletRequest request) { // This will update the database with the time the token is last accessed, // but we need to ensure that we only take valid tokens into account for rate limiting. // If this turns out to be a bottleneck, we need to cache the token hashcode. - var tokenEntity = userService.useAccessToken(token); + var tokenEntity = tokenService.useAccessToken(token); if (tokenEntity != null) { // if a valid token is present we use it as a cache key cacheKey = "token_" + token.hashCode(); diff --git a/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java b/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java index afd3005ec..beec1cbc1 100644 --- a/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java +++ b/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java @@ -13,9 +13,8 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.UserService; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.UserData; -import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.ErrorResultException; import org.springframework.context.event.EventListener; import org.springframework.security.authentication.AuthenticationServiceException; @@ -45,8 +44,7 @@ public class OAuth2UserServices { private final UserService users; - private final TokenService tokens; - private final RepositoryService repositories; + private final EclipseTokenService tokens; private final EntityManager entityManager; private final EclipseService eclipse; private final OAuth2AttributesConfig attributesConfig; @@ -55,15 +53,13 @@ public class OAuth2UserServices { public OAuth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService tokens, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { this.users = users; this.tokens = tokens; - this.repositories = repositories; this.entityManager = entityManager; this.eclipse = eclipse; this.attributesConfig = attributesConfig; diff --git a/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java b/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java index 10cffed5c..63f1d399a 100644 --- a/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java @@ -13,6 +13,7 @@ package org.eclipse.openvsx; import jakarta.persistence.EntityManager; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.Namespace; @@ -62,6 +63,9 @@ class LocalRegistryServiceTest { @Mock UserService users; + @Mock + AccessTokenService tokens; + @Mock SearchUtilService searchUtilService; @@ -93,6 +97,7 @@ void setUp() { extensions, versions, users, + tokens, searchUtilService, validator, storageUtilService, diff --git a/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java b/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java index eec3a188a..921dc1ac0 100644 --- a/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java +++ b/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java @@ -29,4 +29,4 @@ public T execute(TransactionCallback action) throws TransactionException public void afterPropertiesSet() { // Method override to prevent IllegalArgumentException from being thrown } -} \ No newline at end of file +} diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index 8950b37a8..dd8c994cc 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -14,12 +14,13 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.ExtensionJsonCacheKeyGenerator; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.extension_control.ExtensionControlService; import org.eclipse.openvsx.json.*; @@ -2552,25 +2553,32 @@ UserService userService( return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); } + @Bean + AccessTokenService tokenService( + EntityManager entityManager, + RepositoryService repositories + ) { + return new AccessTokenService(entityManager, repositories); + } + @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -2578,29 +2586,31 @@ LocalRegistryService localRegistryService( EntityManager entityManager, RepositoryService repositories, ExtensionService extensions, - @Qualifier("registryTest") VersionService versions, + VersionService versionService, UserService users, + AccessTokenService tokenService, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, EclipseService eclipse, CacheService cache, ExtensionVersionIntegrityService integrityService, - SimilarityService similarityService + SimilarityCheckService similarityCheckService ) { return new LocalRegistryService( entityManager, repositories, extensions, - versions, + versionService, users, + tokenService, search, validator, storageUtil, eclipse, cache, integrityService, - similarityCheckService(similarityConfig(), similarityService(repositories), repositories) + similarityCheckService ); } @@ -2674,7 +2684,6 @@ LocalStorageService localStorageService() { ExtensionJsonCacheKeyGenerator extensionJsonCacheKeyGenerator() { return new ExtensionJsonCacheKeyGenerator(); } @Bean - @Qualifier("registryTest") VersionService versionService() { return new VersionService(); } diff --git a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java index 2ec958d2a..017ed1856 100644 --- a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java @@ -13,10 +13,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; @@ -82,6 +83,9 @@ class UserAPITest { @MockitoSpyBean UserService users; + @MockitoSpyBean + AccessTokenService tokens; + @MockitoBean EntityManager entityManager; @@ -143,7 +147,7 @@ void testAccessTokensNotLoggedIn() throws Exception { @Test void testCreateAccessToken() throws Exception { mockUserData(); - Mockito.doReturn("foobar").when(users).generateTokenValue(); + Mockito.doReturn("foobar").when(tokens).generateTokenValue(); mockMvc.perform(post("/user/token/create?description={description}", "This is my token") .with(user("test_user")) .with(csrf().asHeader())) @@ -789,25 +793,32 @@ UserService userService( return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); } + @Bean + AccessTokenService accessTokenService( + EntityManager entityManager, + RepositoryService repositories + ) { + return new AccessTokenService(entityManager, repositories); + } + @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -822,13 +833,14 @@ LocalRegistryService localRegistryService( ExtensionService extensions, VersionService versions, UserService users, + AccessTokenService accessTokenService, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, EclipseService eclipse, CacheService cache, ExtensionVersionIntegrityService integrityService, - SimilarityService similarityService + SimilarityCheckService similarityCheckService ) { return new LocalRegistryService( entityManager, @@ -836,13 +848,14 @@ LocalRegistryService localRegistryService( extensions, versions, users, + accessTokenService, search, validator, storageUtil, eclipse, cache, integrityService, - similarityCheckService(similarityConfig(), similarityService(repositories), repositories) + similarityCheckService ); } diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java index 0da5dfe69..c688c9a2a 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java @@ -40,7 +40,7 @@ @ExtendWith(SpringExtension.class) @MockitoBean( types = { VSCodeAPI.class, SimpleMeterRegistry.class, SearchUtilService.class, - VersionService.class, StorageUtilService.class, ExtensionVersionIntegrityService.class, + StorageUtilService.class, ExtensionVersionIntegrityService.class, WebResourceService.class, CacheService.class }) public class LocalVSCodeServiceTest { @@ -118,11 +118,6 @@ private ExtensionVersion mockExtensionVersion(Extension extension, long id, Stri @TestConfiguration static class TestConfig { - @Bean - VersionService versionService() { - return new VersionService(); - } - @Bean LocalVSCodeService vsCodeService( RepositoryService repositories, diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java index 5e611e7bc..7bed165ce 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -20,7 +20,7 @@ import org.eclipse.openvsx.cache.FilesCacheKeyGenerator; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.repositories.RepositoryService; @@ -1021,8 +1021,8 @@ private Path mockExtensionBrowse(String namespaceName, String extensionName, Str @Import(SecurityConfig.class) static class TestConfig { @Bean - IExtensionQueryRequestHandler extensionQueryRequestHandler(LocalVSCodeService local, UpstreamVSCodeService upstream) { - return new DefaultExtensionQueryRequestHandler(local, upstream); + IExtensionQueryRequestHandler extensionQueryRequestHandler(LocalVSCodeService localVSCodeService, UpstreamVSCodeService upstream) { + return new DefaultExtensionQueryRequestHandler(localVSCodeService, upstream); } @Bean @@ -1033,22 +1033,21 @@ TransactionTemplate transactionTemplate() { @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -1064,14 +1063,14 @@ WebResourceService webResourceService( @Bean LocalVSCodeService localVSCodeService( RepositoryService repositories, - VersionService versions, + VersionService versionService, SearchUtilService search, StorageUtilService storageUtil, ExtensionVersionIntegrityService integrityService, WebResourceService webResourceService, CacheService cache ) { - return new LocalVSCodeService(repositories, versions, search, storageUtil, integrityService, webResourceService, cache); + return new LocalVSCodeService(repositories, versionService, search, storageUtil, integrityService, webResourceService, cache); } @Bean @@ -1124,7 +1123,7 @@ LocalStorageService localStorage() { } @Bean - VersionService getVersionService() { + VersionService versionService() { return new VersionService(); } diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java index 1af76058a..dcf0c5c68 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java @@ -38,7 +38,7 @@ class VSCodeIdUpdateServiceTest { VSCodeIdUpdateService updateService; @Test - void testUpdateAllNoChanges() throws InterruptedException { + void testUpdateAllNoChanges() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -99,7 +99,7 @@ void testUpdateAllNoChanges() throws InterruptedException { } @Test - void testUpdateAllRandomNoChanges() throws InterruptedException { + void testUpdateAllRandomNoChanges() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -160,7 +160,7 @@ void testUpdateAllRandomNoChanges() throws InterruptedException { } @Test - void testUpdateAllChange() throws InterruptedException { + void testUpdateAllChange() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -230,7 +230,7 @@ void testUpdateAllChange() throws InterruptedException { } @Test - void testUpdateRandom() throws InterruptedException { + void testUpdateRandom() { var namespaceName = "foo"; var namespacePublicId = UUID.randomUUID().toString(); var extensionName = "bar"; @@ -259,7 +259,7 @@ void testUpdateRandom() throws InterruptedException { } @Test - void testUpdateRandomExistsDb() throws InterruptedException { + void testUpdateRandomExistsDb() { var namespaceName = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var namespacePublicId2 = UUID.randomUUID().toString(); @@ -292,7 +292,7 @@ void testUpdateRandomExistsDb() throws InterruptedException { } @Test - void testMustUpdateRandom() throws InterruptedException { + void testMustUpdateRandom() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -345,7 +345,7 @@ void testMustUpdateRandom() throws InterruptedException { } @Test - void testMustUpdateRandomExists() throws InterruptedException { + void testMustUpdateRandomExists() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -398,7 +398,7 @@ void testMustUpdateRandomExists() throws InterruptedException { } @Test - void testMustUpdateRandomExistsDb() throws InterruptedException { + void testMustUpdateRandomExistsDb() { var namespaceName1 = "foo"; var namespaceUuid1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -456,7 +456,7 @@ void testMustUpdateRandomExistsDb() throws InterruptedException { @Test - void testUpdateNoChange() throws InterruptedException { + void testUpdateNoChange() { var namespaceName = "foo"; var namespacePublicId = UUID.randomUUID().toString(); var extensionName = "bar"; @@ -484,7 +484,7 @@ void testUpdateNoChange() throws InterruptedException { } @Test - void testUpdateUpstream() throws InterruptedException { + void testUpdateUpstream() { var namespaceName = "foo"; var namespacePublicId = "123-456-789"; var extensionName = "bar"; @@ -514,7 +514,7 @@ void testUpdateUpstream() throws InterruptedException { } @Test - void testUpdateDuplicateRecursive() throws InterruptedException { + void testUpdateDuplicateRecursive() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; 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 eff82205f..d1c6fcac3 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -14,11 +14,12 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.eclipse.openvsx.*; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; import org.eclipse.openvsx.mail.MailService; @@ -651,7 +652,7 @@ void testRevokePublisherAgreement() throws Exception { .andExpect(content().json(successJson("Deactivated 1 tokens, deactivated 1 extensions of user github/test."))); assertThat(token.isActive()).isFalse(); - assertThat(versions.get(0).isActive()).isFalse(); + assertThat(versions.getFirst().isActive()).isFalse(); } @Test @@ -1454,25 +1455,32 @@ UserService userService( return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); } + @Bean + AccessTokenService tokenService( + EntityManager entityManager, + RepositoryService repositories + ) { + return new AccessTokenService(entityManager, repositories); + } + @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -1481,6 +1489,7 @@ AdminService adminService( ExtensionService extensions, EntityManager entityManager, UserService users, + AccessTokenService tokenService, ExtensionValidator validator, SearchUtilService search, EclipseService eclipse, @@ -1496,6 +1505,7 @@ AdminService adminService( extensions, entityManager, users, + tokenService, validator, search, eclipse, @@ -1513,30 +1523,31 @@ LocalRegistryService localRegistryService( EntityManager entityManager, RepositoryService repositories, ExtensionService extensions, - @Qualifier("adminTest") VersionService versions, + VersionService versionService, UserService users, + AccessTokenService tokenService, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, EclipseService eclipse, CacheService cache, - FileCacheDurationConfig fileCacheDurationConfig, ExtensionVersionIntegrityService integrityService, - SimilarityService similarityService + SimilarityCheckService similarityCheckService ) { return new LocalRegistryService( entityManager, repositories, extensions, - versions, + versionService, users, + tokenService, search, validator, storageUtil, eclipse, cache, integrityService, - similarityCheckService(similarityConfig(), similarityService(repositories), repositories) + similarityCheckService ); } @@ -1626,7 +1637,6 @@ LocalStorageService localStorage() { } @Bean - @Qualifier("adminTest") VersionService versionService() { return new VersionService(); } diff --git a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java index bfc9771ec..6552d775e 100644 --- a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java @@ -75,7 +75,7 @@ class EclipseServiceTest { RepositoryService repositories; @MockitoBean - TokenService tokens; + EclipseTokenService tokens; @MockitoBean RestTemplate restTemplate; @@ -399,7 +399,7 @@ TransactionTemplate transactionTemplate() { @Bean EclipseService eclipseService( - TokenService tokens, + EclipseTokenService tokens, ExtensionService extensions, EntityManager entityManager, RestTemplate restTemplate @@ -478,5 +478,4 @@ LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKeyGenerator( return new LatestExtensionVersionCacheKeyGenerator(); } } - } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java index eca56505b..ba09022f0 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java @@ -138,5 +138,4 @@ void threat_allowsNullDescription() { assertNull(threat.getDescription()); assertEquals("LOW", threat.getSeverity()); } - } diff --git a/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java b/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java index efdced8d6..bf005c4f3 100644 --- a/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java @@ -299,5 +299,4 @@ LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKeyGenerator( return new LatestExtensionVersionCacheKeyGenerator(); } } - } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java b/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java index 49d5ddbf1..6626e293b 100644 --- a/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java +++ b/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java @@ -18,10 +18,9 @@ class CollectionUtilTest { @Test - void testLimit() throws Exception { + void testLimit() { var source = Lists.newArrayList(1, 2, 3, 4, 5); var result = CollectionUtil.limit(source, 3); assertThat(result).isEqualTo(Lists.newArrayList(1, 2, 3)); } - } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java b/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java index b38a7fc54..a46dda194 100644 --- a/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java @@ -13,7 +13,7 @@ import jakarta.persistence.EntityManager; import org.eclipse.openvsx.UserService; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.security.OAuth2AttributesConfig; import org.eclipse.openvsx.security.OAuth2UserServices; @@ -38,7 +38,7 @@ @WebMvcTest(SitemapController.class) @AutoConfigureWebClient @MockitoBean(types = { - EclipseService.class, SimpleMeterRegistry.class, UserService.class, TokenService.class, EntityManager.class + EclipseService.class, SimpleMeterRegistry.class, UserService.class, EclipseTokenService.class, EntityManager.class }) class SitemapControllerTest { @@ -73,13 +73,12 @@ static class TestConfig { @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean From 0508ab20100ca4d5a634bcbe038ac8db8bf2a9f6 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Wed, 4 Mar 2026 18:21:51 +0100 Subject: [PATCH 6/9] overhaul personal access token expiration mechanism --- .../eclipse/openvsx/LocalRegistryService.java | 2 +- .../java/org/eclipse/openvsx/UserAPI.java | 2 +- .../accesstoken/AccessTokenConfig.java | 61 +++++++++++++++++ .../accesstoken/AccessTokenService.java | 52 ++++++++++++--- ...=> ExpirePersonalAccessTokensHandler.java} | 23 ++++--- ...yPersonalAccessTokenExpirationHandler.java | 42 ++++++++++++ ...yPersonalAccessTokenExpirationHandler.java | 55 ++++++++++++++++ ...nalAccessTokenExpiryJobRequestHandler.java | 37 ----------- ...PersonalAccessTokensJobRequestHandler.java | 36 ---------- ...hedulePersonalAccessTokenJobsListener.java | 54 --------------- .../ScheduleTokenExpirationJobs.java | 65 +++++++++++++++++++ .../openvsx/entities/PersonalAccessToken.java | 34 +++++++++- .../eclipse/openvsx/json/AccessTokenJson.java | 22 +++++++ .../org/eclipse/openvsx/mail/MailService.java | 9 +-- .../PersonalAccessTokenRepository.java | 12 +++- .../repositories/RepositoryService.java | 12 ++-- ...__PersonalAccessToken_ExpiresTimestamp.sql | 7 ++ .../org/eclipse/openvsx/RegistryAPITest.java | 10 ++- .../java/org/eclipse/openvsx/UserAPITest.java | 12 ++-- .../eclipse/openvsx/admin/AdminAPITest.java | 9 ++- .../RepositoryServiceSmokeTest.java | 3 +- webui/src/extension-registry-types.ts | 2 + webui/src/pages/user/user-settings-tokens.tsx | 1 + webui/src/utils.ts | 55 +++++++++++----- 24 files changed, 428 insertions(+), 189 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java rename server/src/main/java/org/eclipse/openvsx/accesstoken/{ExpirePersonalAccessTokensJobRequestHandler.java => ExpirePersonalAccessTokensHandler.java} (50%) create mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java create mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java delete mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java delete mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java delete mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java create mode 100644 server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java create mode 100644 server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql diff --git a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java index 8ad2419f5..ebe3b45ab 100644 --- a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java @@ -658,7 +658,7 @@ public ExtensionJson publish(InputStream content, UserData user) throws ErrorRes try { return publish(content, token.getValue()); } finally { - tokens.deleteAccessToken(user, token.getId()); + tokens.deactivateAccessToken(user, token.getId()); } } diff --git a/server/src/main/java/org/eclipse/openvsx/UserAPI.java b/server/src/main/java/org/eclipse/openvsx/UserAPI.java index 5070f71a2..1d3ff54c2 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/UserAPI.java @@ -193,7 +193,7 @@ public ResponseEntity deleteAccessToken(@PathVariable long id) { } try { - return ResponseEntity.ok(tokens.deleteAccessToken(user, id)); + return ResponseEntity.ok(tokens.deactivateAccessToken(user, id)); } catch(NotFoundException e) { return new ResponseEntity<>(ResultJson.error("Token does not exist."), HttpStatus.NOT_FOUND); } diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java new file mode 100644 index 000000000..bc30ee03d --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java @@ -0,0 +1,61 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.accesstoken; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration +public class AccessTokenConfig { + /** + * The token prefix to use when generating a new access token. + *

+ * Property: {@code ovsx.access-token.prefix} + * Old Property: {@code ovsx.token-prefix} + * Default: {@code ''} + */ + @Value("#{'${ovsx.access-token.prefix:${ovsx.token-prefix:}}'}") + String prefix; + + /** + * The expiration period for personal access tokens. + *

+ * If {@code 0} is provided, the access tokens do not expire. + *

+ * Property: {@code ovsx.access-token.expiration} + * Default: {@code P90D}, expires in 90 days + */ + @Value("${ovsx.access-token.expiration:P90D}") + Duration expiration; + + /** + * The duration before the expiration of an access token + * to send out a notification email to users. + *

+ * Property: {@code ovsx.access-token.notification} + * Default: {@code P7D}, 7 days prior to expiration + */ + @Value("${ovsx.access-token.notification:P7D}") + Duration notification; + + @Value("${ovsx.access-token.max-token-notifications:100}") + int maxTokenNotifications; + + @Value("${ovsx.access-token.expiration-schedule:*/20 * * * * *}") + String expirationSchedule; + + @Value("${ovsx.access-token.notification-schedule:*/20 * * * * *}") + String notificationSchedule; +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java index c1a41c6e7..17d150665 100644 --- a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java @@ -18,28 +18,35 @@ import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.json.AccessTokenJson; import org.eclipse.openvsx.json.ResultJson; +import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.NotFoundException; import org.eclipse.openvsx.util.TimeUtil; import org.eclipse.openvsx.util.UrlUtil; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.UUID; import static org.eclipse.openvsx.util.UrlUtil.createApiUrl; @Service public class AccessTokenService { + private final AccessTokenConfig config; private final EntityManager entityManager; private final RepositoryService repositories; + private final MailService mail; - @Value("${ovsx.token-prefix:}") - String tokenPrefix; - - public AccessTokenService(EntityManager entityManager, RepositoryService repositories) { + public AccessTokenService( + AccessTokenConfig config, + EntityManager entityManager, + RepositoryService repositories, + MailService mail + ) { + this.config = config; this.entityManager = entityManager; this.repositories = repositories; + this.mail = mail; } @Transactional @@ -48,7 +55,14 @@ public AccessTokenJson createAccessToken(UserData user, String description) { token.setUser(user); token.setValue(generateTokenValue()); token.setActive(true); - token.setCreatedTimestamp(TimeUtil.getCurrentUTC()); + + var createdAt = TimeUtil.getCurrentUTC(); + token.setCreatedTimestamp(createdAt); + + if (config.expiration != null && config.expiration.isPositive()) { + token.setExpiresTimestamp(createdAt.plus(config.expiration)); + } + token.setDescription(description); entityManager.persist(token); var json = token.toAccessTokenJson(); @@ -63,13 +77,13 @@ public AccessTokenJson createAccessToken(UserData user, String description) { public String generateTokenValue() { String value; do { - value = tokenPrefix + UUID.randomUUID(); + value = config.prefix + UUID.randomUUID(); } while (repositories.hasAccessToken(value)); return value; } @Transactional - public ResultJson deleteAccessToken(UserData user, long id) { + public ResultJson deactivateAccessToken(UserData user, long id) { var token = repositories.findAccessToken(id); if (token == null || !token.isActive()) { throw new NotFoundException(); @@ -81,7 +95,7 @@ public ResultJson deleteAccessToken(UserData user, long id) { } token.setActive(false); - return ResultJson.success("Deleted access token for user " + user.getLoginName() + "."); + return ResultJson.success("Deactivated access token for user " + user.getLoginName() + "."); } @Transactional @@ -93,4 +107,24 @@ public PersonalAccessToken useAccessToken(String tokenValue) { token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); return token; } + + @Transactional + public int expireAccessTokens() { + return repositories.expireAccessTokens(TimeUtil.getCurrentUTC()); + } + + @Transactional + public void scheduleTokenExpirationNotification(PersonalAccessToken token) { + token = entityManager.merge(token); + try { + mail.scheduleAccessTokenExpiryNotification(token); + } finally { + token.setNotified(true); + } + } + + @Transactional + public int setExpirationTimeForLegacyAccessTokens(LocalDateTime expirationTime) { + return repositories.updateExpiresTimeForLegacyAccessTokens(expirationTime); + } } diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensHandler.java similarity index 50% rename from server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensJobRequestHandler.java rename to server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensHandler.java index ea0c360b5..0efbab687 100644 --- a/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensHandler.java @@ -9,25 +9,28 @@ * ****************************************************************************** */ package org.eclipse.openvsx.accesstoken; -import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.migration.HandlerJobRequest; -import org.eclipse.openvsx.repositories.RepositoryService; -import org.eclipse.openvsx.util.TimeUtil; import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component -public class ExpirePersonalAccessTokensJobRequestHandler implements JobRequestHandler { +public class ExpirePersonalAccessTokensHandler implements JobRequestHandler> { - private final RepositoryService repositories; + private final Logger logger = LoggerFactory.getLogger(ExpirePersonalAccessTokensHandler.class); - public ExpirePersonalAccessTokensJobRequestHandler(RepositoryService repositories) { - this.repositories = repositories; + private final AccessTokenService tokens; + + public ExpirePersonalAccessTokensHandler(AccessTokenService tokens) { + this.tokens = tokens; } @Override - public void run(HandlerJobRequest handlerJobRequest) throws Exception { - var timestamp = TimeUtil.getCurrentUTC().minusDays(PersonalAccessToken.EXPIRY_DAYS); - repositories.expireAccessTokens(timestamp); + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + var count = tokens.expireAccessTokens(); + if (count > 0) { + logger.info("Expired {} personal access token(s)", count); + } } } diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java new file mode 100644 index 000000000..4b35c8f32 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java @@ -0,0 +1,42 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class LegacyPersonalAccessTokenExpirationHandler implements JobRequestHandler> { + + private final Logger logger = LoggerFactory.getLogger(LegacyPersonalAccessTokenExpirationHandler.class); + + private final AccessTokenConfig config; + private final AccessTokenService tokens; + + public LegacyPersonalAccessTokenExpirationHandler(AccessTokenConfig config, AccessTokenService tokens) { + this.config = config; + this.tokens = tokens; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + if (config.expiration != null && config.expiration.isPositive()) { + var expirationTime = TimeUtil.getCurrentUTC().plus(config.expiration); + var count = tokens.setExpirationTimeForLegacyAccessTokens(expirationTime); + if (count > 0) { + logger.info("Set expiration time for {} legacy personal access token(s)", count); + } + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java new file mode 100644 index 000000000..034cf6b7a --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java @@ -0,0 +1,55 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Component; + +@Component +public class NotifyPersonalAccessTokenExpirationHandler implements JobRequestHandler> { + + private final Logger logger = LoggerFactory.getLogger(NotifyPersonalAccessTokenExpirationHandler.class); + + private final AccessTokenConfig config; + private final AccessTokenService tokens; + private final RepositoryService repositories; + + public NotifyPersonalAccessTokenExpirationHandler( + AccessTokenConfig config, + AccessTokenService tokens, + RepositoryService repositories + ) { + this.config = config; + this.tokens = tokens; + this.repositories = repositories; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + if (config.notification.isPositive()) { + var expireBefore = TimeUtil.getCurrentUTC().plus(config.notification); + var page = PageRequest.of(0, config.maxTokenNotifications); + var expiringAccessTokens = repositories.findExpiringAccessTokensWithoutNotification(expireBefore, page); + for (var token : expiringAccessTokens) { + tokens.scheduleTokenExpirationNotification(token); + } + + if (!expiringAccessTokens.isEmpty()) { + logger.info("Scheduled {} notification(s) for expiring personal access tokens", expiringAccessTokens.size()); + } + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java deleted file mode 100644 index e4a9ccf5c..000000000 --- a/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpiryJobRequestHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -/** ****************************************************************************** - * Copyright (c) 2025 Precies. Software OU and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * SPDX-License-Identifier: EPL-2.0 - * ****************************************************************************** */ -package org.eclipse.openvsx.accesstoken; - -import org.eclipse.openvsx.entities.PersonalAccessToken; -import org.eclipse.openvsx.mail.MailService; -import org.eclipse.openvsx.migration.HandlerJobRequest; -import org.eclipse.openvsx.repositories.RepositoryService; -import org.eclipse.openvsx.util.TimeUtil; -import org.jobrunr.jobs.lambdas.JobRequestHandler; -import org.springframework.stereotype.Component; - -@Component -public class NotifyPersonalAccessTokenExpiryJobRequestHandler implements JobRequestHandler { - - private final MailService mail; - private final RepositoryService repositories; - - public NotifyPersonalAccessTokenExpiryJobRequestHandler(MailService mail, RepositoryService repositories) { - this.mail = mail; - this.repositories = repositories; - } - - @Override - public void run(HandlerJobRequest handlerJobRequest) throws Exception { - var timestamp = TimeUtil.getCurrentUTC().minusDays(PersonalAccessToken.EXPIRY_DAYS - 7); - var tokens = repositories.findAccessTokensCreatedBefore(timestamp); - tokens.forEach(mail::scheduleAccessTokenExpiryNotification); - } -} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java deleted file mode 100644 index 770e555fb..000000000 --- a/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleExpirePersonalAccessTokensJobRequestHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/** ****************************************************************************** - * Copyright (c) 2025 Precies. Software OU and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * SPDX-License-Identifier: EPL-2.0 - * ****************************************************************************** */ -package org.eclipse.openvsx.accesstoken; - -import org.eclipse.openvsx.migration.HandlerJobRequest; -import org.jobrunr.jobs.lambdas.JobRequestHandler; -import org.jobrunr.scheduling.JobRequestScheduler; -import org.jobrunr.scheduling.cron.Cron; -import org.springframework.stereotype.Component; - -import java.time.ZoneId; - -@Component -public class ScheduleExpirePersonalAccessTokensJobRequestHandler implements JobRequestHandler { - - private final JobRequestScheduler scheduler; - - public ScheduleExpirePersonalAccessTokensJobRequestHandler(JobRequestScheduler scheduler) { - this.scheduler = scheduler; - } - - @Override - public void run(HandlerJobRequest handlerJobRequest) throws Exception { - var zone = ZoneId.of("UTC"); - var expireSchedule = Cron.daily(1, 38); - var expireJobRequest = new HandlerJobRequest<>(ExpirePersonalAccessTokensJobRequestHandler.class); - scheduler.scheduleRecurrently("ExpirePersonalAccessTokens", expireSchedule, zone, expireJobRequest); - } -} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java deleted file mode 100644 index 54cf3f2dc..000000000 --- a/server/src/main/java/org/eclipse/openvsx/accesstoken/SchedulePersonalAccessTokenJobsListener.java +++ /dev/null @@ -1,54 +0,0 @@ -/** ****************************************************************************** - * Copyright (c) 2025 Precies. Software OU and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * SPDX-License-Identifier: EPL-2.0 - * ****************************************************************************** */ -package org.eclipse.openvsx.accesstoken; - -import org.eclipse.openvsx.migration.HandlerJobRequest; -import org.eclipse.openvsx.util.TimeUtil; -import org.jobrunr.scheduling.JobRequestScheduler; -import org.jobrunr.scheduling.cron.Cron; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; -import java.time.ZoneId; -import java.util.UUID; - -@Component -public class SchedulePersonalAccessTokenJobsListener { - - private final JobRequestScheduler scheduler; - - @Value("${ovsx.access-token-expire.delay:0}") - int delay; - - public SchedulePersonalAccessTokenJobsListener(JobRequestScheduler scheduler) { - this.scheduler = scheduler; - } - - @EventListener - public void applicationStarted(ApplicationStartedEvent event) { - var jobIdText = "ScheduleExpirePersonalAccessTokens"; - var jobId = UUID.nameUUIDFromBytes(jobIdText.getBytes(StandardCharsets.UTF_8)); - var scheduleExpireJobRequest = new HandlerJobRequest<>(ScheduleExpirePersonalAccessTokensJobRequestHandler.class); - if(delay > 0) { - scheduler.schedule(jobId, TimeUtil.getCurrentUTC().plusDays(delay), scheduleExpireJobRequest); - } else { - scheduler.enqueue(jobId, scheduleExpireJobRequest); - } - - var zone = ZoneId.of("UTC"); - var notifySchedule = Cron.daily(2, 8); - var notifyJobRequest = new HandlerJobRequest<>(NotifyPersonalAccessTokenExpiryJobRequestHandler.class); - scheduler.scheduleRecurrently("NotifyPersonalAccessTokenExpiry", notifySchedule, zone, notifyJobRequest); - } - -} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java new file mode 100644 index 000000000..bd0090afa --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java @@ -0,0 +1,65 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + + +@Component +public class ScheduleTokenExpirationJobs { + + private final Logger logger = LoggerFactory.getLogger(ScheduleTokenExpirationJobs.class); + + private final AccessTokenConfig config; + private final JobRequestScheduler scheduler; + + public ScheduleTokenExpirationJobs(AccessTokenConfig config, JobRequestScheduler scheduler) { + this.config = config; + this.scheduler = scheduler; + } + + @EventListener + public void scheduleJobs(ApplicationStartedEvent event) { + if (config.expiration != null && config.expiration.isPositive()) { + scheduler.enqueue(new HandlerJobRequest<>(LegacyPersonalAccessTokenExpirationHandler.class)); + } + + if (StringUtils.hasText(config.expirationSchedule)) { + logger.info("Scheduling access token expiration job with schedule '{}'", config.expirationSchedule); + scheduler.scheduleRecurrently( + "access-token-expiration", + config.expirationSchedule, + new HandlerJobRequest<>(ExpirePersonalAccessTokensHandler.class) + ); + } else { + scheduler.deleteRecurringJob("access-token-expiration"); + } + + if (StringUtils.hasText(config.notificationSchedule) && config.notification.isPositive()) { + logger.info("Scheduling access token notification job with schedule '{}'", config.notificationSchedule); + scheduler.scheduleRecurrently( + "access-token-notification", + config.notificationSchedule, + new HandlerJobRequest<>(NotifyPersonalAccessTokenExpirationHandler.class)); + } else { + scheduler.deleteRecurringJob("access-token-notification"); + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java b/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java index de8120262..6af7f6823 100644 --- a/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java +++ b/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java @@ -45,6 +45,10 @@ public class PersonalAccessToken implements Serializable { private LocalDateTime accessedTimestamp; + private LocalDateTime expiresTimestamp; + + private boolean notified; + @Column(length = 2048) private String description; @@ -55,10 +59,16 @@ public AccessTokenJson toAccessTokenJson() { var json = new AccessTokenJson(); json.setId(this.getId()); // The value is not included: it is displayed only when the token is created - if (this.getCreatedTimestamp() != null) + if (this.getCreatedTimestamp() != null) { json.setCreatedTimestamp(TimeUtil.toUTCString(this.getCreatedTimestamp())); - if (this.getAccessedTimestamp() != null) + } + if (this.getAccessedTimestamp() != null) { json.setAccessedTimestamp(TimeUtil.toUTCString(this.getAccessedTimestamp())); + } + if (this.getExpiresTimestamp() != null) { + json.setExpiresTimestamp(TimeUtil.toUTCString(this.getExpiresTimestamp())); + } + json.setNotified(this.isNotified()); json.setDescription(this.getDescription()); return json; } @@ -111,6 +121,22 @@ public void setAccessedTimestamp(LocalDateTime timestamp) { this.accessedTimestamp = timestamp; } + public LocalDateTime getExpiresTimestamp() { + return expiresTimestamp; + } + + public void setExpiresTimestamp(LocalDateTime expiresTimestamp) { + this.expiresTimestamp = expiresTimestamp; + } + + public boolean isNotified() { + return notified; + } + + public void setNotified(boolean notified) { + this.notified = notified; + } + public String getDescription() { return description; } @@ -130,11 +156,13 @@ public boolean equals(Object o) { && Objects.equals(value, that.value) && Objects.equals(createdTimestamp, that.createdTimestamp) && Objects.equals(accessedTimestamp, that.accessedTimestamp) + && Objects.equals(expiresTimestamp, that.expiresTimestamp) + && Objects.equals(notified, that.notified) && Objects.equals(description, that.description); } @Override public int hashCode() { - return Objects.hash(id, user, value, active, createdTimestamp, accessedTimestamp, description); + return Objects.hash(id, user, value, active, createdTimestamp, accessedTimestamp, expiresTimestamp, notified, description); } } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java b/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java index a1116d72b..d3b0a6ef9 100644 --- a/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java +++ b/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java @@ -33,6 +33,11 @@ public static AccessTokenJson error(String message) { @Nullable private String accessedTimestamp; + @Nullable + private String expiresTimestamp; + + private boolean notified; + private String description; private String deleteTokenUrl; @@ -71,6 +76,23 @@ public void setAccessedTimestamp(@Nullable String accessedTimestamp) { this.accessedTimestamp = accessedTimestamp; } + @Nullable + public String getExpiresTimestamp() { + return expiresTimestamp; + } + + public void setExpiresTimestamp(@Nullable String expiresTimestamp) { + this.expiresTimestamp = expiresTimestamp; + } + + public boolean isNotified() { + return notified; + } + + public void setNotified(boolean notified) { + this.notified = notified; + } + public String getDescription() { return description; } diff --git a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java index a30ccfe02..219889b7a 100644 --- a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java +++ b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java @@ -21,7 +21,6 @@ import java.util.Map; -import static org.eclipse.openvsx.entities.PersonalAccessToken.EXPIRY_DAYS; @Component public class MailService { @@ -53,8 +52,9 @@ public void scheduleAccessTokenExpiryNotification(PersonalAccessToken token) { } var user = token.getUser(); + var email = user.getEmail(); - if (user.getEmail() == null) { + if (email == null) { logger.warn("Could not send mail to user '{}' due to expired access token notification: email not known", user.getLoginName()); return; } @@ -67,16 +67,17 @@ public void scheduleAccessTokenExpiryNotification(PersonalAccessToken token) { var variables = Map.of( "name", name, "tokenName", tokenName, - "expiryDate", token.getCreatedTimestamp().plusDays(EXPIRY_DAYS) + "expiryDate", token.getExpiresTimestamp() ); var jobRequest = new SendMailJobRequest( - user.getEmail(), + email, accessTokenExpirySubject, accessTokenExpiryTemplate, variables ); scheduler.enqueue(jobRequest); + logger.debug("Scheduled notification email for expiring token {} to {}", tokenName, email); } public void scheduleRevokedAccessTokensMail(UserData user) { diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java index d0cd6a626..8b3195d30 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java @@ -11,12 +11,14 @@ import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; import org.springframework.data.util.Streamable; import java.time.LocalDateTime; +import java.util.List; public interface PersonalAccessTokenRepository extends Repository { @@ -38,9 +40,13 @@ public interface PersonalAccessTokenRepository extends Repository findByCreatedTimestampLessThanEqualAndActiveTrue(LocalDateTime timestamp); + @Modifying + @Query("update PersonalAccessToken t set t.expiresTimestamp = ?1 where t.active = true and t.expiresTimestamp is null") + int updateExpiresTimeForLegacyAccessTokens(LocalDateTime timestamp); + + List findByExpiresTimestampLessThanEqualAndActiveTrueAndNotifiedFalseOrderById(LocalDateTime timestamp, Pageable pageable); @Modifying - @Query("update PersonalAccessToken t set t.active = false where t.createdTimestamp <= ?1 and t.active = true") - void expireAccessTokens(LocalDateTime timestamp); + @Query("update PersonalAccessToken t set t.active = false where t.expiresTimestamp <= ?1 and t.active = true") + int expireAccessTokens(LocalDateTime timestamp); } \ No newline at end of file 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 f5c0c6bb9..cb13c80e9 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -390,8 +390,8 @@ public PersonalAccessToken findAccessToken(long id) { return tokenRepo.findById(id); } - public Streamable findAccessTokensCreatedBefore(LocalDateTime timestamp) { - return tokenRepo.findByCreatedTimestampLessThanEqualAndActiveTrue(timestamp); + public List findExpiringAccessTokensWithoutNotification(LocalDateTime expirationTime, Pageable pageable) { + return tokenRepo.findByExpiresTimestampLessThanEqualAndActiveTrueAndNotifiedFalseOrderById(expirationTime, pageable); } public Streamable findAllPersistedLogs() { @@ -651,8 +651,12 @@ public int deactivateAccessTokens(UserData user) { return tokenRepo.updateActiveSetFalse(user); } - public void expireAccessTokens(LocalDateTime timestamp) { - tokenRepo.expireAccessTokens(timestamp); + public int expireAccessTokens(LocalDateTime timestamp) { + return tokenRepo.expireAccessTokens(timestamp); + } + + public int updateExpiresTimeForLegacyAccessTokens(LocalDateTime timestamp) { + return tokenRepo.updateExpiresTimeForLegacyAccessTokens(timestamp); } public List findActiveExtensionNames(Namespace namespace) { diff --git a/server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql b/server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql new file mode 100644 index 000000000..1294a18ae --- /dev/null +++ b/server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql @@ -0,0 +1,7 @@ +-- add new columns wrt access token expiration +ALTER TABLE personal_access_token + ADD COLUMN expires_timestamp TIMESTAMP WITHOUT TIME ZONE, + ADD COLUMN notified BOOLEAN; + +UPDATE personal_access_token + SET expires_timestamp = NULL, notified = FALSE; diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index dd8c994cc..a1376f2bb 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -14,6 +14,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.openvsx.accesstoken.AccessTokenConfig; import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; @@ -24,6 +25,7 @@ import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.extension_control.ExtensionControlService; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.publish.PublishExtensionVersionService; @@ -94,7 +96,7 @@ AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class, EclipseService.class, PublishExtensionVersionService.class, SimpleMeterRegistry.class, JobRequestScheduler.class, ExtensionControlService.class, FileCacheDurationConfig.class, CdnServiceConfig.class, - ExtensionScanPersistenceService.class, LogService.class + ExtensionScanPersistenceService.class, LogService.class, AccessTokenConfig.class, MailService.class }) class RegistryAPITest { @@ -2555,10 +2557,12 @@ UserService userService( @Bean AccessTokenService tokenService( + AccessTokenConfig config, EntityManager entityManager, - RepositoryService repositories + RepositoryService repositories, + MailService mailService ) { - return new AccessTokenService(entityManager, repositories); + return new AccessTokenService(config, entityManager, repositories, mailService); } @Bean diff --git a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java index 017ed1856..8c21a0c1f 100644 --- a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; +import org.eclipse.openvsx.accesstoken.AccessTokenConfig; import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; @@ -20,6 +21,7 @@ import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.repositories.RepositoryService; @@ -76,7 +78,7 @@ EclipseService.class, ClientRegistrationRepository.class, StorageUtilService.class, CacheService.class, ExtensionValidator.class, SimpleMeterRegistry.class, SearchUtilService.class, PublishExtensionVersionHandler.class, JobRequestScheduler.class, VersionService.class, ExtensionVersionIntegrityService.class, ExtensionScanService.class, - ExtensionScanPersistenceService.class, LogService.class + ExtensionScanPersistenceService.class, LogService.class, AccessTokenConfig.class, MailService.class }) class UserAPITest { @@ -181,7 +183,7 @@ void testDeleteAccessToken() throws Exception { .with(user("test_user")) .with(csrf().asHeader())) .andExpect(status().isOk()) - .andExpect(content().json(successJson("Deleted access token for user test_user."))); + .andExpect(content().json(successJson("Deactivated access token for user test_user."))); } @Test @@ -795,10 +797,12 @@ UserService userService( @Bean AccessTokenService accessTokenService( + AccessTokenConfig config, EntityManager entityManager, - RepositoryService repositories + RepositoryService repositories, + MailService mailService ) { - return new AccessTokenService(entityManager, repositories); + return new AccessTokenService(config, entityManager, repositories, mailService); } @Bean 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 d1c6fcac3..22a580713 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -14,6 +14,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.eclipse.openvsx.*; +import org.eclipse.openvsx.accesstoken.AccessTokenConfig; import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; @@ -81,7 +82,7 @@ AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class, PublishExtensionVersionHandler.class, SearchUtilService.class, EclipseService.class, SimpleMeterRegistry.class, FileCacheDurationConfig.class, MailService.class, CdnServiceConfig.class, - ExtensionScanService.class, ExtensionScanPersistenceService.class, LogService.class + ExtensionScanService.class, ExtensionScanPersistenceService.class, LogService.class, AccessTokenConfig.class }) class AdminAPITest { @@ -1457,10 +1458,12 @@ UserService userService( @Bean AccessTokenService tokenService( + AccessTokenConfig config, EntityManager entityManager, - RepositoryService repositories + RepositoryService repositories, + MailService mailService ) { - return new AccessTokenService(entityManager, repositories); + return new AccessTokenService(config, entityManager, repositories, mailService); } @Bean 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 1c533af26..dd5193a5d 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; import java.lang.reflect.Modifier; @@ -282,7 +283,7 @@ void testExecuteQueries() { () -> repositories.isDeleteAllVersions("namespaceName", "extensionName", Collections.emptyList(), userData), () -> repositories.deactivateAccessTokens(userData), () -> repositories.expireAccessTokens(NOW), - () -> repositories.findAccessTokensCreatedBefore(NOW), + () -> repositories.findExpiringAccessTokensWithoutNotification(NOW, page), () -> repositories.findSimilarExtensionsByLevenshtein("extensionName", "namespaceName", "displayName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findSimilarNamespacesByLevenshtein("namespaceName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findExtensionScans(extVersion), diff --git a/webui/src/extension-registry-types.ts b/webui/src/extension-registry-types.ts index b3c267c14..6bbdb2ae9 100644 --- a/webui/src/extension-registry-types.ts +++ b/webui/src/extension-registry-types.ts @@ -180,6 +180,8 @@ export interface PersonalAccessToken { value?: string; createdTimestamp: TimestampString; accessedTimestamp?: TimestampString; + expiresTimestamp?: TimestampString; + notified?: boolean; description: string; deleteTokenUrl: UrlString; } diff --git a/webui/src/pages/user/user-settings-tokens.tsx b/webui/src/pages/user/user-settings-tokens.tsx index 4a97dfca4..88a001362 100644 --- a/webui/src/pages/user/user-settings-tokens.tsx +++ b/webui/src/pages/user/user-settings-tokens.tsx @@ -105,6 +105,7 @@ export const UserSettingsTokens: FunctionComponent = () => { {token.description} Created: + Expires: {token.expiresTimestamp ? : 'never'} Accessed: {token.accessedTimestamp ? : 'never'} diff --git a/webui/src/utils.ts b/webui/src/utils.ts index b8c9fd9f1..bedf77f65 100644 --- a/webui/src/utils.ts +++ b/webui/src/utils.ts @@ -63,23 +63,46 @@ export function toRelativeTime(timestamp?: string): string | undefined { } const date = new Date(timestamp); const elapsed = Date.now() - date.getTime(); - if (elapsed < msPerMinute) { - return 'now'; - } else if (elapsed < msPerHour) { - const value = Math.round(elapsed / msPerMinute); - return `${value} minute${value !== 1 ? 's' : ''} ago`; - } else if (elapsed < msPerDay) { - const value = Math.round(elapsed / msPerHour); - return `${value} hour${value !== 1 ? 's' : ''} ago`; - } else if (elapsed < msPerMonth) { - const value = Math.round(elapsed / msPerDay); - return `${value} day${value !== 1 ? 's' : ''} ago`; - } else if (elapsed < msPerYear) { - const value = Math.round(elapsed / msPerMonth); - return `${value} month${value !== 1 ? 's' : ''} ago`; + + if (elapsed < 0) { + const remaining = -elapsed + if (remaining < msPerMinute) { + return 'now'; + } else if (remaining < msPerHour) { + const value = Math.round(remaining / msPerMinute); + return `in ${value} minute${value !== 1 ? 's' : ''}`; + } else if (remaining < msPerDay) { + const value = Math.round(remaining / msPerHour); + return `in ${value} hour${value !== 1 ? 's' : ''}`; + } else if (remaining < msPerMonth) { + const value = Math.round(remaining / msPerDay); + return `in ${value} day${value !== 1 ? 's' : ''}`; + } else if (remaining < msPerYear) { + const value = Math.round(remaining / msPerMonth); + return `in ${value} month${value !== 1 ? 's' : ''}`; + } else { + const value = Math.round(remaining / msPerYear); + return `in ${value} year${value !== 1 ? 's' : ''}`; + } } else { - const value = Math.round(elapsed / msPerYear); - return `${value} year${value !== 1 ? 's' : ''} ago`; + if (elapsed < msPerMinute) { + return 'now'; + } else if (elapsed < msPerHour) { + const value = Math.round(elapsed / msPerMinute); + return `${value} minute${value !== 1 ? 's' : ''} ago`; + } else if (elapsed < msPerDay) { + const value = Math.round(elapsed / msPerHour); + return `${value} hour${value !== 1 ? 's' : ''} ago`; + } else if (elapsed < msPerMonth) { + const value = Math.round(elapsed / msPerDay); + return `${value} day${value !== 1 ? 's' : ''} ago`; + } else if (elapsed < msPerYear) { + const value = Math.round(elapsed / msPerMonth); + return `${value} month${value !== 1 ? 's' : ''} ago`; + } else { + const value = Math.round(elapsed / msPerYear); + return `${value} year${value !== 1 ? 's' : ''} ago`; + } } } From 36930453d9adf13d5ec17d2acca96cb811ac2642 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Wed, 4 Mar 2026 19:33:44 +0100 Subject: [PATCH 7/9] revert accidental commit --- server/src/dev/resources/application.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index 5f7bf220b..21da3de74 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -33,9 +33,6 @@ spring: baseline-description: JobRunr tables jpa: open-in-view: false - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect hibernate: ddl-auto: none session: From 039a4f663c879766153af1cfacb52784cf446931 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Wed, 4 Mar 2026 19:33:59 +0100 Subject: [PATCH 8/9] add javadoc for configuration options --- .../accesstoken/AccessTokenConfig.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java index bc30ee03d..e05e169a8 100644 --- a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java @@ -50,12 +50,33 @@ public class AccessTokenConfig { @Value("${ovsx.access-token.notification:P7D}") Duration notification; + /** + * The maximum number of expiring token notifications to handle + * within one job execution. + *

+ * Property: {@code ovsx.access-token.max-token-notifications} + * Default: {@code 100} + */ @Value("${ovsx.access-token.max-token-notifications:100}") int maxTokenNotifications; - @Value("${ovsx.access-token.expiration-schedule:*/20 * * * * *}") + /** + * The cron schedule for the job to disable expired + * access tokens. + *

+ * Property: {@code ovsx.access-token.expiration-schedule} + * Default: every 15 min + */ + @Value("${ovsx.access-token.expiration-schedule:0 */15 * * * *}") String expirationSchedule; - @Value("${ovsx.access-token.notification-schedule:*/20 * * * * *}") + /** + * The cron schedule for the job to send out notifications + * for soon to be expired access tokens. + *

+ * Property: {@code ovsx.access-token.notification-schedule} + * Default: every 15 min + */ + @Value("${ovsx.access-token.notification-schedule:30 */15 * * * *}") String notificationSchedule; } From ddcaabb777601c093061a9e73eead0176ca47a32 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Wed, 4 Mar 2026 19:48:19 +0100 Subject: [PATCH 9/9] fix smoke tests --- .../openvsx/repositories/RepositoryServiceSmokeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd5193a5d..761e83453 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; import java.lang.reflect.Modifier; @@ -284,6 +283,7 @@ void testExecuteQueries() { () -> repositories.deactivateAccessTokens(userData), () -> repositories.expireAccessTokens(NOW), () -> repositories.findExpiringAccessTokensWithoutNotification(NOW, page), + () -> repositories.updateExpiresTimeForLegacyAccessTokens(NOW), () -> repositories.findSimilarExtensionsByLevenshtein("extensionName", "namespaceName", "displayName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findSimilarNamespacesByLevenshtein("namespaceName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findExtensionScans(extVersion),