Skip to content

Commit e09b6d0

Browse files
authored
Feat: improve access token expiry by supporting sending an email when tokens have expired (#1701)
* support sending expired token mails * update development settings to sane defaults * add validation for access token settings, make mirror mode and token expiration mutually exclusive * provide default values for mail templates, add validation
1 parent e61af59 commit e09b6d0

16 files changed

Lines changed: 245 additions & 52 deletions

server/src/dev/resources/application.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ spring:
4343
# explicitly disable thymeleaf view resolution
4444
enabled: false
4545

46+
# mail:
47+
# host: "localhost"
48+
# port: 25
49+
4650
security:
4751
oauth2:
4852
client:
@@ -170,18 +174,16 @@ ovsx:
170174
integrity:
171175
key-pair: create # create, renew, delete, 'undefined'
172176
registry:
173-
version: 'v0.32.0-dev'
177+
version: 'v0.33.0-dev'
174178
storage:
175179
local:
176180
directory: /tmp
181+
access-token:
182+
prefix: dev_ovsxat_ # use a token prefix that clearly indicates that it's for development
183+
expiration: 0 # do not expire tokens in a dev environment
184+
notification: 0
177185
mail:
178186
from: no-reply@example.com
179-
revoked-access-tokens:
180-
subject: 'Open VSX Access Tokens Revoked'
181-
template: 'revoked-access-tokens.html'
182-
access-token-expiry:
183-
subject: 'Open VSX Access Token Expiry Notification'
184-
template: 'access-token-expiry-notification.html'
185187
# dynamic tier-based rate limiting configuration
186188
rate-limit:
187189
enabled: false
@@ -204,7 +206,7 @@ ovsx:
204206
"contact": "infrastructure@eclipse-foundation.org"
205207
}
206208
scanning:
207-
enabled: true
209+
enabled: false
208210
# Shared archive limits for all scanning checks (secret detection, blocklist, etc.)
209211
max-archive-size-bytes: 1073741824 # 1 GB total archive limit
210212
max-single-file-bytes: 268435456 # 256 MB per-file limit

server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
*****************************************************************************/
1313
package org.eclipse.openvsx.accesstoken;
1414

15+
import jakarta.annotation.Nonnull;
16+
import jakarta.annotation.PostConstruct;
1517
import org.springframework.beans.factory.annotation.Value;
1618
import org.springframework.context.annotation.Configuration;
19+
import org.springframework.util.StringUtils;
1720

1821
import java.time.Duration;
1922

@@ -27,7 +30,7 @@ public class AccessTokenConfig {
2730
* Default: {@code ''}
2831
*/
2932
@Value("#{'${ovsx.access-token.prefix:${ovsx.token-prefix:}}'}")
30-
String prefix;
33+
private String prefix;
3134

3235
/**
3336
* The expiration period for personal access tokens.
@@ -38,7 +41,7 @@ public class AccessTokenConfig {
3841
* Default: {@code P90D}, expires in 90 days
3942
*/
4043
@Value("${ovsx.access-token.expiration:P90D}")
41-
Duration expiration;
44+
private Duration expiration;
4245

4346
/**
4447
* The duration before the expiration of an access token
@@ -48,7 +51,16 @@ public class AccessTokenConfig {
4851
* Default: {@code P7D}, 7 days prior to expiration
4952
*/
5053
@Value("${ovsx.access-token.notification:P7D}")
51-
Duration notification;
54+
private Duration notification;
55+
56+
/**
57+
* Whether an email shall be sent when a token has expired.
58+
* <p>
59+
* Property: {@code ovsx.access-token.send-expired-mail}
60+
* Default: {@code true}
61+
*/
62+
@Value("${ovsx.access-token.send-expired-mail:false}")
63+
private boolean sendExpiredMail;
5264

5365
/**
5466
* The maximum number of expiring token notifications to handle
@@ -58,7 +70,7 @@ public class AccessTokenConfig {
5870
* Default: {@code 100}
5971
*/
6072
@Value("${ovsx.access-token.max-token-notifications:100}")
61-
int maxTokenNotifications;
73+
private int maxTokenNotifications;
6274

6375
/**
6476
* The cron schedule for the job to disable expired
@@ -68,7 +80,7 @@ public class AccessTokenConfig {
6880
* Default: every 15 min
6981
*/
7082
@Value("${ovsx.access-token.expiration-schedule:0 */15 * * * *}")
71-
String expirationSchedule;
83+
private String expirationSchedule;
7284

7385
/**
7486
* The cron schedule for the job to send out notifications
@@ -78,5 +90,75 @@ public class AccessTokenConfig {
7890
* Default: every 15 min
7991
*/
8092
@Value("${ovsx.access-token.notification-schedule:30 */15 * * * *}")
81-
String notificationSchedule;
93+
private String notificationSchedule;
94+
95+
@Value("${ovsx.data.mirror.enabled:false}")
96+
private boolean mirrorEnabled;
97+
98+
public @Nonnull String getPrefix() {
99+
return this.prefix;
100+
}
101+
102+
public boolean isTokenExpiryEnabled() {
103+
return this.expiration.isPositive();
104+
}
105+
106+
public @Nonnull Duration getExpiration() {
107+
return this.expiration;
108+
}
109+
110+
public boolean isTokenExpiryNotificationEnabled() {
111+
return this.notification.isPositive();
112+
}
113+
114+
public @Nonnull Duration getNotification() {
115+
return this.notification;
116+
}
117+
118+
public boolean isSendExpiredMailEnabled() {
119+
return this.sendExpiredMail;
120+
}
121+
122+
public int getMaxTokenNotifications() {
123+
return this.maxTokenNotifications;
124+
}
125+
126+
public boolean hasExpirationSchedule() {
127+
return StringUtils.hasText(this.expirationSchedule);
128+
}
129+
130+
public @Nonnull String getExpirationSchedule() {
131+
return this.expirationSchedule;
132+
}
133+
134+
public boolean hasNotificationSchedule() {
135+
return StringUtils.hasText(this.notificationSchedule);
136+
}
137+
138+
public @Nonnull String getNotificationSchedule() {
139+
return this.notificationSchedule;
140+
}
141+
142+
@PostConstruct
143+
public void validate() {
144+
if (isTokenExpiryEnabled() && mirrorEnabled) {
145+
throw new IllegalArgumentException(
146+
"ovsx.access-token.expiration can not be enabled when mirror mode is active, got: " + expiration);
147+
}
148+
149+
if (expiration.isNegative()) {
150+
throw new IllegalArgumentException(
151+
"ovsx.access-token.expiration must be a non-negative duration, got: " + expiration);
152+
}
153+
154+
if (notification.isNegative()) {
155+
throw new IllegalArgumentException(
156+
"ovsx.access-token.notification must be a non-negative duration, got: " + notification);
157+
}
158+
159+
if (maxTokenNotifications < 0) {
160+
throw new IllegalArgumentException(
161+
"ovsx.access-token.max-token-notifications must be a non-negative number, got: " + maxTokenNotifications);
162+
}
163+
}
82164
}

server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.stereotype.Service;
2727

2828
import java.time.LocalDateTime;
29+
import java.util.List;
2930
import java.util.UUID;
3031

3132
import static org.eclipse.openvsx.util.UrlUtil.createApiUrl;
@@ -59,8 +60,8 @@ public AccessTokenJson createAccessToken(UserData user, String description) {
5960
var createdAt = TimeUtil.getCurrentUTC();
6061
token.setCreatedTimestamp(createdAt);
6162

62-
if (config.expiration != null && config.expiration.isPositive()) {
63-
token.setExpiresTimestamp(createdAt.plus(config.expiration));
63+
if (config.isTokenExpiryEnabled()) {
64+
token.setExpiresTimestamp(createdAt.plus(config.getExpiration()));
6465
}
6566

6667
token.setDescription(description);
@@ -77,7 +78,7 @@ public AccessTokenJson createAccessToken(UserData user, String description) {
7778
public String generateTokenValue() {
7879
String value;
7980
do {
80-
value = config.prefix + UUID.randomUUID();
81+
value = config.getPrefix() + UUID.randomUUID();
8182
} while (repositories.hasAccessToken(value));
8283
return value;
8384
}
@@ -108,9 +109,14 @@ public PersonalAccessToken useAccessToken(String tokenValue) {
108109
return token;
109110
}
110111

111-
@Transactional
112112
public int expireAccessTokens() {
113-
return repositories.expireAccessTokens(TimeUtil.getCurrentUTC());
113+
var expiredAccessTokens = repositories.expireAccessTokens(TimeUtil.getCurrentUTC());
114+
if (config.isSendExpiredMailEnabled()) {
115+
for (var token : expiredAccessTokens) {
116+
mail.scheduleAccessTokenExpiredMail(token);
117+
}
118+
}
119+
return expiredAccessTokens.size();
114120
}
115121

116122
@Transactional
@@ -123,6 +129,10 @@ public void scheduleTokenExpirationNotification(PersonalAccessToken token) {
123129
}
124130
}
125131

132+
public void scheduleTokenExpiredMail(PersonalAccessToken token) {
133+
mail.scheduleAccessTokenExpiredMail(token);
134+
}
135+
126136
@Transactional
127137
public int setExpirationTimeForLegacyAccessTokens(LocalDateTime expirationTime) {
128138
return repositories.updateExpiresTimeForLegacyAccessTokens(expirationTime);

server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public LegacyPersonalAccessTokenExpirationHandler(AccessTokenConfig config, Acce
3131

3232
@Override
3333
public void run(HandlerJobRequest<?> handlerJobRequest) throws Exception {
34-
if (config.expiration != null && config.expiration.isPositive()) {
35-
var expirationTime = TimeUtil.getCurrentUTC().plus(config.expiration);
34+
if (config.isTokenExpiryEnabled()) {
35+
var expirationTime = TimeUtil.getCurrentUTC().plus(config.getExpiration());
3636
var count = tokens.setExpirationTimeForLegacyAccessTokens(expirationTime);
3737
if (count > 0) {
3838
logger.info("Set expiration time for {} legacy personal access token(s)", count);

server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ public NotifyPersonalAccessTokenExpirationHandler(
3939

4040
@Override
4141
public void run(HandlerJobRequest<?> handlerJobRequest) throws Exception {
42-
if (config.notification.isPositive()) {
43-
var expireBefore = TimeUtil.getCurrentUTC().plus(config.notification);
44-
var page = PageRequest.of(0, config.maxTokenNotifications);
42+
if (config.isTokenExpiryNotificationEnabled()) {
43+
var expireBefore = TimeUtil.getCurrentUTC().plus(config.getNotification());
44+
var page = PageRequest.of(0, config.getMaxTokenNotifications());
4545
var expiringAccessTokens = repositories.findExpiringAccessTokensWithoutNotification(expireBefore, page);
4646
for (var token : expiringAccessTokens) {
4747
tokens.scheduleTokenExpirationNotification(token);

server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,29 @@ public ScheduleTokenExpirationJobs(AccessTokenConfig config, JobRequestScheduler
3737

3838
@EventListener
3939
public void scheduleJobs(ApplicationStartedEvent event) {
40-
if (config.expiration != null && config.expiration.isPositive()) {
40+
var expirationEnabled = config.isTokenExpiryEnabled();
41+
var notificationEnabled = config.isTokenExpiryNotificationEnabled();
42+
43+
if (expirationEnabled) {
4144
scheduler.enqueue(new HandlerJobRequest<>(LegacyPersonalAccessTokenExpirationHandler.class));
4245
}
4346

44-
if (StringUtils.hasText(config.expirationSchedule)) {
45-
logger.info("Scheduling access token expiration job with schedule '{}'", config.expirationSchedule);
47+
if (expirationEnabled && config.hasExpirationSchedule()) {
48+
logger.info("Scheduling access token expiration job with schedule '{}'", config.getExpirationSchedule());
4649
scheduler.scheduleRecurrently(
4750
"access-token-expiration",
48-
config.expirationSchedule,
51+
config.getExpirationSchedule(),
4952
new HandlerJobRequest<>(ExpirePersonalAccessTokensHandler.class)
5053
);
5154
} else {
5255
scheduler.deleteRecurringJob("access-token-expiration");
5356
}
5457

55-
if (StringUtils.hasText(config.notificationSchedule) && config.notification.isPositive()) {
56-
logger.info("Scheduling access token notification job with schedule '{}'", config.notificationSchedule);
58+
if (expirationEnabled && notificationEnabled && config.hasNotificationSchedule()) {
59+
logger.info("Scheduling access token notification job with schedule '{}'", config.getNotificationSchedule());
5760
scheduler.scheduleRecurrently(
5861
"access-token-notification",
59-
config.notificationSchedule,
62+
config.getNotificationSchedule(),
6063
new HandlerJobRequest<>(NotifyPersonalAccessTokenExpirationHandler.class));
6164
} else {
6265
scheduler.deleteRecurringJob("access-token-notification");

0 commit comments

Comments
 (0)