Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/src/dev/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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;
Expand All @@ -73,6 +75,7 @@ public LocalRegistryService(
ExtensionService extensions,
VersionService versions,
UserService users,
AccessTokenService tokens,
SearchUtilService search,
ExtensionValidator validator,
StorageUtilService storageUtil,
Expand All @@ -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;
Expand Down Expand Up @@ -579,7 +583,7 @@ private Map<Long, List<NamespaceMembership>> getMemberships(Collection<Extension

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson createNamespace(NamespaceJson json, String tokenValue) {
var token = users.useAccessToken(tokenValue);
var token = tokens.useAccessToken(tokenValue);
if (token == null) {
throw new ErrorResultException(ACCESS_TOKEN_ERROR);
}
Expand Down Expand Up @@ -631,7 +635,7 @@ public ResultJson createNamespace(NamespaceJson json, UserData user) {
}

public ResultJson verifyToken(String namespaceName, String tokenValue) {
var token = users.useAccessToken(tokenValue);
var token = tokens.useAccessToken(tokenValue);
if (token == null) {
throw new ErrorResultException(ACCESS_TOKEN_ERROR);
}
Expand All @@ -650,16 +654,16 @@ public ResultJson verifyToken(String namespaceName, String tokenValue) {
}

public ExtensionJson publish(InputStream content, UserData user) throws ErrorResultException {
var token = users.createAccessToken(user, "One time use publish token");
var token = tokens.createAccessToken(user, "One time use publish token");
try {
return publish(content, token.getValue());
} finally {
users.deleteAccessToken(user, token.getId());
tokens.deactivateAccessToken(user, token.getId());
}
}

public ExtensionJson publish(InputStream content, String tokenValue) throws ErrorResultException {
var token = users.useAccessToken(tokenValue);
var token = tokens.useAccessToken(tokenValue);
if (token == null || token.getUser() == null) {
throw new ErrorResultException(ACCESS_TOKEN_ERROR);
}
Expand Down
8 changes: 6 additions & 2 deletions server/src/main/java/org/eclipse/openvsx/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package org.eclipse.openvsx;

import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.openvsx.accesstoken.AccessTokenService;
import org.eclipse.openvsx.eclipse.EclipseService;
import org.eclipse.openvsx.entities.ExtensionVersion;
import org.eclipse.openvsx.entities.NamespaceMembership;
Expand Down Expand Up @@ -53,6 +54,7 @@ public class UserAPI {

private final RepositoryService repositories;
private final UserService users;
private final AccessTokenService tokens;
private final EclipseService eclipse;
private final StorageUtilService storageUtil;
private final LocalRegistryService local;
Expand All @@ -62,6 +64,7 @@ public class UserAPI {
public UserAPI(
RepositoryService repositories,
UserService users,
AccessTokenService tokens,
EclipseService eclipse,
StorageUtilService storageUtil,
LocalRegistryService local,
Expand All @@ -70,6 +73,7 @@ public UserAPI(
) {
this.repositories = repositories;
this.users = users;
this.tokens = tokens;
this.eclipse = eclipse;
this.storageUtil = storageUtil;
this.local = local;
Expand Down Expand Up @@ -175,7 +179,7 @@ public ResponseEntity<AccessTokenJson> 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(
Expand All @@ -189,7 +193,7 @@ public ResponseEntity<ResultJson> deleteAccessToken(@PathVariable long id) {
}

try {
return ResponseEntity.ok(users.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);
}
Expand Down
58 changes: 0 additions & 58 deletions server/src/main/java/org/eclipse/openvsx/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/******************************************************************************
* 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.
* <p>
* 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.
* <p>
* If {@code 0} is provided, the access tokens do not expire.
* <p>
* 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.
* <p>
* Property: {@code ovsx.access-token.notification}
* Default: {@code P7D}, 7 days prior to expiration
*/
@Value("${ovsx.access-token.notification:P7D}")
Duration notification;

/**
* The maximum number of expiring token notifications to handle
* within one job execution.
* <p>
* Property: {@code ovsx.access-token.max-token-notifications}
* Default: {@code 100}
*/
@Value("${ovsx.access-token.max-token-notifications:100}")
int maxTokenNotifications;

/**
* The cron schedule for the job to disable expired
* access tokens.
* <p>
* Property: {@code ovsx.access-token.expiration-schedule}
* Default: every 15 min
*/
@Value("${ovsx.access-token.expiration-schedule:0 */15 * * * *}")
String expirationSchedule;

/**
* The cron schedule for the job to send out notifications
* for soon to be expired access tokens.
* <p>
* Property: {@code ovsx.access-token.notification-schedule}
* Default: every 15 min
*/
@Value("${ovsx.access-token.notification-schedule:30 */15 * * * *}")
String notificationSchedule;
}
Loading