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
100 changes: 34 additions & 66 deletions server/src/main/java/org/eclipse/openvsx/RegistryAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.openvsx.entities.SemanticVersion;
import org.eclipse.openvsx.json.*;
Expand All @@ -26,10 +28,8 @@
import org.eclipse.openvsx.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
Expand All @@ -43,6 +43,7 @@
import static org.eclipse.openvsx.util.TargetPlatform.*;

@RestController
@Validated
public class RegistryAPI {
private static final int REVIEW_TITLE_SIZE = 255;
private static final int REVIEW_COMMENT_SIZE = 2048;
Expand Down Expand Up @@ -191,18 +192,6 @@ private String namespaceNotFoundMessage(String namespace) {
return "Namespace not found: " + namespace;
}

private String negativeSizeMessage() {
return negativeParameterMessage("size");
}

private String negativeOffsetMessage() {
return negativeParameterMessage("offset");
}

private String negativeParameterMessage(String field) {
return "The parameter '" + field + "' must not be negative.";
}

@GetMapping(
path = "/api/{namespace}/logo/{fileName}",
produces = { MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE }
Expand Down Expand Up @@ -436,9 +425,12 @@ public ResponseEntity<VersionsJson> getVersions(
@PathVariable @Parameter(description = "Extension name", example = "vim")
String extension,
@RequestParam(defaultValue = "18")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 100, message = "parameter must not exceed 100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "100", defaultValue = "18"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
Expand Down Expand Up @@ -479,24 +471,19 @@ public ResponseEntity<VersionsJson> getVersions(
)
String targetPlatform,
@RequestParam(defaultValue = "18")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 100, message = "parameter must not exceed 100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "100", defaultValue = "18"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
return handleGetVersions(namespace, extension, targetPlatform, size, offset);
}

private ResponseEntity<VersionsJson> handleGetVersions(String namespace, String extension, String targetPlatform, int size, int offset) {
if (size < 0) {
var json = VersionsJson.error(negativeSizeMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
var json = VersionsJson.error(negativeOffsetMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
for (var registry : getRegistries()) {
try {
return ResponseEntity.ok()
Expand Down Expand Up @@ -534,9 +521,12 @@ public ResponseEntity<VersionReferencesJson> getVersionReferences(
@PathVariable @Parameter(description = "Extension name", example = "svelte-vscode")
String extension,
@RequestParam(defaultValue = "18")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 100, message = "parameter must not exceed 100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "100", defaultValue = "18"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
Expand Down Expand Up @@ -577,24 +567,19 @@ public ResponseEntity<VersionReferencesJson> getVersionReferences(
)
String targetPlatform,
@RequestParam(defaultValue = "18")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 100, message = "parameter must not exceed 100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "100", defaultValue = "18"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
return handleGetVersionReferences(namespace, extension, targetPlatform, size, offset);
}

private ResponseEntity<VersionReferencesJson> handleGetVersionReferences(String namespace, String extension, String targetPlatform, int size, int offset) {
if (size < 0) {
var json = VersionReferencesJson.error(negativeSizeMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
var json = VersionReferencesJson.error(negativeOffsetMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
for (var registry : getRegistries()) {
try {
return ResponseEntity.ok()
Expand Down Expand Up @@ -792,9 +777,12 @@ public ResponseEntity<SearchResultJson> search(
)
String targetPlatform,
@RequestParam(defaultValue = "18")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 1000, message = "parameter must not exceed 1000")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "1000", defaultValue = "18"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset,
@RequestParam(defaultValue = "desc")
Expand All @@ -807,15 +795,6 @@ public ResponseEntity<SearchResultJson> search(
@Parameter(description = "Whether to include information on all available versions for each returned entry")
boolean includeAllVersions
) {
if (size < 0) {
var json = SearchResultJson.error(negativeSizeMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
var json = SearchResultJson.error(negativeOffsetMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}

var options = new ISearchService.Options(query, category, targetPlatform, size, offset, sortOrder, sortBy, includeAllVersions, null);
var resultOffset = 0;
var resultSize = 0;
Expand Down Expand Up @@ -918,21 +897,16 @@ public ResponseEntity<QueryResultJson> getQueryV2(
)
String targetPlatform,
@RequestParam(defaultValue = "100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "100"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 1000, message = "parameter must not exceed 1000")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "1000", defaultValue = "100"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
if (size < 0) {
var json = QueryResultJson.error(negativeSizeMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
var json = QueryResultJson.error(negativeOffsetMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if(!List.of("true", "false", "links").contains(includeAllVersions)) {
if (!List.of("true", "false", "links").contains(includeAllVersions)) {
var json = QueryResultJson.error("Invalid includeAllVersions value: " + includeAllVersions + ".");
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
Expand Down Expand Up @@ -1035,21 +1009,15 @@ public ResponseEntity<QueryResultJson> getQuery(
)
String targetPlatform,
@RequestParam(defaultValue = "100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "100"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 1000, message = "parameter must not exceed 1000")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "1000", defaultValue = "100"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
if (size < 0) {
var json = QueryResultJson.error(negativeSizeMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
var json = QueryResultJson.error(negativeOffsetMessage());
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}

var request = new QueryRequest(
namespaceName,
extensionName,
Expand Down
5 changes: 5 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 @@ -42,6 +42,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.util.Streamable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -203,6 +204,10 @@ public ResponseEntity<Page<PersistedLogJson>> getLog(
try {
admins.checkAdminUser();

if (pageable.getPageSize() > 1000) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Page size must not exceed 1000");
}

Page<PersistedLog> logsPage;
if (StringUtils.isEmpty(periodString)) {
logsPage = repositories.findPersistedLogsPaginated(pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.eclipse.openvsx.entities.FileDecision;
import org.eclipse.openvsx.json.*;
import org.eclipse.openvsx.repositories.RepositoryService;
Expand All @@ -27,6 +29,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
Expand All @@ -38,6 +41,7 @@
* Provides endpoints for managing file-level security decisions.
*/
@RestController
@Validated
@RequestMapping("/admin/scans")
@ApiResponse(
responseCode = "403",
Expand Down Expand Up @@ -82,9 +86,12 @@ public ResponseEntity<FileDecisionListJson> getFiles(
@Parameter(description = "Filter by display name, extension name, or file name")
String name,
@RequestParam(defaultValue = "18")
@Parameter(description = "Maximum number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 100, message = "parameter must not be larger than 100")
@Parameter(description = "Maximum number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "100", defaultValue = "18"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset,
@RequestParam(defaultValue = "dateDecided")
Expand All @@ -103,13 +110,6 @@ public ResponseEntity<FileDecisionListJson> getFiles(
try {
admins.checkAdminUser();

if (size < 0) {
throw new ErrorResultException("Parameter 'size' must be >= 0", HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
throw new ErrorResultException("Parameter 'offset' must be >= 0", HttpStatus.BAD_REQUEST);
}

var decidedFrom = parseUtcDateTime(dateDecidedFrom, "dateDecidedFrom");
var decidedTo = parseUtcDateTime(dateDecidedTo, "dateDecidedTo");
var dbSortField = toFileSortField(sortBy);
Expand Down
26 changes: 13 additions & 13 deletions server/src/main/java/org/eclipse/openvsx/admin/ScanAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.json.*;
import org.eclipse.openvsx.repositories.RepositoryService;
Expand All @@ -32,6 +34,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
Expand All @@ -51,6 +54,7 @@
* Used by the admin dashboard to monitor extension validation and scanning.
*/
@RestController
@Validated
@RequestMapping("/admin/scans")
@ApiResponse(
responseCode = "403",
Expand Down Expand Up @@ -205,11 +209,11 @@ public ResponseEntity<ScanStatisticsJson> getScanCounts(

/**
* Enforcement filter for Scan API /scans/counts.
*
* <p>
* - ALL: no filtering
* - ENFORCED: scans that have at least one enforced validation or threat
* - NOT_ENFORCED: scans that have at least one non-enforced validation or threat
*
* <p>
* Threat scanning enforcement will be added when threats are persisted.
*/
private enum EnforcementFilter {
Expand Down Expand Up @@ -273,9 +277,12 @@ public ResponseEntity<ScanResultListJson> getAllScans(
@Parameter(description = "Filter by display name or extension name (partial matches supported)")
String name,
@RequestParam(defaultValue = "10")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "10"))
@Min(value = 0, message = "parameter must not be negative")
@Max(value = 100, message = "parameter must not be larger than 100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", maximum = "100", defaultValue = "10"))
int size,
@RequestParam(defaultValue = "0")
@Min(value = 0, message = "parameter must not be negative")
@Parameter(description = "Number of entries to skip", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset,
@RequestParam(defaultValue = "scanEndTime")
Expand Down Expand Up @@ -324,13 +331,6 @@ public ResponseEntity<ScanResultListJson> getAllScans(
try {
admins.checkAdminUser();

if (size < 0) {
throw new ErrorResultException("Parameter 'size' must be >= 0", HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
throw new ErrorResultException("Parameter 'offset' must be >= 0", HttpStatus.BAD_REQUEST);
}

var statusFilter = parseStatusFilter(status);
var normalizedPublisher = normalizeSearch(publisher);
var normalizedNamespace = normalizeSearch(namespace);
Expand Down Expand Up @@ -624,12 +624,12 @@ public ResponseEntity<ScanResultJson> retryFailedScannerJobs(
* Make security decisions for one or more quarantined scans.
* Only valid for scans with QUARANTINED status.
* Pass a single scanId for individual decisions, or multiple scanIds for bulk operations.
*
* <p>
* When a scan is allowed:
* - The extension is automatically activated
* - The scan status is updated to PASSED
* - File decisions are created to add enforced threat files to allow list
*
* <p>
* When a scan is blocked:
* - The extension remains inactive
* - File decisions are created to add enforced threat files to block list
Expand Down Expand Up @@ -1100,7 +1100,7 @@ private LocalDateTime parseUtcDateTime(String raw, String paramName) {

/**
* Parses one or multiple status filter values into a set of ScanStatus values.
*
* <p>
* Supports:
* - one value: status=PASSED
* - multiple values (comma-separated with explode=false): status=PASSED,ERROR
Expand Down
Loading