Skip to content
Draft
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
100 changes: 95 additions & 5 deletions server/src/main/java/org/eclipse/openvsx/admin/RateLimitAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@
*****************************************************************************/
package org.eclipse.openvsx.admin;

import org.eclipse.openvsx.entities.Customer;
import org.eclipse.openvsx.entities.Tier;
import org.eclipse.openvsx.entities.TierType;
import org.eclipse.openvsx.entities.UsageStats;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.json.*;
import org.eclipse.openvsx.ratelimit.CustomerService;
import org.eclipse.openvsx.ratelimit.cache.RateLimitCacheService;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.LogService;
import org.eclipse.openvsx.util.TimeUtil;
import org.slf4j.Logger;
Expand All @@ -27,7 +26,6 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Optional;


Expand All @@ -39,17 +37,20 @@ public class RateLimitAPI {
private final RepositoryService repositories;
private final AdminService admins;
private final LogService logs;
private final CustomerService customerService;
private RateLimitCacheService rateLimitCacheService;

public RateLimitAPI(
RepositoryService repositories,
AdminService admins,
LogService logs,
CustomerService customerService,
Optional<RateLimitCacheService> rateLimitCacheService
) {
this.repositories = repositories;
this.admins = admins;
this.logs = logs;
this.customerService = customerService;
rateLimitCacheService.ifPresent(service -> this.rateLimitCacheService = service);
}

Expand Down Expand Up @@ -222,6 +223,26 @@ public ResponseEntity<CustomerListJson> getCustomers() {
}
}

@GetMapping(
path = "/customers/{name}",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<CustomerJson> getCustomer(@PathVariable String name) {
try {
admins.checkAdminUser();

var customer = repositories.findCustomer(name);
if (customer == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(customer.toJson());
} catch (Exception exc) {
logger.error("failed retrieving customer {}", name, exc);
return ResponseEntity.internalServerError().build();
}
}

@PostMapping(
path = "/customers/create",
consumes = MediaType.APPLICATION_JSON_VALUE,
Expand Down Expand Up @@ -306,6 +327,75 @@ public ResponseEntity<CustomerJson> updateCustomer(@PathVariable String name, @R
}
}

@GetMapping(
path = "/customers/{name}/members",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<CustomerMembershipListJson> getCustomerMembers(@PathVariable String name) {
try {
admins.checkAdminUser();

var customer = repositories.findCustomer(name);
if (customer == null) {
return ResponseEntity.notFound().build();
}

var memberships = repositories.findCustomerMemberships(customer);
var membershipList = new CustomerMembershipListJson();
membershipList.setCustomerMemberships(memberships.stream().map(CustomerMembership::toJson).toList());
return ResponseEntity.ok(membershipList);
} catch (Exception exc) {
logger.error("failed retrieving customer members {}", name, exc);
return ResponseEntity.internalServerError().build();
}
}

@PostMapping(
path = "/customers/{name}/add-member",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<ResultJson> addCustomerMember(
@PathVariable String name,
@RequestParam("user") String userName,
@RequestParam(required = false) String provider
) {
try {
var admin = admins.checkAdminUser();

var result = customerService.addCustomerMember(name, userName, provider);
logs.logAction(admin, result);
return ResponseEntity.ok(result);
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
} catch (Exception exc) {
logger.error("failed adding user {} to customer {}", userName, name, exc);
return ResponseEntity.internalServerError().build();
}
}

@PostMapping(
path = "/customers/{name}/remove-member",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<ResultJson> removeCustomerMember(
@PathVariable String name,
@RequestParam("user") String userName,
@RequestParam(required = false) String provider
) {
try {
var admin = admins.checkAdminUser();

var result = customerService.removeCustomerMember(name, userName, provider);
logs.logAction(admin, result);
return ResponseEntity.ok(result);
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
} catch (Exception exc) {
logger.error("failed removing user {} from customer {}", userName, name, exc);
return ResponseEntity.internalServerError().build();
}
}

@DeleteMapping(
path = "/customers/{name}",
produces = MediaType.APPLICATION_JSON_VALUE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/******************************************************************************
* 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.entities;

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

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

@Entity
public class CustomerMembership implements Serializable {

@Serial
private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(generator = "customerMembershipSeq")
@SequenceGenerator(name = "customerMembershipSeq", sequenceName = "customer_membership_seq", allocationSize = 1)
private long id;

@ManyToOne
@JoinColumn(name = "customer")
private Customer customer;

@ManyToOne
@JoinColumn(name = "user_data")
private UserData user;

public CustomerMembershipJson toJson() {
return new CustomerMembershipJson(
this.customer.getName(),
this.user.toUserJson()
);
}

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public UserData getUser() {
return user;
}

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

public Customer getCustomer() {
return customer;
}

public void setCustomer(Customer customer) {
this.customer = customer;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomerMembership that = (CustomerMembership) o;
return id == that.id
&& Objects.equals(customer, that.customer)
&& Objects.equals(user, that.user);
}

@Override
public int hashCode() {
return Objects.hash(id, customer, user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/******************************************************************************
* 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.json;

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

@JsonInclude(Include.NON_NULL)
public record CustomerMembershipJson(
String customer,
UserJson user
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/******************************************************************************
* 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.json;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

import java.util.List;

@Schema(
name = "CustomerMembershipList",
description = "Metadata of a customer member list"
)
@JsonInclude(Include.NON_NULL)
public class CustomerMembershipListJson extends ResultJson {

public static CustomerMembershipListJson error(String message) {
var result = new CustomerMembershipListJson();
result.setError(message);
return result;
}

@Schema(description = "List of memberships")
@NotNull
private List<CustomerMembershipJson> customerMemberships;

public List<CustomerMembershipJson> getCustomerMemberships() {
return customerMemberships;
}

public void setCustomerMemberships(List<CustomerMembershipJson> customerMemberships) {
this.customerMemberships = customerMemberships;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@
import inet.ipaddr.IPAddressString;
import inet.ipaddr.ipv4.IPv4AddressAssociativeTrie;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.eclipse.openvsx.entities.Customer;
import org.eclipse.openvsx.entities.CustomerMembership;
import org.eclipse.openvsx.entities.NamespaceMembership;
import org.eclipse.openvsx.entities.UserData;
import org.eclipse.openvsx.json.ResultJson;
import org.eclipse.openvsx.ratelimit.cache.ConfigurationChanged;
import org.eclipse.openvsx.ratelimit.cache.RateLimitCacheService;
import org.eclipse.openvsx.ratelimit.config.RateLimitConfig;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.util.ErrorResultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand All @@ -30,15 +37,16 @@
import java.util.Optional;

@Service
@ConditionalOnBean(RateLimitConfig.class)
public class CustomerService {

private final Logger logger = LoggerFactory.getLogger(CustomerService.class);

private final EntityManager entityManager;
private final RepositoryService repositories;
private IPv4AddressAssociativeTrie<Customer> customersByIPAddress;

public CustomerService(RepositoryService repositories) {
public CustomerService(EntityManager entityManager, RepositoryService repositories) {
this.entityManager = entityManager;
this.repositories = repositories;
}

Expand Down Expand Up @@ -84,4 +92,47 @@ private IPv4AddressAssociativeTrie<Customer> rebuildIPAddressCache() {
}
return trie;
}

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson addCustomerMember(String customerName, String userName, String provider) throws ErrorResultException {
var customer = repositories.findCustomer(customerName);
if (customer == null) {
throw new ErrorResultException("Customer not found: " + customerName);
}
var user = repositories.findUserByLoginName(provider, userName);
if (user == null) {
throw new ErrorResultException("User not found: " + (provider + "/" + userName));
}

var membership = repositories.findCustomerMembership(user, customer);
if (membership != null) {
throw new ErrorResultException("User " + user.getLoginName() + " is already member of customer " + customer.getName() + ".");
}

membership = new CustomerMembership();
membership.setCustomer(customer);
membership.setUser(user);
entityManager.persist(membership);
return ResultJson.success("Added " + user.getLoginName() + " as member of customer " + customer.getName() + ".");
}

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson removeCustomerMember(String customerName, String userName, String provider) throws ErrorResultException {
var customer = repositories.findCustomer(customerName);
if (customer == null) {
throw new ErrorResultException("Customer not found: " + customerName);
}
var user = repositories.findUserByLoginName(provider, userName);
if (user == null) {
throw new ErrorResultException("User not found: " + (provider + "/" + userName));
}

var membership = repositories.findCustomerMembership(user, customer);
if (membership == null) {
throw new ErrorResultException("User " + user.getLoginName() + " is not a member of customer " + customer.getName() + ".");
}

entityManager.remove(membership);
return ResultJson.success("Removed " + user.getLoginName() + " as member of customer " + customer.getName() + ".");
}
}
Loading
Loading