Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ private boolean isVerified(ExtensionVersion extVersion) {
}

var user = extVersion.getPublishedWith().getUser();
if (UserData.ROLE_PRIVILEGED.equals(user.getRole())) {
if (UserData.Role.PRIVILEGED.equals(user.getRole())) {
return true;
}

Expand All @@ -1124,7 +1124,7 @@ private boolean isVerified(ExtensionVersion extVersion, Map<Long, List<Namespace
}

var user = extVersion.getPublishedWith().getUser();
if(UserData.ROLE_PRIVILEGED.equals(user.getRole())) {
if(UserData.Role.PRIVILEGED.equals(user.getRole())) {
return true;
}

Expand Down
2 changes: 0 additions & 2 deletions server/src/main/java/org/eclipse/openvsx/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ public UserJson getUserData() {
}
var json = user.toUserJson();
var serverUrl = UrlUtil.getBaseUrl();
json.setRole(user.getRole());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the role is not part anymore of the User object, the frontend will not be able to check if the current user is an admin I guess?

Copy link
Copy Markdown
Member Author

@gnugomez gnugomez May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserData::toUserJson is already setting the role, here we're just adding it twice

json.setTokensUrl(createApiUrl(serverUrl, "user", "tokens"));
json.setCreateTokenUrl(createApiUrl(serverUrl, "user", "token", "create"));
eclipse.enrichUserJsonWithPublisherAgreement(json, user);
Expand Down Expand Up @@ -551,7 +550,6 @@ public ResponseEntity<UserJson> signPublisherAgreement() {
var agreement = eclipse.signPublisherAgreement(user);
var json = user.toUserJson();
var serverUrl = UrlUtil.getBaseUrl();
json.setRole(user.getRole());
json.setTokensUrl(createApiUrl(serverUrl, "user", "tokens"));
json.setCreateTokenUrl(createApiUrl(serverUrl, "user", "token", "create"));
eclipse.enrichUserJson(json, user, agreement);
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/java/org/eclipse/openvsx/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public UserData findLoggedInUser() {
}

public boolean hasPublishPermission(UserData user, Namespace namespace) {
if (UserData.ROLE_PRIVILEGED.equals(user.getRole())) {
if (UserData.Role.PRIVILEGED.equals(user.getRole())) {
// Privileged users can publish to every namespace.
return true;
}
Expand Down
41 changes: 41 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.eclipse.openvsx.json.ResultJson;
import org.eclipse.openvsx.json.StatsJson;
import org.eclipse.openvsx.json.TargetPlatformVersionJson;
import org.eclipse.openvsx.json.UserRelationshipsJson;
import org.eclipse.openvsx.json.UserPublishInfoJson;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.search.SearchUtilService;
Expand Down Expand Up @@ -163,6 +164,24 @@ public ResponseEntity<StatsJson> getStats() {
}
}

@GetMapping(
path = "/admin/users",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Page<UserRelationshipsJson>> getUsers(
Pageable pageable,
@RequestParam(name = "search", required = false) String search,
@RequestParam(name = "role", required = false) String role
) {
try {
admins.checkAdminUser();
return ResponseEntity.ok(admins.searchUsers(search, role, pageable));
} catch (ErrorResultException exc) {
var status = exc.getStatus() != null ? exc.getStatus() : HttpStatus.BAD_REQUEST;
throw new ResponseStatusException(status);
}
}

@GetMapping(
path = "/admin/log",
produces = MediaType.TEXT_PLAIN_VALUE
Expand Down Expand Up @@ -375,6 +394,28 @@ public ResponseEntity<ResultJson> deleteReview(
}
}

@PostMapping(
path = "/admin/user/{provider}/{loginName}/role",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<ResultJson> updateUserRole(
@PathVariable String provider,
@PathVariable String loginName,
@RequestParam
@Parameter(
description = "The role to assign to the user, or 'none' to remove their role",
schema = @Schema(allowableValues = {"admin", "privileged", "none"})
)
String role
) {
try {
var adminUser = admins.checkAdminUser();
return ResponseEntity.ok(admins.updateUserRole(provider, loginName, role, adminUser));
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
}
}

@GetMapping(
path = "/admin/namespace/{namespaceName}",
produces = MediaType.APPLICATION_JSON_VALUE
Expand Down
49 changes: 48 additions & 1 deletion server/src/main/java/org/eclipse/openvsx/admin/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.eclipse.openvsx.json.NamespaceJson;
import org.eclipse.openvsx.json.ResultJson;
import org.eclipse.openvsx.json.TargetPlatformVersionJson;
import org.eclipse.openvsx.json.UserRelationshipsJson;
import org.eclipse.openvsx.json.UserPublishInfoJson;
import org.eclipse.openvsx.mail.MailService;
import org.eclipse.openvsx.migration.HandlerJobRequest;
Expand All @@ -62,6 +63,8 @@
import org.jobrunr.scheduling.cron.Cron;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -482,6 +485,42 @@ public UserPublishInfoJson getUserPublishInfo(String provider, String loginName)
return userPublishInfo;
}

public Page<UserRelationshipsJson> searchUsers(String search, String role, Pageable pageable) {
return repositories.searchUsers(search, role, pageable)
.map(user -> {
var json = new UserRelationshipsJson();
json.setUser(user.toUserJson());
json.setNamespaces(repositories.findMemberships(user).stream()
.map(membership -> membership.getNamespace().toNamespaceDetailsJson())
.toList());
json.setCustomers(repositories.findCustomerMemberships(user).stream()
.map(membership -> membership.getCustomer().toJson())
.toList());
return json;
});
}

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson updateUserRole(String provider, String loginName, String role, UserData admin) {
var user = repositories.findUserByLoginName(provider, loginName);
if (user == null) {
throw new ErrorResultException(userNotFoundMessage(provider + "/" + loginName), HttpStatus.NOT_FOUND);
}

var updatedRole = "none".equalsIgnoreCase(role) ? null : parseRole(role);
if (Objects.equals(user.getRole(), updatedRole)) {
return ResultJson.success("User " + provider + "/" + loginName + " already has the role " + user.getRole() + ".");
}

user.setRole(updatedRole);
var message = updatedRole == null
? "Removed role from user " + provider + "/" + loginName + "."
: "Updated role for user " + provider + "/" + loginName + " to " + updatedRole + ".";
var result = ResultJson.success(message);
logs.logAction(admin, result);
return result;
}

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson revokePublisherContributions(String provider, String loginName, UserData admin) {
var user = repositories.findUserByLoginName(provider, loginName);
Expand Down Expand Up @@ -554,12 +593,20 @@ public UserData checkAdminUser(String tokenValue) {
}

private UserData checkAdminUser(UserData user) {
if (user == null || !UserData.ROLE_ADMIN.equals(user.getRole())) {
if (user == null || !UserData.Role.ADMIN.equals(user.getRole())) {
throw new ErrorResultException("Administration role is required.", HttpStatus.FORBIDDEN);
}
return user;
}

private UserData.Role parseRole(String role) {
try {
return UserData.Role.valueOfIgnoreCase(role);
} catch (IllegalArgumentException ignored) {
throw new ErrorResultException("Invalid role: " + role, HttpStatus.BAD_REQUEST);
}
}

public AdminStatistics getAdminStatistics(int year, int month) throws ErrorResultException {
validateYearAndMonth(year, month);
var statistics = repositories.findAdminStatisticsByYearAndMonth(year, month);
Expand Down
16 changes: 13 additions & 3 deletions server/src/main/java/org/eclipse/openvsx/entities/Customer.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@
*****************************************************************************/
package org.eclipse.openvsx.entities;

import jakarta.persistence.*;
import org.eclipse.openvsx.json.CustomerJson;

import java.io.Serial;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import org.eclipse.openvsx.json.CustomerJson;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;

@Entity
public class Customer implements Serializable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import jakarta.persistence.*;
import org.eclipse.openvsx.json.CustomerMembershipJson;
import org.eclipse.openvsx.json.NamespaceMembershipJson;

import java.io.Serial;
import java.io.Serializable;
Expand Down
38 changes: 30 additions & 8 deletions server/src/main/java/org/eclipse/openvsx/entities/UserData.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,51 @@
********************************************************************************/
package org.eclipse.openvsx.entities;

import jakarta.persistence.*;
import org.eclipse.openvsx.json.UserJson;

import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.eclipse.openvsx.json.UserJson;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.SequenceGenerator;

@Entity
public class UserData implements Serializable {

@Serial
private static final long serialVersionUID = 1L;

public static final String ROLE_ADMIN = "admin";
public static final String ROLE_PRIVILEGED = "privileged";
public enum Role {
ADMIN,
PRIVILEGED;

public static Role valueOfIgnoreCase(String value) {
if (value == null) return null;
return Role.valueOf(value.trim().toUpperCase());
}

@Override
public String toString() {
return name().toLowerCase();
}
}

@Id
@GeneratedValue(generator = "userDataSeq")
@SequenceGenerator(name = "userDataSeq", sequenceName = "user_data_seq")
private long id;

@Column(length = 32)
private String role;
@Convert(converter = UserRoleConverter.class)
private Role role;

private String loginName;

Expand Down Expand Up @@ -72,6 +93,7 @@ public UserJson toUserJson() {
json.setAvatarUrl(this.getAvatarUrl());
json.setHomepage(this.getProviderUrl());
json.setProvider(this.getProvider());
json.setRole(Optional.ofNullable(this.getRole()).map(Role::toString).orElse(null));
return json;
}

Expand All @@ -83,11 +105,11 @@ public void setId(long id) {
this.id = id;
}

public String getRole() {
public Role getRole() {
return role;
}

public void setRole(String role) {
public void setRole(Role role) {
this.role = role;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* 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.entities;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class UserRoleConverter implements AttributeConverter<UserData.Role, String> {

@Override
public String convertToDatabaseColumn(UserData.Role role) {
return role != null ? role.toString() : null;
}

@Override
public UserData.Role convertToEntityAttribute(String value) {
return UserData.Role.valueOfIgnoreCase(value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/
package org.eclipse.openvsx.json;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

import java.util.List;

@JsonInclude(Include.NON_NULL)
public class UserRelationshipsJson {

private UserJson user;
private List<NamespaceDetailsJson> namespaces;
private List<CustomerJson> customers;

public UserJson getUser() {
return user;
}

public List<NamespaceDetailsJson> getNamespaces() {
return namespaces;
}

public List<CustomerJson> getCustomers() {
return customers;
}

public void setUser(UserJson user) {
this.user = user;
}

public void setNamespaces(List<NamespaceDetailsJson> namespaces) {
this.namespaces = namespaces;
}

public void setCustomers(List<CustomerJson> customers) {
this.customers = customers;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ private ExtensionVersion toExtensionVersionFull(

var user = new UserData();
user.setId(row.get(USER_DATA.ID));
user.setRole(row.get(USER_DATA.ROLE));
user.setRole(UserData.Role.valueOfIgnoreCase(row.get(USER_DATA.ROLE)));
user.setLoginName(row.get(USER_DATA.LOGIN_NAME));
user.setFullName(row.get(USER_DATA.FULL_NAME));
user.setAvatarUrl(row.get(USER_DATA.AVATAR_URL));
Expand Down
Loading