From a1ca091c558fce92a82ed76c828a822d8a3683e3 Mon Sep 17 00:00:00 2001 From: Aart van Baren Date: Tue, 30 Dec 2025 13:17:24 +0200 Subject: [PATCH] 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 | 3 ++ ...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 | 4 +- 11 files changed, 232 insertions(+), 6 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 dcce629a4..262317b3d 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -170,3 +170,6 @@ 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' \ No newline at end of file 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 a669d321b..fbe64c316 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.springframework.beans.factory.annotation.Autowired; @@ -18,6 +19,8 @@ import java.util.Map; +import static org.eclipse.openvsx.entities.PersonalAccessToken.EXPIRY_DAYS; + @Component public class MailService { @@ -30,11 +33,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 d909d1823..208c0a4f6 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -659,4 +659,12 @@ public List findRemoveFileResourceTypeResourceMigrationItems(int public boolean isDeleteAllVersions(String namespaceName, String extensionName, List targetVersions, UserData user) { return extensionVersionJooqRepo.isDeleteAllVersions(namespaceName, extensionName, targetVersions, user); } + + public Streamable findAccessTokensCreatedBefore(LocalDateTime timestamp) { + return tokenRepo.findByCreatedTimestampLessThanEqualAndActiveTrue(timestamp); + } + + public void expireAccessTokens(LocalDateTime timestamp) { + tokenRepo.expireAccessTokens(timestamp); + } } \ No newline at end of file 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 4ce19f17b..0b61345fe 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -220,7 +220,9 @@ void testExecuteQueries() { () -> repositories.findVersion(userData,"version", "targetPlatform", "extensionName", "namespace"), () -> repositories.findLatestVersion(userData, "namespaceName", "extensionName"), () -> repositories.isDeleteAllVersions("namespaceName", "extensionName", Collections.emptyList(), userData), - () -> repositories.deactivateAccessTokens(userData) + () -> repositories.deactivateAccessTokens(userData), + () -> repositories.expireAccessTokens(NOW), + () -> repositories.findAccessTokensCreatedBefore(NOW) ); // check that we did not miss anything