diff --git a/server/build.gradle b/server/build.gradle index 571992416..62131a2aa 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -32,7 +32,8 @@ def versions = [ jobrunr: '5.1.2', bucket4j: '0.4.0', ehcache: '3.10.0', - tika: '2.6.0' + tika: '2.6.0', + bouncycastle: '1.69' ] ext['junit-jupiter.version'] = versions.junit sourceCompatibility = versions.java @@ -71,6 +72,7 @@ dependencies { implementation "org.springframework.security:spring-security-oauth2-jose" implementation "org.springframework.session:spring-session-jdbc" implementation "org.springframework.retry:spring-retry" + implementation "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" implementation "org.ehcache:ehcache:${versions.ehcache}" implementation "com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:${versions.bucket4j}" implementation "org.jobrunr:jobrunr-spring-boot-starter:${versions.jobrunr}" diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index 494c812c5..6bdeac926 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -130,3 +130,5 @@ ovsx: base-url: https://api.eclipse.org publisher-agreement: timezone: US/Eastern + integrity: + key-pair: create # create, renew, delete, 'undefined' \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java index f3c432d5e..e568dab68 100644 --- a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java +++ b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java @@ -329,27 +329,19 @@ public void processEachResource(ExtensionVersion extVersion, Consumer getNamespaceLogo(String namespaceName, String fileName); + + String getPublicKey(String publicId); } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java index 58a83b947..33b1975ce 100644 --- a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java @@ -28,6 +28,7 @@ import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.search.ExtensionSearch; import org.eclipse.openvsx.search.ISearchService; @@ -82,6 +83,9 @@ public class LocalRegistryService implements IExtensionRegistry { @Autowired CacheService cache; + @Autowired + ExtensionVersionIntegrityService integrityService; + @Override public NamespaceJson getNamespace(String namespaceName) { var namespace = repositories.findNamespace(namespaceName); @@ -130,7 +134,7 @@ private Map getDownloads(Extension extension, String targetPlatf var download = files != null ? files.get(DOWNLOAD) : null; if(download == null) { var e = ev.getExtension(); - logger.warn("Could not find download for: {}.{}-{}@{}", e.getNamespace().getName(), e.getName(), ev.getVersion(), ev.getTargetPlatform()); + logger.warn("Could not find download for: {}", NamingUtil.toLogFormat(ev)); return null; } else { return new AbstractMap.SimpleEntry<>(ev.getTargetPlatform(), download); @@ -182,7 +186,11 @@ public ResponseEntity getFile(String namespace, String extensionName, St } public boolean isType (String fileName){ - var expectedTypes = List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, DOWNLOAD_SHA256, CHANGELOG, VSIXMANIFEST); + var expectedTypes = new ArrayList<>(List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, DOWNLOAD_SHA256, CHANGELOG, VSIXMANIFEST)); + if(integrityService.isEnabled()) { + expectedTypes.add(DOWNLOAD_SIG); + } + return expectedTypes.stream().anyMatch(fileName::equalsIgnoreCase); } @@ -431,10 +439,26 @@ private SearchEntryJson toSearchEntryJson(Extension extension) { var extVersion = versions.getLatest(extension, null, false, true); var entry = extVersion.toSearchEntryJson(); entry.url = createApiUrl(serverUrl, "api", entry.namespace, entry.name); - entry.files = storageUtil.getFileUrls(extVersion, serverUrl, DOWNLOAD, DOWNLOAD_SHA256, ICON); + entry.files = storageUtil.getFileUrls(extVersion, serverUrl, withFileTypes(DOWNLOAD, ICON)); + if(entry.files.containsKey(DOWNLOAD_SIG)) { + entry.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion)); + } + return entry; } + private String[] withFileTypes(String... types) { + var typesList = new ArrayList<>(List.of(types)); + if(typesList.contains(DOWNLOAD)) { + typesList.add(DOWNLOAD_SHA256); + if(integrityService.isEnabled()) { + typesList.add(DOWNLOAD_SIG); + } + } + + return typesList.toArray(String[]::new); + } + @Override public ResponseEntity getNamespaceLogo(String namespaceName, String fileName) { if(fileName == null) { @@ -515,7 +539,7 @@ private Map> getFileResources(List ex return Collections.emptyMap(); } - var fileTypes = List.of(DOWNLOAD, DOWNLOAD_SHA256, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST); + var fileTypes = List.of(withFileTypes(DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST)); var extensionVersionIds = extensionVersions.stream() .map(ExtensionVersion::getId) .collect(Collectors.toSet()); @@ -674,8 +698,7 @@ public ExtensionJson publish(InputStream content, String tokenValue) throws Erro var semver = extVersion.getSemanticVersion(); var newVersion = String.join(".", String.valueOf(semver.getMajor()), String.valueOf(semver.getMinor() + 1), "0"); - json.warning = "A " + existingRelease + " already exists for " + - extension.getNamespace().getName() + "." + extension.getName() + "-" + extVersion.getVersion() + ".\n" + + json.warning = "A " + existingRelease + " already exists for " + NamingUtil.toLogFormat(extension.getNamespace().getName(), extension.getName(), extVersion.getVersion()) + ".\n" + "To prevent update conflicts, we recommend that this " + thisRelease + " uses " + newVersion + " as its version instead."; } @@ -690,7 +713,8 @@ public ResultJson postReview(ReviewJson review, String namespace, String extensi } var extension = repositories.findExtension(extensionName, namespace); if (extension == null || !extension.isActive()) { - return ResultJson.error("Extension not found: " + namespace + "." + extensionName); + var extensionId = NamingUtil.toExtensionId(namespace, extensionName); + return ResultJson.error("Extension not found: " + extensionId); } var activeReviews = repositories.findActiveReviews(extension, user); if (!activeReviews.isEmpty()) { @@ -711,7 +735,7 @@ public ResultJson postReview(ReviewJson review, String namespace, String extensi search.updateSearchEntry(extension); cache.evictExtensionJsons(extension); cache.evictLatestExtensionVersion(extension); - return ResultJson.success("Added review for " + extension.getNamespace().getName() + "." + extension.getName()); + return ResultJson.success("Added review for " + NamingUtil.toExtensionId(extension)); } @Transactional(rollbackOn = ResponseStatusException.class) @@ -722,7 +746,7 @@ public ResultJson deleteReview(String namespace, String extensionName) { } var extension = repositories.findExtension(extensionName, namespace); if (extension == null || !extension.isActive()) { - return ResultJson.error("Extension not found: " + namespace + "." + extensionName); + return ResultJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extensionName)); } var activeReviews = repositories.findActiveReviews(extension, user); if (activeReviews.isEmpty()) { @@ -738,7 +762,7 @@ public ResultJson deleteReview(String namespace, String extensionName) { search.updateSearchEntry(extension); cache.evictExtensionJsons(extension); cache.evictLatestExtensionVersion(extension); - return ResultJson.success("Deleted review for " + extension.getNamespace().getName() + "." + extension.getName()); + return ResultJson.success("Deleted review for " + NamingUtil.toExtensionId(extension)); } private Extension getExtension(SearchHit searchHit) { @@ -776,8 +800,14 @@ private List toSearchEntries(SearchHits search }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - var fileUrls = storageUtil.getFileUrls(latestVersions.values(), serverUrl, DOWNLOAD, DOWNLOAD_SHA256, ICON); - searchEntries.forEach((extensionId, searchEntry) -> searchEntry.files = fileUrls.get(latestVersions.get(extensionId).getId())); + var fileUrls = storageUtil.getFileUrls(latestVersions.values(), serverUrl, withFileTypes(DOWNLOAD, ICON)); + searchEntries.forEach((extensionId, searchEntry) -> { + var extVersion = latestVersions.get(extensionId); + searchEntry.files = fileUrls.get(extVersion.getId()); + if(searchEntry.files.containsKey(DOWNLOAD_SIG)) { + searchEntry.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion)); + } + }); if (options.includeAllVersions) { var allActiveVersions = repositories.findActiveVersions(extensions).stream() .sorted(ExtensionVersion.SORT_COMPARATOR) @@ -867,8 +897,11 @@ public ExtensionJson toExtensionVersionJson(ExtensionVersion extVersion, String .forEach(e -> json.allVersions.put(e.getKey(), e.getValue())); } - var fileUrls = storageUtil.getFileUrls(List.of(extVersion), serverUrl, DOWNLOAD, DOWNLOAD_SHA256, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST); + var fileUrls = storageUtil.getFileUrls(List.of(extVersion), serverUrl, withFileTypes(DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST)); json.files = fileUrls.get(extVersion.getId()); + if(json.files.containsKey(DOWNLOAD_SIG)) { + json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion)); + } if (json.dependencies != null) { json.dependencies.forEach(ref -> { ref.url = createApiUrl(serverUrl, "api", ref.namespace, ref.extension); @@ -928,12 +961,15 @@ public ExtensionJson toExtensionVersionJson( json.allVersions.put(version, createApiUrl(versionBaseUrl, version)); } - json.files = Maps.newLinkedHashMapWithExpectedSize(6); + json.files = Maps.newLinkedHashMapWithExpectedSize(8); var fileBaseUrl = UrlUtil.createApiFileBaseUrl(serverUrl, json.namespace, json.name, json.targetPlatform, json.version); for (var resource : resources) { var fileUrl = UrlUtil.createApiFileUrl(fileBaseUrl, resource.getName()); json.files.put(resource.getType(), fileUrl); } + if(json.files.containsKey(DOWNLOAD_SIG)) { + json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion)); + } if (json.dependencies != null) { json.dependencies.forEach(ref -> { @@ -1001,12 +1037,15 @@ public ExtensionJson toExtensionVersionJsonV2( } } - json.files = Maps.newLinkedHashMapWithExpectedSize(6); + json.files = Maps.newLinkedHashMapWithExpectedSize(8); var fileBaseUrl = UrlUtil.createApiFileBaseUrl(serverUrl, json.namespace, json.name, json.targetPlatform, json.version); for (var resource : resources) { var fileUrl = UrlUtil.createApiFileUrl(fileBaseUrl, resource.getName()); json.files.put(resource.getType(), fileUrl); } + if(json.files.containsKey(DOWNLOAD_SIG)) { + json.files.put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion)); + } if (json.dependencies != null) { json.dependencies.forEach(ref -> { @@ -1051,4 +1090,13 @@ private boolean isVerified(ExtensionVersion extVersion, Map m.getRole().equalsIgnoreCase(NamespaceMembership.ROLE_OWNER)) && memberships.stream().anyMatch(m -> m.getUser().getId() == user.getId()); } + + public String getPublicKey(String publicId) { + var keyPair = repositories.findKeyPair(publicId); + if(keyPair == null) { + throw new NotFoundException(); + } + + return keyPair.getPublicKeyText(); + } } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java index 9326741a4..995b187e9 100644 --- a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java @@ -273,7 +273,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + namespace + "." + extension); + var json = ExtensionJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extension)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -339,7 +339,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + namespace + "." + extension + " (" + targetPlatform + ")"); + var json = ExtensionJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, targetPlatform.toString(), null)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -394,7 +394,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + namespace + "." + extension + " " + version); + var json = ExtensionJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, version)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -462,7 +462,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + namespace + "." + extension + " " + version + " (" + targetPlatform + ")"); + var json = ExtensionJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, targetPlatform, version)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -633,7 +633,7 @@ public ResponseEntity getReviews( // Try the next registry } } - var json = ReviewListJson.error("Extension not found: " + namespace + "." + extension); + var json = ReviewListJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extension)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -1337,4 +1337,56 @@ public ResponseEntity deleteReview(@PathVariable String namespace, @ return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } } + + @GetMapping( + path = "/api/-/public-key/{publicId}", + produces = MediaType.TEXT_PLAIN_VALUE + ) + @CrossOrigin + @Operation(summary = "Access a public key file") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "The file content is returned" + ), + @ApiResponse( + responseCode = "404", + description = "The specified public key file could not be found", + content = @Content() + ), + @ApiResponse( + responseCode = "429", + description = "A client has sent too many requests in a given amount of time", + content = @Content(), + headers = { + @Header( + name = "X-Rate-Limit-Retry-After-Seconds", + description = "Number of seconds to wait after receiving a 429 response", + schema = @Schema(type = "integer", format = "int32") + ), + @Header( + name = "X-Rate-Limit-Remaining", + description = "Remaining number of requests left", + schema = @Schema(type = "integer", format = "int32") + ) + } + ) + }) + public ResponseEntity getPublicKey( + @PathVariable @Parameter(description = "Public ID of a public key file", example = "92dea4de-80b5-4577-b27d-44cdcda82c63") + String publicId + ) { + for (var registry : getRegistries()) { + try { + var publicKeyText = registry.getPublicKey(publicId); + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic()) + .body(publicKeyText); + } catch (NotFoundException exc) { + // Try the next registry + } + } + + return ResponseEntity.notFound().build(); + } } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java b/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java index 44f6c52e1..57eedcf95 100644 --- a/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java @@ -307,6 +307,23 @@ public QueryResultJson queryV2(QueryRequestV2 request) { } } + public String getPublicKey(String publicId) { + var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/public-key/{publicId}"; + var uriVariables = new HashMap(); + uriVariables.put("publicId", publicId); + + try { + return restTemplate.getForObject(urlTemplate, String.class, uriVariables); + } catch (RestClientException exc) { + if(!isNotFound(exc)) { + var url = UriComponentsBuilder.fromUriString(urlTemplate).build(uriVariables); + logger.error("GET " + url, exc); + } + + throw new NotFoundException(); + } + } + private void handleError(Throwable exc) throws RuntimeException { if (exc instanceof HttpStatusCodeException) { var status = ((HttpStatusCodeException) exc).getStatusCode(); diff --git a/server/src/main/java/org/eclipse/openvsx/UserAPI.java b/server/src/main/java/org/eclipse/openvsx/UserAPI.java index a4d4c78f4..19573b595 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/UserAPI.java @@ -189,14 +189,14 @@ public List getOwnExtensions() { throw new ResponseStatusException(HttpStatus.FORBIDDEN); } + var types = new String[]{ DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST }; return repositories.findExtensions(user) .map(e -> versions.getLatestTrxn(e, null, false, false)) .map(latest -> { var json = latest.toExtensionJson(); json.preview = latest.isPreview(); json.active = latest.getExtension().isActive(); - json.files = storageUtil.getFileUrls(latest, UrlUtil.getBaseUrl(), - DOWNLOAD, DOWNLOAD_SHA256, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST); + json.files = storageUtil.getFileUrls(latest, UrlUtil.getBaseUrl(), types); return json; }) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java index d7d5795c8..169a85902 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java @@ -9,6 +9,7 @@ * ****************************************************************************** */ package org.eclipse.openvsx.adapter; +import org.eclipse.openvsx.util.NamingUtil; import org.eclipse.openvsx.util.NotFoundException; import org.springframework.web.server.ResponseStatusException; @@ -76,7 +77,7 @@ private Iterable getVSCodeServices() { private void mergeExtensionQueryResults(List extensions, Set extensionIds, List subExtensions, int limit) { if(extensionIds.isEmpty() && !extensions.isEmpty()) { var extensionIdSet = extensions.stream() - .map(extension -> extension.publisher.publisherName + "." + extension.extensionName) + .map(extension -> NamingUtil.toExtensionId(extension)) .collect(Collectors.toSet()); extensionIds.addAll(extensionIdSet); @@ -85,7 +86,7 @@ private void mergeExtensionQueryResults(List ext var subExtensionsIter = subExtensions.iterator(); while (subExtensionsIter.hasNext() && extensions.size() < limit) { var subExtension = subExtensionsIter.next(); - var key = subExtension.publisher.publisherName + "." + subExtension.extensionName; + var key = NamingUtil.toExtensionId(subExtension); if(!extensionIds.contains(key)) { extensions.add(subExtension); extensionIds.add(key); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/ExtensionQueryResult.java b/server/src/main/java/org/eclipse/openvsx/adapter/ExtensionQueryResult.java index f9adb8d96..09fc6c7bd 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/ExtensionQueryResult.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/ExtensionQueryResult.java @@ -85,6 +85,8 @@ public static class ExtensionFile { public static final String FILE_LICENSE = "Microsoft.VisualStudio.Services.Content.License"; public static final String FILE_WEB_RESOURCES = "Microsoft.VisualStudio.Code.WebResources/"; public static final String FILE_VSIXMANIFEST = "Microsoft.VisualStudio.Services.VsixManifest"; + public static final String FILE_SIGNATURE = "Microsoft.VisualStudio.Services.VsixSignature"; + public static final String FILE_PUBLIC_KEY = "Microsoft.VisualStudio.Services.PublicKey"; public String assetType; public String source; diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java index dfe519cf7..23250f35f 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -16,6 +16,7 @@ import org.eclipse.openvsx.entities.ExtensionVersion; import org.eclipse.openvsx.entities.FileResource; import org.eclipse.openvsx.entities.Namespace; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.search.SearchUtilService; import org.eclipse.openvsx.storage.StorageUtilService; @@ -27,6 +28,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.server.ResponseStatusException; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.TimeUnit; @@ -57,6 +59,9 @@ public class LocalVSCodeService implements IVSCodeService { @Autowired StorageUtilService storageUtil; + @Autowired + ExtensionVersionIntegrityService integrityService; + @Value("${ovsx.webui.url:}") String webuiUrl; @@ -175,7 +180,11 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul Map> fileResources; if (test(flags, FLAG_INCLUDE_FILES) && !extensionVersionsMap.isEmpty()) { - var types = List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, CHANGELOG, VSIXMANIFEST); + var types = new ArrayList<>(List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, CHANGELOG, VSIXMANIFEST)); + if(integrityService.isEnabled()) { + types.add(DOWNLOAD_SIG); + } + var idsMap = extensionVersionsMap.values().stream() .flatMap(Collection::stream) .collect(Collectors.toMap(ev -> ev.getId(), ev -> ev)); @@ -281,6 +290,21 @@ public ResponseEntity getAsset( } var asset = (restOfTheUrl != null && restOfTheUrl.length() > 0) ? (assetType + "/" + restOfTheUrl) : assetType; + if((asset.equals(FILE_PUBLIC_KEY) || asset.equals(FILE_SIGNATURE)) && !integrityService.isEnabled()) { + throw new NotFoundException(); + } + + if(asset.equals(FILE_PUBLIC_KEY)) { + if(extVersion.getSignatureKeyPair() == null) { + throw new NotFoundException(); + } else { + return ResponseEntity + .status(HttpStatus.FOUND) + .location(URI.create(UrlUtil.getPublicKeyUrl(extVersion))) + .build(); + } + } + var resource = getFileFromDB(extVersion, asset); if (resource == null) { throw new NotFoundException(); @@ -300,7 +324,8 @@ private FileResource getFileFromDB(ExtensionVersion extVersion, String assetType FILE_CHANGELOG, CHANGELOG, FILE_LICENSE, LICENSE, FILE_ICON, ICON, - FILE_VSIXMANIFEST, VSIXMANIFEST + FILE_VSIXMANIFEST, VSIXMANIFEST, + FILE_SIGNATURE, DOWNLOAD_SIG ); var type = assets.get(assetType); @@ -564,6 +589,10 @@ private ExtensionQueryResult.ExtensionVersion toQueryVersion( queryVer.addFile(FILE_VSIX, createFileUrl(resourcesByType.get(DOWNLOAD), fileBaseUrl)); queryVer.addFile(FILE_CHANGELOG, createFileUrl(resourcesByType.get(CHANGELOG), fileBaseUrl)); queryVer.addFile(FILE_VSIXMANIFEST, createFileUrl(resourcesByType.get(VSIXMANIFEST), fileBaseUrl)); + queryVer.addFile(FILE_SIGNATURE, createFileUrl(resourcesByType.get(DOWNLOAD_SIG), fileBaseUrl)); + if(resourcesByType.containsKey(DOWNLOAD_SIG)) { + queryVer.addFile(FILE_PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVer)); + } } return queryVer; diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java index 4e491389d..3cb55ad75 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java @@ -14,6 +14,7 @@ import org.eclipse.openvsx.UrlConfigService; import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.NamingUtil; import org.eclipse.openvsx.util.UrlUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,7 +102,7 @@ private ExtensionQueryParam createRequestData(Extension extension) { filter.criteria.add(targetCriterion); var nameCriterion = new ExtensionQueryParam.Criterion(); nameCriterion.filterType = ExtensionQueryParam.Criterion.FILTER_EXTENSION_NAME; - nameCriterion.value = extension.getNamespace().getName() + "." + extension.getName(); + nameCriterion.value = NamingUtil.toExtensionId(extension); filter.criteria.add(nameCriterion); filter.pageNumber = 1; filter.pageSize = 1; diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java index c1d281125..45074f7c2 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java @@ -181,7 +181,7 @@ public ResponseEntity getExtension(@PathVariable String namespace var extension = repositories.findExtension(extensionName, namespaceName); if (extension == null) { - var json = ExtensionJson.error("Extension not found: " + namespaceName + "." + extensionName); + var json = ExtensionJson.error("Extension not found: " + NamingUtil.toExtensionId(namespaceName, extensionName)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java index 730ee9b40..f3d1d6bef 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java @@ -20,10 +20,7 @@ import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.search.SearchUtilService; import org.eclipse.openvsx.storage.StorageUtilService; -import org.eclipse.openvsx.util.ErrorResultException; -import org.eclipse.openvsx.util.TimeUtil; -import org.eclipse.openvsx.util.UrlUtil; -import org.eclipse.openvsx.util.VersionService; +import org.eclipse.openvsx.util.*; import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +34,6 @@ import java.time.DateTimeException; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashSet; import java.util.stream.Collectors; @@ -86,8 +82,8 @@ public ResultJson deleteExtension(String namespaceName, String extensionName, Us throws ErrorResultException { var extension = repositories.findExtension(extensionName, namespaceName); if (extension == null) { - throw new ErrorResultException("Extension not found: " + namespaceName + "." + extensionName, - HttpStatus.NOT_FOUND); + var extensionId = NamingUtil.toExtensionId(namespaceName, extensionName); + throw new ErrorResultException("Extension not found: " + extensionId, HttpStatus.NOT_FOUND); } return deleteExtension(extension, admin); } @@ -97,9 +93,7 @@ public ResultJson deleteExtension(String namespaceName, String extensionName, St throws ErrorResultException { var extVersion = repositories.findVersion(version, targetPlatform, extensionName, namespaceName); if (extVersion == null) { - var message = "Extension not found: " + namespaceName + "." + extensionName + - " " + version + - (Strings.isNullOrEmpty(targetPlatform) ? "" : " (" + targetPlatform + ")"); + var message = "Extension not found: " + NamingUtil.toLogFormat(namespaceName, extensionName, targetPlatform, version); throw new ErrorResultException(message, HttpStatus.NOT_FOUND); } @@ -111,18 +105,17 @@ protected ResultJson deleteExtension(Extension extension, UserData admin) throws var namespace = extension.getNamespace(); var bundledRefs = repositories.findBundledExtensionsReference(extension); if (!bundledRefs.isEmpty()) { - throw new ErrorResultException("Extension " + namespace.getName() + "." + extension.getName() + throw new ErrorResultException("Extension " + NamingUtil.toExtensionId(extension) + " is bundled by the following extension packs: " + bundledRefs.stream() - .map(ev -> ev.getExtension().getNamespace().getName() + "." + ev.getExtension().getName() + "@" + ev.getVersion()) + .map(NamingUtil::toFileFormat) .collect(Collectors.joining(", "))); } var dependRefs = repositories.findDependenciesReference(extension); if (!dependRefs.isEmpty()) { - throw new ErrorResultException("The following extensions have a dependency on " + namespace.getName() + "." - + extension.getName() + ": " + throw new ErrorResultException("The following extensions have a dependency on " + NamingUtil.toExtensionId(extension) + ": " + dependRefs.stream() - .map(ev -> ev.getExtension().getNamespace().getName() + "." + ev.getExtension().getName() + "@" + ev.getVersion()) + .map(NamingUtil::toFileFormat) .collect(Collectors.joining(", "))); } @@ -137,7 +130,7 @@ protected ResultJson deleteExtension(Extension extension, UserData admin) throws entityManager.remove(extension); search.removeSearchEntry(extension); - var result = ResultJson.success("Deleted " + namespace.getName() + "." + extension.getName()); + var result = ResultJson.success("Deleted " + NamingUtil.toExtensionId(extension)); logAdminAction(admin, result); return result; } @@ -152,8 +145,7 @@ protected ResultJson deleteExtension(ExtensionVersion extVersion, UserData admin extension.getVersions().remove(extVersion); extensions.updateExtension(extension); - var result = ResultJson.success("Deleted " + extension.getNamespace().getName() + "." + extension.getName() - + " " + extVersion.getVersion()); + var result = ResultJson.success("Deleted " + NamingUtil.toLogFormat(extVersion)); logAdminAction(admin, result); return result; } @@ -277,7 +269,7 @@ public UserPublishInfoJson getUserPublishInfo(String provider, String loginName) json.preview = latest.isPreview(); json.active = latest.getExtension().isActive(); json.files = storageUtil.getFileUrls(latest, UrlUtil.getBaseUrl(), - DOWNLOAD, DOWNLOAD_SHA256, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST); + DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST); return json; }) diff --git a/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java index cf6c6b5db..f2584d98a 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java @@ -9,7 +9,6 @@ * ****************************************************************************** */ package org.eclipse.openvsx.admin; -import org.eclipse.openvsx.ExtensionProcessor; import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.entities.ExtensionVersion; @@ -18,6 +17,7 @@ import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.storage.StorageUtilService; import org.eclipse.openvsx.util.ErrorResultException; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,15 +29,14 @@ import java.util.*; import java.util.stream.Collectors; -import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD; -import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD_SHA256; +import static org.eclipse.openvsx.entities.FileResource.*; @Component public class ChangeNamespaceJobRequestHandler implements JobRequestHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ChangeNamespaceJobRequestHandler.class); - private static final List RENAME_TYPES = List.of(DOWNLOAD, DOWNLOAD_SHA256); + private static final List RENAME_TYPES = List.of(DOWNLOAD, DOWNLOAD_SHA256, DOWNLOAD_SIG); private static final Map LOCKS; static { @@ -173,9 +172,7 @@ private List> copyResources(Streamable { - return new AbstractMap.SimpleEntry<>(extVersion.getId(), newBinaryName(newNamespace, extVersion)); - }) + .map(extVersion -> new AbstractMap.SimpleEntry<>(extVersion.getId(), newBinaryName(newNamespace, extVersion))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); return resources.stream() @@ -200,22 +197,21 @@ private String getNewResourceName(FileResource resource, Map newBi if(resource.getType().equals(DOWNLOAD_SHA256)) { name = name.replace(".vsix", ".sha256"); } + if(resource.getType().equals(DOWNLOAD_SIG)) { + name = name.replace(".vsix", ".sigzip"); + } LOGGER.info("New resource name: {}", name); return name; } private String newBinaryName(Namespace newNamespace, ExtensionVersion extVersion) { - var newExtension = new Extension(); - newExtension.setNamespace(newNamespace); - newExtension.setName(extVersion.getExtension().getName()); - - var newExtVersion = new ExtensionVersion(); - newExtVersion.setVersion(extVersion.getVersion()); - newExtVersion.setTargetPlatform(extVersion.getTargetPlatform()); - newExtVersion.setExtension(newExtension); - try(var processor = new ExtensionProcessor(null)) { - return processor.getBinaryName(newExtVersion); - } + return NamingUtil.toFileFormat( + newNamespace.getName(), + extVersion.getExtension().getName(), + extVersion.getTargetPlatform(), + extVersion.getVersion(), + ".vsix" + ); } } diff --git a/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java b/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java index ddea14674..baac1a416 100644 --- a/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java +++ b/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java @@ -46,6 +46,10 @@ public class CacheService { @Autowired LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKey; + public void evictNamespaceDetails() { + invalidateCache(CACHE_NAMESPACE_DETAILS_JSON); + } + public void evictNamespaceDetails(Extension extension) { var cache = cacheManager.getCache(CACHE_NAMESPACE_DETAILS_JSON); if(cache == null) { @@ -56,6 +60,10 @@ public void evictNamespaceDetails(Extension extension) { cache.evictIfPresent(namespaceName); } + public void evictExtensionJsons() { + invalidateCache(CACHE_EXTENSION_JSON); + } + public void evictExtensionJsons(String namespaceName, String extensionName) { evictExtensionJsons(repositoryService.findExtension(extensionName, namespaceName)); } @@ -92,19 +100,54 @@ public void evictExtensionJsons(Extension extension) { } } + public void evictExtensionJsons(ExtensionVersion extVersion) { + var cache = cacheManager.getCache(CACHE_EXTENSION_JSON); + if (cache == null) { + return; // cache is not created + } + + var extension = extVersion.getExtension(); + var namespace = extension.getNamespace(); + var versions = new ArrayList<>(List.of(VersionAlias.LATEST, extVersion.getVersion())); + if (extVersion.isPreRelease()) { + versions.add(VersionAlias.PRE_RELEASE); + } + if (extVersion.isPreview()) { + versions.add(VersionAlias.PREVIEW); + } + for (var version : versions) { + cache.evictIfPresent(extensionJsonCacheKey.generate(namespace.getName(), extension.getName(), extVersion.getTargetPlatform(), version)); + } + } + + public void evictLatestExtensionVersions() { + invalidateCache(CACHE_LATEST_EXTENSION_VERSION); + } + public void evictLatestExtensionVersion(Extension extension) { var cache = cacheManager.getCache(CACHE_LATEST_EXTENSION_VERSION); - if(cache != null) { - var targetPlatforms = new ArrayList<>(TargetPlatform.TARGET_PLATFORM_NAMES); - targetPlatforms.add(null); - for (var targetPlatform : targetPlatforms) { - for (var preRelease : List.of(true, false)) { - for (var onlyActive : List.of(true, false)) { - var key = latestExtensionVersionCacheKey.generate(null, null, extension, targetPlatform, preRelease, onlyActive); - cache.evictIfPresent(key); - } + if(cache == null) { + return; + } + + var targetPlatforms = new ArrayList<>(TargetPlatform.TARGET_PLATFORM_NAMES); + targetPlatforms.add(null); + for (var targetPlatform : targetPlatforms) { + for (var preRelease : List.of(true, false)) { + for (var onlyActive : List.of(true, false)) { + var key = latestExtensionVersionCacheKey.generate(null, null, extension, targetPlatform, preRelease, onlyActive); + cache.evictIfPresent(key); } } } } + + private void invalidateCache(String cacheName) { + var cache = cacheManager.getCache(cacheName); + if(cache == null) { + return; + } + + cache.invalidate(); + } } diff --git a/server/src/main/java/org/eclipse/openvsx/cache/ExtensionJsonCacheKeyGenerator.java b/server/src/main/java/org/eclipse/openvsx/cache/ExtensionJsonCacheKeyGenerator.java index 82edb4c20..46a2ec17c 100644 --- a/server/src/main/java/org/eclipse/openvsx/cache/ExtensionJsonCacheKeyGenerator.java +++ b/server/src/main/java/org/eclipse/openvsx/cache/ExtensionJsonCacheKeyGenerator.java @@ -9,6 +9,7 @@ * ****************************************************************************** */ package org.eclipse.openvsx.cache; +import org.eclipse.openvsx.util.NamingUtil; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.stereotype.Component; @@ -23,6 +24,6 @@ public Object generate(Object target, Method method, Object... params) { } public String generate(String namespaceName, String extensionName, String targetPlatform, String version) { - return namespaceName + "." + extensionName + "-" + version + "@" + targetPlatform; + return NamingUtil.toFileFormat(namespaceName, extensionName, version, targetPlatform); } } diff --git a/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java b/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java index 61bcc84b7..945fd63ef 100644 --- a/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java +++ b/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java @@ -11,6 +11,7 @@ import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.util.NamingUtil; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.stereotype.Component; @@ -46,7 +47,7 @@ public Object generate(Object target, Method method, Object... params) { var extensionName = extension.getName(); var namespaceName = extension.getNamespace().getName(); - return namespaceName + "." + extensionName + "-latest@" + targetPlatform + + return NamingUtil.toFileFormat(namespaceName, extensionName, targetPlatform, "latest") + ",pre-release=" + preRelease + ",only-active=" + onlyActive + ",type=" + type; } } diff --git a/server/src/main/java/org/eclipse/openvsx/eclipse/PublisherComplianceChecker.java b/server/src/main/java/org/eclipse/openvsx/eclipse/PublisherComplianceChecker.java index 8954284e8..a1fc8f38b 100644 --- a/server/src/main/java/org/eclipse/openvsx/eclipse/PublisherComplianceChecker.java +++ b/server/src/main/java/org/eclipse/openvsx/eclipse/PublisherComplianceChecker.java @@ -18,7 +18,7 @@ import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.repositories.RepositoryService; -import org.eclipse.openvsx.util.TargetPlatform; +import org.eclipse.openvsx.util.NamingUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -100,9 +100,7 @@ private void deactivateExtensions(Streamable accessTokens) entityManager.merge(version); var extension = version.getExtension(); affectedExtensions.add(extension); - logger.info("Deactivated: " + accessToken.getUser().getLoginName() + " - " - + extension.getNamespace().getName() + "." + extension.getName() + " " + version.getVersion() - + (TargetPlatform.isUniversal(version) ? "" : " (" + version.getTargetPlatform() + ")")); + logger.info("Deactivated: " + accessToken.getUser().getLoginName() + " - " + NamingUtil.toLogFormat(version)); } } diff --git a/server/src/main/java/org/eclipse/openvsx/entities/Extension.java b/server/src/main/java/org/eclipse/openvsx/entities/Extension.java index 0ce37ea83..5e4c01054 100644 --- a/server/src/main/java/org/eclipse/openvsx/entities/Extension.java +++ b/server/src/main/java/org/eclipse/openvsx/entities/Extension.java @@ -18,6 +18,7 @@ import javax.persistence.*; import org.eclipse.openvsx.search.ExtensionSearch; +import org.eclipse.openvsx.util.NamingUtil; @Entity @Table(uniqueConstraints = { @@ -61,7 +62,7 @@ public ExtensionSearch toSearch(ExtensionVersion latest) { search.id = this.getId(); search.name = this.getName(); search.namespace = this.getNamespace().getName(); - search.extensionId = search.namespace + "." + search.name; + search.extensionId = NamingUtil.toExtensionId(search); search.downloadCount = this.getDownloadCount(); search.targetPlatforms = this.getVersions().stream() .map(ExtensionVersion::getTargetPlatform) diff --git a/server/src/main/java/org/eclipse/openvsx/entities/ExtensionVersion.java b/server/src/main/java/org/eclipse/openvsx/entities/ExtensionVersion.java index 90f09aa20..86bc025dd 100644 --- a/server/src/main/java/org/eclipse/openvsx/entities/ExtensionVersion.java +++ b/server/src/main/java/org/eclipse/openvsx/entities/ExtensionVersion.java @@ -120,6 +120,9 @@ public enum Type { @Convert(converter = ListOfStringConverter.class) List bundledExtensions; + @ManyToOne + SignatureKeyPair signatureKeyPair; + @Transient Type type; @@ -438,6 +441,14 @@ public void setBundledExtensions(List bundledExtensions) { this.bundledExtensions = bundledExtensions; } + public SignatureKeyPair getSignatureKeyPair() { + return signatureKeyPair; + } + + public void setSignatureKeyPair(SignatureKeyPair signatureKeyPair) { + this.signatureKeyPair = signatureKeyPair; + } + public void setType(ExtensionVersion.Type type) { this.type = type; } @@ -477,6 +488,7 @@ public boolean equals(Object o) { && Objects.equals(qna, that.qna) && Objects.equals(dependencies, that.dependencies) && Objects.equals(bundledExtensions, that.bundledExtensions) + && Objects.equals(signatureKeyPair, that.signatureKeyPair) && type == that.type; } @@ -486,7 +498,7 @@ public int hashCode() { id, getId(extension), version, targetPlatform, semver, preRelease, preview, timestamp, getId(publishedWith), active, displayName, description, engines, categories, tags, extensionKind, license, homepage, repository, sponsorLink, bugs, markdown, galleryColor, galleryTheme, localizedLanguages, qna, dependencies, - bundledExtensions, type + bundledExtensions, signatureKeyPair, type ); } diff --git a/server/src/main/java/org/eclipse/openvsx/entities/FileResource.java b/server/src/main/java/org/eclipse/openvsx/entities/FileResource.java index 3df30c562..fa7712e5c 100644 --- a/server/src/main/java/org/eclipse/openvsx/entities/FileResource.java +++ b/server/src/main/java/org/eclipse/openvsx/entities/FileResource.java @@ -17,6 +17,8 @@ public class FileResource { // Resource types public static final String DOWNLOAD = "download"; public static final String DOWNLOAD_SHA256 = "sha256"; + public static final String DOWNLOAD_SIG = "signature"; + public static final String PUBLIC_KEY = "publicKey"; public static final String MANIFEST = "manifest"; public static final String ICON = "icon"; public static final String README = "readme"; diff --git a/server/src/main/java/org/eclipse/openvsx/entities/SignatureKeyPair.java b/server/src/main/java/org/eclipse/openvsx/entities/SignatureKeyPair.java new file mode 100644 index 000000000..ae7a8f1dc --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/entities/SignatureKeyPair.java @@ -0,0 +1,111 @@ +/** ****************************************************************************** + * Copyright (c) 2023 Precies. Software Ltd and others + * + * 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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Objects; + +@Entity +public class SignatureKeyPair implements Serializable { + + public static final String KEYPAIR_MODE_CREATE = "create"; + public static final String KEYPAIR_MODE_RENEW = "renew"; + public static final String KEYPAIR_MODE_DELETE = "delete"; + + @Id + @GeneratedValue + long id; + + @Column(length = 128) + String publicId; + + @Column(length = 32) + byte[] privateKey; + + String publicKeyText; + + LocalDateTime created; + + boolean active; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getPublicId() { + return publicId; + } + + public void setPublicId(String publicId) { + this.publicId = publicId; + } + + public byte[] getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(byte[] privateKey) { + this.privateKey = privateKey; + } + + public String getPublicKeyText() { + return publicKeyText; + } + + public void setPublicKeyText(String publicKeyText) { + this.publicKeyText = publicKeyText; + } + + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SignatureKeyPair that = (SignatureKeyPair) o; + return id == that.id + && active == that.active + && Objects.equals(publicId, that.publicId) + && Arrays.equals(privateKey, that.privateKey) + && Objects.equals(publicKeyText, that.publicKeyText) + && Objects.equals(created, that.created); + } + + @Override + public int hashCode() { + int result = Objects.hash(id, publicId, publicKeyText, created, active); + result = 31 * result + Arrays.hashCode(privateKey); + return result; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/migration/ExtensionVersionSignatureJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/ExtensionVersionSignatureJobRequestHandler.java new file mode 100644 index 000000000..0d53e2a07 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/migration/ExtensionVersionSignatureJobRequestHandler.java @@ -0,0 +1,72 @@ +/** ****************************************************************************** + * Copyright (c) 2023 Precies. Software Ltd and others + * + * 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.migration; + +import org.eclipse.openvsx.cache.CacheService; +import org.eclipse.openvsx.entities.FileResource; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.NamingUtil; +import org.jobrunr.jobs.annotations.Job; +import org.jobrunr.jobs.context.JobRunrDashboardLogger; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(value = "ovsx.data.mirror.enabled", havingValue = "false", matchIfMissing = true) +public class ExtensionVersionSignatureJobRequestHandler implements JobRequestHandler { + + protected final Logger logger = new JobRunrDashboardLogger(LoggerFactory.getLogger(ExtensionVersionSignatureJobRequestHandler.class)); + + @Autowired + RepositoryService repositories; + + @Autowired + CacheService cache; + + @Autowired + MigrationService migrations; + + @Autowired + ExtensionVersionIntegrityService integrityService; + + @Override + @Job(name = "Generate signature for extension version", retries = 3) + public void run(MigrationJobRequest jobRequest) throws Exception { + var extVersion = migrations.getExtension(jobRequest.getEntityId()); + logger.info("Generating signature for: {}", NamingUtil.toLogFormat(extVersion)); + + var existingSignature = migrations.getFileResource(extVersion, FileResource.DOWNLOAD_SIG); + if(existingSignature != null) { + migrations.removeFile(existingSignature); + migrations.deleteFileResource(existingSignature); + } + + var entry = migrations.getDownload(extVersion); + try(var extensionFile = migrations.getExtensionFile(entry)) { + var download = entry.getKey(); + var keyPair = repositories.findActiveKeyPair(); + var signature = integrityService.generateSignature(download, extensionFile, keyPair); + signature.setStorageType(download.getStorageType()); + integrityService.setSignatureKeyPair(extVersion, keyPair); + migrations.uploadFileResource(signature); + migrations.persistFileResource(signature); + } + + var extension = extVersion.getExtension(); + cache.evictExtensionJsons(extVersion); + cache.evictLatestExtensionVersion(extension); + cache.evictNamespaceDetails(extension); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/migration/ExtractResourcesJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/ExtractResourcesJobRequestHandler.java index f0659697c..0053fd0c4 100644 --- a/server/src/main/java/org/eclipse/openvsx/migration/ExtractResourcesJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/migration/ExtractResourcesJobRequestHandler.java @@ -10,6 +10,7 @@ package org.eclipse.openvsx.migration; import org.eclipse.openvsx.ExtensionProcessor; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.annotations.Job; import org.jobrunr.jobs.context.JobRunrDashboardLogger; import org.jobrunr.jobs.lambdas.JobRequestHandler; @@ -19,8 +20,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; -import java.nio.file.Files; - @Component @ConditionalOnProperty(value = "ovsx.data.mirror.enabled", havingValue = "false", matchIfMissing = true) public class ExtractResourcesJobRequestHandler implements JobRequestHandler { @@ -37,14 +36,14 @@ public class ExtractResourcesJobRequestHandler implements JobRequestHandler { resource.setStorageType(download.getStorageType()); diff --git a/server/src/main/java/org/eclipse/openvsx/migration/ExtractVsixManifestsJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/ExtractVsixManifestsJobRequestHandler.java index dbd23272c..8f072171f 100644 --- a/server/src/main/java/org/eclipse/openvsx/migration/ExtractVsixManifestsJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/migration/ExtractVsixManifestsJobRequestHandler.java @@ -11,6 +11,7 @@ import org.eclipse.openvsx.ExtensionProcessor; import org.eclipse.openvsx.entities.FileResource; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.annotations.Job; import org.jobrunr.jobs.context.JobRunrDashboardLogger; import org.jobrunr.jobs.lambdas.JobRequestHandler; @@ -36,7 +37,7 @@ public class ExtractVsixManifestsJobRequestHandler implements JobRequestHandler< public void run(MigrationJobRequest jobRequest) throws Exception { var download = migrations.getResource(jobRequest); var extVersion = download.getExtension(); - logger.info("Extracting VSIX manifests for: {}.{}-{}@{}", extVersion.getExtension().getNamespace().getName(), extVersion.getExtension().getName(), extVersion.getVersion(), extVersion.getTargetPlatform()); + logger.info("Extracting VSIX manifests for: {}", NamingUtil.toLogFormat(extVersion)); var existingVsixManifest = migrations.getFileResource(extVersion, FileResource.VSIXMANIFEST); if(existingVsixManifest != null) { diff --git a/server/src/main/java/org/eclipse/openvsx/migration/FixTargetPlatformsJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/FixTargetPlatformsJobRequestHandler.java index 40f5e9d17..01ce7ced3 100644 --- a/server/src/main/java/org/eclipse/openvsx/migration/FixTargetPlatformsJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/migration/FixTargetPlatformsJobRequestHandler.java @@ -13,6 +13,7 @@ import org.eclipse.openvsx.ExtensionService; import org.eclipse.openvsx.admin.AdminService; import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.annotations.Job; import org.jobrunr.jobs.context.JobRunrDashboardLogger; import org.jobrunr.jobs.lambdas.JobRequestHandler; @@ -56,7 +57,7 @@ public void run(MigrationJobRequest jobRequest) throws Exception { } if (fixTargetPlatform) { - logger.info("Fixing target platform for: {}.{}-{}@{}", extVersion.getExtension().getNamespace().getName(), extVersion.getExtension().getName(), extVersion.getVersion(), extVersion.getTargetPlatform()); + logger.info("Fixing target platform for: {}", NamingUtil.toLogFormat(extVersion)); deleteExtension(extVersion); try (var input = Files.newInputStream(extensionFile.getPath())) { extensions.publishVersion(input, extVersion.getPublishedWith()); diff --git a/server/src/main/java/org/eclipse/openvsx/migration/GenerateKeyPairJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/GenerateKeyPairJobRequestHandler.java new file mode 100644 index 000000000..82b9252b2 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/migration/GenerateKeyPairJobRequestHandler.java @@ -0,0 +1,152 @@ +/** ****************************************************************************** + * Copyright (c) 2023 Precies. Software Ltd and others + * + * 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.migration; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator; +import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.eclipse.openvsx.admin.RemoveFileJobRequest; +import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.entities.FileResource; +import org.eclipse.openvsx.entities.SignatureKeyPair; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.util.Streamable; +import org.springframework.stereotype.Component; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD_SIG; +import static org.eclipse.openvsx.entities.FileResource.STORAGE_DB; +import static org.eclipse.openvsx.entities.SignatureKeyPair.*; + +@Component +@ConditionalOnProperty(value = "ovsx.data.mirror.enabled", havingValue = "false", matchIfMissing = true) +public class GenerateKeyPairJobRequestHandler implements JobRequestHandler> { + + @Autowired + EntityManager entityManager; + + @Autowired + RepositoryService repositories; + + @Autowired + JobRequestScheduler scheduler; + + @Value("${ovsx.integrity.key-pair:}") + String keyPairMode; + + @Override + @Transactional + public void run(HandlerJobRequest jobRequest) throws Exception { + switch (keyPairMode) { + case KEYPAIR_MODE_CREATE: + createKeyPair(); + break; + case KEYPAIR_MODE_RENEW: + renewKeyPair(); + break; + case KEYPAIR_MODE_DELETE: + deleteKeyPairs(); + break; + } + } + + private void createKeyPair() { + var activeKeyPair = repositories.findActiveKeyPair(); + Streamable extVersions; + if(activeKeyPair == null) { + generateKeyPair(); + extVersions = repositories.findVersions(); + } else { + extVersions = repositories.findVersionsWithout(activeKeyPair); + } + + extVersions.forEach(this::enqueueCreateSignatureJob); + } + + private void renewKeyPair() { + var activeKeyPair = repositories.findActiveKeyPair(); + generateKeyPair(); + repositories.findVersions().forEach(this::enqueueCreateSignatureJob); + if(activeKeyPair != null) { + activeKeyPair.setActive(false); + } + } + + private void deleteKeyPairs() { + repositories.deleteAllKeyPairs(); + repositories.findFilesByType(DOWNLOAD_SIG).forEach(this::enqueueDeleteSignatureJob); + repositories.deleteDownloadSigFiles(); + } + + private void generateKeyPair() { + var generator = new Ed25519KeyPairGenerator(); + generator.init(new Ed25519KeyGenerationParameters(new SecureRandom())); + var pair = generator.generateKeyPair(); + + var keyPair = new SignatureKeyPair(); + keyPair.setPublicId(UUID.randomUUID().toString()); + keyPair.setPrivateKey(((Ed25519PrivateKeyParameters) pair.getPrivate()).getEncoded()); + keyPair.setPublicKeyText(getPublicKeyText(pair)); + keyPair.setCreated(LocalDateTime.now()); + keyPair.setActive(true); + entityManager.persist(keyPair); + } + + private String getPublicKeyText(AsymmetricCipherKeyPair pair) { + PemObject pemObject; + try { + var publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(pair.getPublic()); + pemObject = new PemObject("PUBLIC KEY", publicKeyInfo.getEncoded()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + try ( + var output = new ByteArrayOutputStream(); + var writer = new PemWriter(new OutputStreamWriter(output)) + ) { + writer.writeObject(pemObject); + writer.flush(); + return output.toString(StandardCharsets.UTF_8); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void enqueueCreateSignatureJob(ExtensionVersion extVersion) { + var handler = ExtensionVersionSignatureJobRequestHandler.class; + var jobRequest = new MigrationJobRequest<>(handler, extVersion.getId()); + scheduler.schedule(LocalDateTime.now().plusSeconds(30), jobRequest); + } + + private void enqueueDeleteSignatureJob(FileResource resource) { + if(!resource.getStorageType().equals(STORAGE_DB)) { + scheduler.schedule(LocalDateTime.now().plusSeconds(30), new RemoveFileJobRequest(resource)); + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/migration/GenerateSha256ChecksumJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/GenerateSha256ChecksumJobRequestHandler.java index c2dd34bf4..8b842df84 100644 --- a/server/src/main/java/org/eclipse/openvsx/migration/GenerateSha256ChecksumJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/migration/GenerateSha256ChecksumJobRequestHandler.java @@ -11,6 +11,7 @@ import org.eclipse.openvsx.ExtensionProcessor; import org.eclipse.openvsx.entities.FileResource; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.annotations.Job; import org.jobrunr.jobs.context.JobRunrDashboardLogger; import org.jobrunr.jobs.lambdas.JobRequestHandler; @@ -34,7 +35,7 @@ public class GenerateSha256ChecksumJobRequestHandler implements JobRequestHandle public void run(MigrationJobRequest jobRequest) throws Exception { var download = migrations.getResource(jobRequest); var extVersion = download.getExtension(); - logger.info("Generate sha256 checksum for: {}.{}-{}@{}", extVersion.getExtension().getNamespace().getName(), extVersion.getExtension().getName(), extVersion.getVersion(), extVersion.getTargetPlatform()); + logger.info("Generate sha256 checksum for: {}", NamingUtil.toLogFormat(extVersion)); var existingChecksum = migrations.getFileResource(extVersion, FileResource.DOWNLOAD_SHA256); if(existingChecksum != null) { diff --git a/server/src/main/java/org/eclipse/openvsx/migration/MigrationRunner.java b/server/src/main/java/org/eclipse/openvsx/migration/MigrationRunner.java index 13f587de2..0b530e7b7 100644 --- a/server/src/main/java/org/eclipse/openvsx/migration/MigrationRunner.java +++ b/server/src/main/java/org/eclipse/openvsx/migration/MigrationRunner.java @@ -14,6 +14,7 @@ import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.jobrunr.scheduling.JobRequestScheduler; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @@ -31,6 +32,9 @@ public class MigrationRunner implements JobRequestHandler> @Autowired JobRequestScheduler scheduler; + @Value("${ovsx.data.mirror.enabled:false}") + boolean mirrorEnabled; + @Override @Job(name = "Run migrations", retries = 0) public void run(HandlerJobRequest jobRequest) throws Exception { @@ -41,6 +45,7 @@ public void run(HandlerJobRequest jobRequest) throws Exception { extractVsixManifestMigration(); fixTargetPlatformMigration(); generateSha256ChecksumMigration(); + extensionVersionSignatureMigration(); } private void extractResourcesMigration() { @@ -78,4 +83,10 @@ private void generateSha256ChecksumMigration() { var handler = GenerateSha256ChecksumJobRequestHandler.class; repositories.findNotMigratedSha256Checksums().forEach(item -> migrations.enqueueMigration(jobName, handler, item)); } + + private void extensionVersionSignatureMigration() { + if(!mirrorEnabled) { + scheduler.enqueue(new HandlerJobRequest<>(GenerateKeyPairJobRequestHandler.class)); + } + } } diff --git a/server/src/main/java/org/eclipse/openvsx/migration/RenameDownloadsJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/migration/RenameDownloadsJobRequestHandler.java index cb92bd4e2..6d8f7b7b8 100644 --- a/server/src/main/java/org/eclipse/openvsx/migration/RenameDownloadsJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/migration/RenameDownloadsJobRequestHandler.java @@ -9,6 +9,7 @@ * ****************************************************************************** */ package org.eclipse.openvsx.migration; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,7 +17,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; -import java.nio.file.Files; import java.util.AbstractMap; @Component @@ -34,7 +34,7 @@ public class RenameDownloadsJobRequestHandler implements JobRequestHandler getExtensionVersions(MigrationJobRequest jobRequest, Logger logger) { var extension = entityManager.find(Extension.class, jobRequest.getEntityId()); - logger.info("Setting pre-release for: {}.{}", extension.getNamespace().getName(), extension.getName()); + logger.info("Setting pre-release for: {}", NamingUtil.toExtensionId(extension)); return extension.getVersions(); } diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorJobRequestHandler.java index c3836a226..d39e47042 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorJobRequestHandler.java @@ -23,6 +23,7 @@ import org.eclipse.openvsx.admin.AdminService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.ErrorResultException; +import org.eclipse.openvsx.util.NamingUtil; import org.jobrunr.jobs.annotations.Job; import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.slf4j.Logger; @@ -89,7 +90,7 @@ public void run(DataMirrorJobRequest jobRequest) throws Exception { var pathParams = location.getPath().split("/"); var namespace = pathParams[pathParams.length - 2]; var extension = pathParams[pathParams.length - 1]; - var extensionId = namespace + "." + extension; + var extensionId = NamingUtil.toExtensionId(namespace, extension); if (!data.match(namespace, extension)) { jobContext().logger().info("excluded, skipping " + extensionId + " (" + (i+1) + "/" + urls.getLength() + ")"); continue; @@ -116,17 +117,17 @@ public void run(DataMirrorJobRequest jobRequest) throws Exception { var notMatchingExtensions = repositories.findAllNotMatchingByExtensionId(extensionIds); if (!notMatchingExtensions.isEmpty()) { for(var extension : notMatchingExtensions) { - var namespace = extension.getNamespace().getName(); - var extensionName = extension.getName(); - jobContext().logger().info("deleting " + namespace + "." + extensionName); + var extensionId = NamingUtil.toExtensionId(extension); + jobContext().logger().info("deleting " + extensionId); try { - admin.deleteExtension(namespace, extensionName, mirrorUser); + var namespace = extension.getNamespace(); + admin.deleteExtension(namespace.getName(), extension.getName(), mirrorUser); } catch (ErrorResultException t) { if (t.getStatus() != HttpStatus.NOT_FOUND) { - logger.warn("mirror: failed to delete extension " + namespace + "." + extensionName, t); + logger.warn("mirror: failed to delete extension " + extensionId, t); } } catch (Throwable t) { - logger.error("mirror: failed to delete extension " + namespace + "." + extensionName, t); + logger.error("mirror: failed to delete extension " + extensionId, t); } } } diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java index 7ff027e96..c7511525d 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java @@ -35,6 +35,7 @@ import org.eclipse.openvsx.json.UserJson; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.storage.StorageUtilService; +import org.eclipse.openvsx.util.NamingUtil; import org.eclipse.openvsx.util.TimeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,12 +122,12 @@ public boolean needsMatch() { public boolean match(String namespaceName, String extensionName) { if (!excludeExtensions.isEmpty() && (excludeExtensions.contains(namespaceName + ".*") || - excludeExtensions.contains(namespaceName + "." + extensionName))) { + excludeExtensions.contains(NamingUtil.toExtensionId(namespaceName, extensionName)))) { return false; } return includeExtensions.isEmpty() || includeExtensions.contains(namespaceName + ".*") || - includeExtensions.contains(namespaceName + "." + extensionName); + includeExtensions.contains(NamingUtil.toExtensionId(namespaceName, extensionName)); } @Transactional diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java b/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java index cf3a4d7dd..7a62a2cce 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java @@ -15,6 +15,7 @@ import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.json.ExtensionJson; import org.eclipse.openvsx.json.UserJson; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.*; import org.jobrunr.jobs.context.JobContext; @@ -26,6 +27,7 @@ import org.springframework.web.client.RestTemplate; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.time.LocalDate; import java.util.ArrayList; @@ -34,6 +36,9 @@ import java.util.Set; import java.util.stream.Collectors; +import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD_SIG; +import static org.eclipse.openvsx.entities.FileResource.PUBLIC_KEY; + @Component public class MirrorExtensionService { @@ -60,6 +65,9 @@ public class MirrorExtensionService { @Autowired ExtensionService extensions; + @Autowired + ExtensionVersionIntegrityService integrityService; + /** * It applies delta from previous execution. */ @@ -68,12 +76,14 @@ public void mirrorExtension(String namespaceName, String extensionName, UserData if (shouldMirrorExtensionVersions(namespaceName, extensionName, lastModified, latest)) { mirrorExtensionVersions(namespaceName, extensionName, mirrorUser, jobContext); } else { - jobContext.logger().info("all versions are up to date " + namespaceName + "." + extensionName); + jobContext.logger().info("all versions are up to date " + NamingUtil.toExtensionId(namespaceName, extensionName)); } - logger.debug("activating extension: {}", namespaceName + "." + extensionName); + + var extensionId = logger.isDebugEnabled() ? NamingUtil.toExtensionId(namespaceName, extensionName) : null; + logger.debug("activating extension: {}", extensionId); data.activateExtension(namespaceName, extensionName); - logger.debug("updating extension metadata: {}", namespaceName + "." + extensionName); + logger.debug("updating extension metadata: {}", extensionId); data.updateMetadata(namespaceName, extensionName, latest); logger.debug("updating namespace metadata: {}", namespaceName); @@ -126,7 +136,7 @@ private void mirrorExtensionVersions(String namespaceName, String extensionName, for(var i = 0; i < toAdd.size(); i++) { var json = toAdd.get(i); - jobContext.logger().info("mirroring " + json.namespace + "." + json.name + "-" + json.version + "@" + json.targetPlatform + " (" + (i+1) + "/" + toAdd.size() + ")"); + jobContext.logger().info("mirroring " + NamingUtil.toLogFormat(json) + " (" + (i+1) + "/" + toAdd.size() + ")"); try { mirrorExtensionVersion(json); data.getMirroredVersions().increment(); @@ -138,7 +148,6 @@ private void mirrorExtensionVersions(String namespaceName, String extensionName, } private void mirrorExtensionVersion(ExtensionJson json) throws RuntimeException { - logger.debug("mirroring: {}", json.namespace + "." + json.name + "-" + json.version + "@" + json.targetPlatform); var download = json.files.get("download"); var userJson = new UserJson(); userJson.provider = json.publishedBy.provider; @@ -160,14 +169,22 @@ private void mirrorExtensionVersion(ExtensionJson json) throws RuntimeException throw new RuntimeException("Invalid vsix filename from redirected vsix url"); } - try (var extensionFile = new TempFile("extension_", ".vsix")) { - backgroundRestTemplate.execute("{vsixLocation}", HttpMethod.GET, null, response -> { - try(var out = Files.newOutputStream(extensionFile.getPath())) { - response.getBody().transferTo(out); + String signatureName = null; + try (var extensionFile = downloadToFile(download, "extension_", ".vsix")) { + if(json.files.containsKey(DOWNLOAD_SIG)) { + try( + var signatureFile = downloadToFile(json.files.get(DOWNLOAD_SIG), "extension_", ".sigzip"); + var publicKeyFile = downloadToFile(json.files.get(PUBLIC_KEY), "public_", ".pem") + ) { + var verified = integrityService.verifyExtensionVersion(extensionFile, signatureFile, publicKeyFile); + if (!verified) { + throw new RuntimeException("Unverified vsix package"); + } } - return extensionFile; - }, Map.of("vsixLocation", download)); + var signaturePathParams = URI.create(json.files.get("signature")).getPath().split("/"); + signatureName = signaturePathParams[signaturePathParams.length - 1]; + } var user = data.getOrAddUser(userJson); var namespace = repositories.findNamespace(namespaceName); @@ -177,11 +194,23 @@ private void mirrorExtensionVersion(ExtensionJson json) throws RuntimeException var accessTokenValue = data.getOrAddAccessTokenValue(user, description); var token = users.useAccessToken(accessTokenValue); - extensions.mirrorVersion(extensionFile, token, filename, json.timestamp); - logger.debug("completed mirroring of extension version: {}", json.namespace + "." + json.name + "-" + json.version + "@" + json.targetPlatform); + extensions.mirrorVersion(extensionFile, signatureName, token, filename, json.timestamp); + logger.debug("completed mirroring of extension version: {}", NamingUtil.toLogFormat(json)); } catch (IOException e) { throw new RuntimeException(e); } } + private TempFile downloadToFile(String url, String prefix, String suffix) throws IOException { + var file = new TempFile(prefix, suffix); + backgroundRestTemplate.execute("{url}", HttpMethod.GET, null, response -> { + try(var out = Files.newOutputStream(file.getPath())) { + response.getBody().transferTo(out); + } + + return file; + }, Map.of("url", url)); + + return file; + } } diff --git a/server/src/main/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityService.java b/server/src/main/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityService.java new file mode 100644 index 000000000..5494c94d6 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityService.java @@ -0,0 +1,120 @@ +/** ****************************************************************************** + * Copyright (c) 2023 Precies. Software Ltd and others + * + * 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.publish; + +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.signers.Ed25519Signer; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.openssl.PEMParser; +import org.eclipse.openvsx.cache.CacheService; +import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.entities.FileResource; +import org.eclipse.openvsx.entities.SignatureKeyPair; +import org.eclipse.openvsx.util.ErrorResultException; +import org.eclipse.openvsx.util.NamingUtil; +import org.eclipse.openvsx.util.TempFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; + +import static org.eclipse.openvsx.entities.SignatureKeyPair.KEYPAIR_MODE_CREATE; +import static org.eclipse.openvsx.entities.SignatureKeyPair.KEYPAIR_MODE_RENEW; + +@Component +public class ExtensionVersionIntegrityService { + + protected final Logger logger = LoggerFactory.getLogger(ExtensionVersionIntegrityService.class); + + @Autowired + EntityManager entityManager; + + @Autowired + CacheService cache; + + @Value("${ovsx.integrity.key-pair:}") + String keyPairMode; + + @EventListener + public void applicationStarted(ApplicationStartedEvent event) { + if(!isEnabled()) { + return; + } + + cache.evictLatestExtensionVersions(); + cache.evictExtensionJsons(); + cache.evictNamespaceDetails(); + } + + public boolean isEnabled() { + return keyPairMode.equals(KEYPAIR_MODE_CREATE) || keyPairMode.equals(KEYPAIR_MODE_RENEW); + } + + public boolean verifyExtensionVersion(TempFile extensionFile, TempFile signatureFile, TempFile publicKeyFile) { + AsymmetricKeyParameter publicKeyParameters; + try (var inReader = new InputStreamReader(Files.newInputStream(publicKeyFile.getPath()))) { + var pemParser = new PEMParser(inReader); + var publicKeyInfo = (SubjectPublicKeyInfo) pemParser.readObject(); + publicKeyParameters = PublicKeyFactory.createKey(publicKeyInfo); + } catch (IOException e) { + throw new ErrorResultException("Failed to read private key file", e); + } + + boolean verified; + try { + var signer = new Ed25519Signer(); + signer.init(false, publicKeyParameters); + var fileBytes = Files.readAllBytes(extensionFile.getPath()); + signer.update(fileBytes, 0, fileBytes.length); + verified = signer.verifySignature(Files.readAllBytes(signatureFile.getPath())); + } catch (IOException e) { + throw new ErrorResultException("Failed to verify extension file", e); + } + + return verified; + } + + @Transactional + public void setSignatureKeyPair(ExtensionVersion extVersion, SignatureKeyPair keyPair) { + extVersion = entityManager.merge(extVersion); + extVersion.setSignatureKeyPair(keyPair); + } + + public FileResource generateSignature(FileResource download, TempFile extensionFile, SignatureKeyPair keyPair) { + var resource = new FileResource(); + resource.setExtension(download.getExtension()); + resource.setName(NamingUtil.toFileFormat(download.getExtension(), ".sigzip")); + resource.setType(FileResource.DOWNLOAD_SIG); + + var privateKeyParameters = new Ed25519PrivateKeyParameters(keyPair.getPrivateKey(), 0); + try { + var signer = new Ed25519Signer(); + signer.init(true, privateKeyParameters); + var fileBytes = Files.readAllBytes(extensionFile.getPath()); + signer.update(fileBytes, 0, fileBytes.length); + resource.setContent(signer.generateSignature()); + } catch (IOException e) { + throw new ErrorResultException("Failed to sign extension file", e); + } + + return resource; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java b/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java index e1c138a16..b678821be 100644 --- a/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java @@ -18,7 +18,7 @@ import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.ErrorResultException; -import org.eclipse.openvsx.util.TargetPlatform; +import org.eclipse.openvsx.util.NamingUtil; import org.eclipse.openvsx.util.TempFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +45,9 @@ public class PublishExtensionVersionHandler { @Autowired PublishExtensionVersionService service; + @Autowired + ExtensionVersionIntegrityService integrityService; + @Autowired EntityManager entityManager; @@ -77,6 +80,10 @@ public ExtensionVersion createExtensionVersion(ExtensionProcessor processor, Per extVersion.setDependencies(dependencies); extVersion.setBundledExtensions(bundledExtensions); + if(integrityService.isEnabled()) { + extVersion.setSignatureKeyPair(repositories.findActiveKeyPair()); + } + return extVersion; } @@ -121,12 +128,10 @@ private ExtensionVersion createExtensionVersion(ExtensionProcessor processor, Us } else { var existingVersion = repositories.findVersion(extVersion.getVersion(), extVersion.getTargetPlatform(), extension); if (existingVersion != null) { - throw new ErrorResultException( - "Extension " + namespace.getName() + "." + extension.getName() - + " " + extVersion.getVersion() - + (TargetPlatform.isUniversal(extVersion) ? "" : " (" + extVersion.getTargetPlatform() + ")") - + " is already published" - + (existingVersion.isActive() ? "." : ", but is currently inactive and therefore not visible.")); + var extVersionId = NamingUtil.toLogFormat(namespaceName, extensionName, extVersion.getTargetPlatform(), extVersion.getVersion()); + var message = "Extension " + extVersionId + " is already published"; + message += existingVersion.isActive() ? "." : ", but currently isn't active and therefore not visible."; + throw new ErrorResultException(message); } } @@ -194,6 +199,7 @@ private List updateExistingPublicIds(Extension extension) { @Retryable public void publishAsync(FileResource download, TempFile extensionFile, ExtensionService extensionService) { var extVersion = download.getExtension(); + // Delete file resources in case publishAsync is retried service.deleteFileResources(extVersion); download.setId(0L); @@ -206,6 +212,19 @@ public void publishAsync(FileResource download, TempFile extensionFile, Extensio service.persistResource(resource); }; + if(integrityService.isEnabled()) { + var keyPair = extVersion.getSignatureKeyPair(); + if(keyPair != null) { + var signature = integrityService.generateSignature(download, extensionFile, keyPair); + consumer.accept(signature); + } else { + // Can happen when GenerateKeyPairJobRequestHandler hasn't run yet and there is no active SignatureKeyPair. + // This extension version should be assigned a SignatureKeyPair and a signature FileResource should be created + // by the ExtensionVersionSignatureJobRequestHandler migration. + logger.warn("Integrity service is enabled, but {} did not have an active key pair", NamingUtil.toLogFormat(extVersion)); + } + } + processor.processEachResource(extVersion, consumer); processor.getFileResources(extVersion).forEach(consumer); consumer.accept(processor.generateSha256Checksum(extVersion)); @@ -220,13 +239,24 @@ public void publishAsync(FileResource download, TempFile extensionFile, Extensio } } - public void mirror(FileResource download, TempFile extensionFile) { + public void mirror(FileResource download, TempFile extensionFile, String signatureName) { var extVersion = download.getExtension(); service.mirrorResource(download); + if(signatureName != null) { + service.mirrorResource(getSignatureResource(signatureName, extVersion)); + } try(var processor = new ExtensionProcessor(extensionFile)) { processor.getFileResources(extVersion).forEach(resource -> service.mirrorResource(resource)); service.mirrorResource(processor.generateSha256Checksum(extVersion)); // don't store file resources, they can be generated on the fly to avoid traversing entire zip file } } + + private FileResource getSignatureResource(String signatureName, ExtensionVersion extVersion) { + var resource = new FileResource(); + resource.setExtension(extVersion); + resource.setName(signatureName); + resource.setType(FileResource.DOWNLOAD_SIG); + return resource; + } } diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java index d6206fcc3..77975f9a8 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java @@ -51,11 +51,13 @@ public List findAllActiveByExtensionIdAndTargetPlatform(Collec EXTENSION_VERSION.GALLERY_THEME, EXTENSION_VERSION.LOCALIZED_LANGUAGES, EXTENSION_VERSION.DEPENDENCIES, - EXTENSION_VERSION.BUNDLED_EXTENSIONS + EXTENSION_VERSION.BUNDLED_EXTENSIONS, + SIGNATURE_KEY_PAIR.PUBLIC_ID ) .from(EXTENSION_VERSION) .join(EXTENSION).on(EXTENSION.ID.eq(EXTENSION_VERSION.EXTENSION_ID)) .join(NAMESPACE).on(NAMESPACE.ID.eq(EXTENSION.NAMESPACE_ID)) + .leftJoin(SIGNATURE_KEY_PAIR).on(SIGNATURE_KEY_PAIR.ID.eq(EXTENSION_VERSION.SIGNATURE_KEY_PAIR_ID)) .where(EXTENSION_VERSION.ACTIVE.eq(true)) .and(EXTENSION_VERSION.EXTENSION_ID.in(extensionIds)); @@ -166,13 +168,15 @@ private SelectConditionStep findAllActive() { EXTENSION_VERSION.LOCALIZED_LANGUAGES, EXTENSION_VERSION.QNA, EXTENSION_VERSION.DEPENDENCIES, - EXTENSION_VERSION.BUNDLED_EXTENSIONS + EXTENSION_VERSION.BUNDLED_EXTENSIONS, + SIGNATURE_KEY_PAIR.PUBLIC_ID ) .from(EXTENSION_VERSION) .join(EXTENSION).on(EXTENSION.ID.eq(EXTENSION_VERSION.EXTENSION_ID)) .join(NAMESPACE).on(NAMESPACE.ID.eq(EXTENSION.NAMESPACE_ID)) .leftJoin(PERSONAL_ACCESS_TOKEN).on(PERSONAL_ACCESS_TOKEN.ID.eq(EXTENSION_VERSION.PUBLISHED_WITH_ID)) .join(USER_DATA).on(USER_DATA.ID.eq(PERSONAL_ACCESS_TOKEN.USER_DATA)) + .leftJoin(SIGNATURE_KEY_PAIR).on(SIGNATURE_KEY_PAIR.ID.eq(EXTENSION_VERSION.SIGNATURE_KEY_PAIR_ID)) .where(EXTENSION_VERSION.ACTIVE.eq(true)); } @@ -255,6 +259,9 @@ private ExtensionVersion toExtensionVersionCommon(Record record) { namespace.setName(record.get(NAMESPACE.NAME)); extension.setNamespace(namespace); + var keyPair = new SignatureKeyPair(); + keyPair.setPublicId(record.get(SIGNATURE_KEY_PAIR.PUBLIC_ID)); + extVersion.setSignatureKeyPair(keyPair); return extVersion; } diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java index 87ab7ae0e..540427570 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java @@ -9,7 +9,8 @@ ********************************************************************************/ package org.eclipse.openvsx.repositories; -import org.eclipse.openvsx.entities.UserData; +import org.eclipse.openvsx.entities.*; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; import org.springframework.data.util.Streamable; @@ -17,10 +18,6 @@ import java.time.LocalDateTime; import java.util.Collection; -import org.eclipse.openvsx.entities.Extension; -import org.eclipse.openvsx.entities.ExtensionVersion; -import org.eclipse.openvsx.entities.PersonalAccessToken; - public interface ExtensionVersionRepository extends Repository { Streamable findByExtension(Extension extension); @@ -43,6 +40,10 @@ public interface ExtensionVersionRepository extends Repository findByPublishedWithAndActive(PersonalAccessToken publishedWith, boolean active); + Streamable findAll(); + + Streamable findBySignatureKeyPairNotOrSignatureKeyPairIsNull(SignatureKeyPair keyPair); + @Query("select ev from ExtensionVersion ev where concat(',', ev.bundledExtensions, ',') like concat('%,', ?1, ',%')") Streamable findByBundledExtensions(String extensionId); @@ -53,4 +54,8 @@ public interface ExtensionVersionRepository extends Repository { Streamable findByExtension(ExtensionVersion extVersion); @@ -37,4 +36,8 @@ public interface FileResourceRepository extends Repository { Streamable findByExtensionAndTypeIn(ExtensionVersion extVersion, Collection types); void deleteByExtensionAndType(ExtensionVersion extVersion, String type); + + void deleteByType(String type); + + Streamable findByType(String type); } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index 247e44676..aefbac24d 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -10,6 +10,7 @@ package org.eclipse.openvsx.repositories; import org.eclipse.openvsx.entities.*; +import org.eclipse.openvsx.util.NamingUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Streamable; import org.springframework.stereotype.Component; @@ -20,6 +21,7 @@ import java.util.Map; import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD; +import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD_SIG; @Component public class RepositoryService { @@ -41,6 +43,7 @@ public class RepositoryService { @Autowired AdminStatisticsRepository adminStatisticsRepo; @Autowired AdminStatisticCalculationsRepository adminStatisticCalculationsRepo; @Autowired MigrationItemRepository migrationItemRepo; + @Autowired SignatureKeyPairRepository signatureKeyPairRepo; public Namespace findNamespace(String name) { return namespaceRepo.findByNameIgnoreCase(name); @@ -131,15 +134,11 @@ public Streamable findActiveVersions(Collection ext } public Streamable findBundledExtensionsReference(Extension extension) { - return extensionVersionRepo.findByBundledExtensions(extensionId(extension)); + return extensionVersionRepo.findByBundledExtensions(NamingUtil.toExtensionId(extension)); } public Streamable findDependenciesReference(Extension extension) { - return extensionVersionRepo.findByDependencies(extensionId(extension)); - } - - private String extensionId(Extension extension) { - return extension.getNamespace().getName() + "." + extension.getName(); + return extensionVersionRepo.findByDependencies(NamingUtil.toExtensionId(extension)); } public Streamable findExtensions(UserData user) { @@ -174,6 +173,10 @@ public Streamable findDownloadsByStorageTypeAndName(String storage return fileResourceRepo.findByTypeAndStorageTypeAndNameIgnoreCaseIn(DOWNLOAD, storageType, names); } + public Streamable findFilesByType(String type) { + return fileResourceRepo.findByType(type); + } + public FileResource findFileByType(ExtensionVersion extVersion, String type) { return fileResourceRepo.findByExtensionAndType(extVersion, type); } @@ -429,4 +432,29 @@ public Double getAverageReviewRating(Extension extension) { public Streamable findFileResources(Namespace namespace) { return fileResourceRepo.findByExtensionExtensionNamespace(namespace); } + + public SignatureKeyPair findActiveKeyPair() { + return signatureKeyPairRepo.findByActiveTrue(); + } + + public Streamable findVersions() { + return extensionVersionRepo.findAll(); + } + + public Streamable findVersionsWithout(SignatureKeyPair keyPair) { + return extensionVersionRepo.findBySignatureKeyPairNotOrSignatureKeyPairIsNull(keyPair); + } + + public void deleteDownloadSigFiles() { + fileResourceRepo.deleteByType(DOWNLOAD_SIG); + } + + public void deleteAllKeyPairs() { + extensionVersionRepo.setKeyPairsNull(); + signatureKeyPairRepo.deleteAll(); + } + + public SignatureKeyPair findKeyPair(String publicId) { + return signatureKeyPairRepo.findByPublicId(publicId); + } } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/SignatureKeyPairRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/SignatureKeyPairRepository.java new file mode 100644 index 000000000..a22e60386 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/repositories/SignatureKeyPairRepository.java @@ -0,0 +1,22 @@ +/** ****************************************************************************** + * Copyright (c) 2023 Precies. Software Ltd and others + * + * 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.repositories; + +import org.eclipse.openvsx.entities.SignatureKeyPair; +import org.springframework.data.repository.Repository; + +public interface SignatureKeyPairRepository extends Repository { + + SignatureKeyPair findByActiveTrue(); + + void deleteAll(); + + SignatureKeyPair findByPublicId(String publicId); +} diff --git a/server/src/main/java/org/eclipse/openvsx/search/RelevanceService.java b/server/src/main/java/org/eclipse/openvsx/search/RelevanceService.java index d7846dc45..f9203e7c9 100644 --- a/server/src/main/java/org/eclipse/openvsx/search/RelevanceService.java +++ b/server/src/main/java/org/eclipse/openvsx/search/RelevanceService.java @@ -16,6 +16,7 @@ import org.eclipse.openvsx.entities.ExtensionVersion; import org.eclipse.openvsx.entities.NamespaceMembership; import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.NamingUtil; import org.eclipse.openvsx.util.TimeUtil; import org.eclipse.openvsx.util.VersionService; import org.slf4j.Logger; @@ -130,7 +131,7 @@ private double calculateRelevance(Extension extension, ExtensionVersion latest, } if (Double.isNaN(entry.relevance) || Double.isInfinite(entry.relevance)) { - var message = "Invalid relevance for entry " + entry.namespace + "." + entry.name; + var message = "Invalid relevance for entry " + NamingUtil.toExtensionId(entry); try { message += " " + new ObjectMapper().writeValueAsString(stats); } catch (JsonProcessingException exc) { diff --git a/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java b/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java new file mode 100644 index 000000000..8c200345d --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java @@ -0,0 +1,88 @@ +/** ****************************************************************************** + * Copyright (c) 2023 Precies. Software Ltd and others + * + * 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.util; + +import com.google.common.base.Strings; +import org.eclipse.openvsx.adapter.ExtensionQueryResult; +import org.eclipse.openvsx.entities.Extension; +import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.json.ExtensionJson; +import org.eclipse.openvsx.search.ExtensionSearch; + +public class NamingUtil { + + private NamingUtil() {} + + public static String toFileFormat(ExtensionVersion extVersion, String suffix) { + return toFileFormat(extVersion) + suffix; + } + + public static String toFileFormat(ExtensionVersion extVersion) { + var extension = extVersion.getExtension(); + var namespace = extension.getNamespace(); + return toFileFormat(namespace.getName(), extension.getName(), extVersion.getTargetPlatform(), extVersion.getVersion()); + } + + public static String toFileFormat(String namespace, String extension, String targetPlatform, String version, String suffix) { + return toFileFormat(namespace, extension, targetPlatform, version) + suffix; + } + + public static String toFileFormat(String namespace, String extension, String targetPlatform, String version) { + var name = toExtensionId(namespace, extension) + "-" + version; + if(!TargetPlatform.isUniversal(targetPlatform)) { + name += "@" + targetPlatform; + } + + return name; + } + + public static String toLogFormat(ExtensionVersion extVersion) { + var extension = extVersion.getExtension(); + var namespace = extension.getNamespace(); + return toLogFormat(namespace.getName(), extension.getName(), extVersion.getTargetPlatform(), extVersion.getVersion()); + } + + public static String toLogFormat(ExtensionJson json) { + return toLogFormat(json.namespace, json.name, json.targetPlatform, json.version); + } + + public static String toLogFormat(String namespace, String extension, String version) { + return toLogFormat(namespace, extension, null, version); + } + + public static String toLogFormat(String namespace, String extension, String targetPlatform, String version) { + var name = toExtensionId(namespace, extension); + if(!Strings.isNullOrEmpty(version)) { + name += " " + version; + } + if(!Strings.isNullOrEmpty(targetPlatform) && !TargetPlatform.isUniversal(targetPlatform)) { + name += " (" + targetPlatform + ")"; + } + + return name; + } + + public static String toExtensionId(Extension extension) { + var namespace = extension.getNamespace(); + return toExtensionId(namespace.getName(), extension.getName()); + } + + public static String toExtensionId(ExtensionQueryResult.Extension extension) { + return toExtensionId(extension.publisher.publisherName, extension.extensionName); + } + + public static String toExtensionId(ExtensionSearch search) { + return toExtensionId(search.namespace, search.name); + } + + public static String toExtensionId(String namespace, String extension) { + return namespace + "." + extension; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java b/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java index 0fd812015..909b5fcc0 100644 --- a/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java +++ b/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java @@ -231,4 +231,9 @@ public static String extractWildcardPath(HttpServletRequest request, String patt ? new AntPathMatcher().extractPathWithinPattern(pattern, path) : ""; } + + public static String getPublicKeyUrl(ExtensionVersion extVersion) { + var publicId = extVersion.getSignatureKeyPair().getPublicId(); + return createApiUrl(getBaseUrl(), "api", "-", "public-key", publicId); + } } diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Keys.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Keys.java index db3faa8de..e3ffdb499 100644 --- a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Keys.java +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Keys.java @@ -31,6 +31,7 @@ import org.eclipse.openvsx.jooq.tables.PersistedLog; import org.eclipse.openvsx.jooq.tables.PersonalAccessToken; import org.eclipse.openvsx.jooq.tables.Shedlock; +import org.eclipse.openvsx.jooq.tables.SignatureKeyPair; import org.eclipse.openvsx.jooq.tables.SpringSession; import org.eclipse.openvsx.jooq.tables.SpringSessionAttributes; import org.eclipse.openvsx.jooq.tables.UserData; @@ -61,6 +62,7 @@ import org.eclipse.openvsx.jooq.tables.records.PersistedLogRecord; import org.eclipse.openvsx.jooq.tables.records.PersonalAccessTokenRecord; import org.eclipse.openvsx.jooq.tables.records.ShedlockRecord; +import org.eclipse.openvsx.jooq.tables.records.SignatureKeyPairRecord; import org.eclipse.openvsx.jooq.tables.records.SpringSessionAttributesRecord; import org.eclipse.openvsx.jooq.tables.records.SpringSessionRecord; import org.eclipse.openvsx.jooq.tables.records.UserDataRecord; @@ -106,6 +108,7 @@ public class Keys { public static final UniqueKey PERSONAL_ACCESS_TOKEN_PKEY = Internal.createUniqueKey(PersonalAccessToken.PERSONAL_ACCESS_TOKEN, DSL.name("personal_access_token_pkey"), new TableField[] { PersonalAccessToken.PERSONAL_ACCESS_TOKEN.ID }, true); public static final UniqueKey UKJEUD5MSSQBQKID58RD2K1INOF = Internal.createUniqueKey(PersonalAccessToken.PERSONAL_ACCESS_TOKEN, DSL.name("ukjeud5mssqbqkid58rd2k1inof"), new TableField[] { PersonalAccessToken.PERSONAL_ACCESS_TOKEN.VALUE }, true); public static final UniqueKey SHEDLOCK_PKEY = Internal.createUniqueKey(Shedlock.SHEDLOCK, DSL.name("shedlock_pkey"), new TableField[] { Shedlock.SHEDLOCK.NAME }, true); + public static final UniqueKey SIGNATURE_KEY_PAIR_PKEY = Internal.createUniqueKey(SignatureKeyPair.SIGNATURE_KEY_PAIR, DSL.name("signature_key_pair_pkey"), new TableField[] { SignatureKeyPair.SIGNATURE_KEY_PAIR.ID }, true); public static final UniqueKey SPRING_SESSION_PK = Internal.createUniqueKey(SpringSession.SPRING_SESSION, DSL.name("spring_session_pk"), new TableField[] { SpringSession.SPRING_SESSION.PRIMARY_ID }, true); public static final UniqueKey SPRING_SESSION_ATTRIBUTES_PK = Internal.createUniqueKey(SpringSessionAttributes.SPRING_SESSION_ATTRIBUTES, DSL.name("spring_session_attributes_pk"), new TableField[] { SpringSessionAttributes.SPRING_SESSION_ATTRIBUTES.SESSION_PRIMARY_ID, SpringSessionAttributes.SPRING_SESSION_ATTRIBUTES.ATTRIBUTE_NAME }, true); public static final UniqueKey USER_DATA_PKEY = Internal.createUniqueKey(UserData.USER_DATA, DSL.name("user_data_pkey"), new TableField[] { UserData.USER_DATA.ID }, true); @@ -123,6 +126,7 @@ public class Keys { public static final ForeignKey EXTENSION__FK64IMD3NRJ67D50TPKJS94NGMN = Internal.createForeignKey(Extension.EXTENSION, DSL.name("fk64imd3nrj67d50tpkjs94ngmn"), new TableField[] { Extension.EXTENSION.NAMESPACE_ID }, Keys.NAMESPACE_PKEY, new TableField[] { Namespace.NAMESPACE.ID }, true); public static final ForeignKey EXTENSION_REVIEW__FKGD2DQDC23OGBNOBX8AFJFPNKP = Internal.createForeignKey(ExtensionReview.EXTENSION_REVIEW, DSL.name("fkgd2dqdc23ogbnobx8afjfpnkp"), new TableField[] { ExtensionReview.EXTENSION_REVIEW.EXTENSION_ID }, Keys.EXTENSION_PKEY, new TableField[] { Extension.EXTENSION.ID }, true); public static final ForeignKey EXTENSION_REVIEW__FKINJBN9GRK135Y6IK0UT4UJP0W = Internal.createForeignKey(ExtensionReview.EXTENSION_REVIEW, DSL.name("fkinjbn9grk135y6ik0ut4ujp0w"), new TableField[] { ExtensionReview.EXTENSION_REVIEW.USER_ID }, Keys.USER_DATA_PKEY, new TableField[] { UserData.USER_DATA.ID }, true); + public static final ForeignKey EXTENSION_VERSION__EXTENSION_VERSION_SIGNATURE_KEY_PAIR_FKEY = Internal.createForeignKey(ExtensionVersion.EXTENSION_VERSION, DSL.name("extension_version_signature_key_pair_fkey"), new TableField[] { ExtensionVersion.EXTENSION_VERSION.SIGNATURE_KEY_PAIR_ID }, Keys.SIGNATURE_KEY_PAIR_PKEY, new TableField[] { SignatureKeyPair.SIGNATURE_KEY_PAIR.ID }, true); public static final ForeignKey EXTENSION_VERSION__FK70KHJ8PM0VACASUIIAQ0W0R80 = Internal.createForeignKey(ExtensionVersion.EXTENSION_VERSION, DSL.name("fk70khj8pm0vacasuiiaq0w0r80"), new TableField[] { ExtensionVersion.EXTENSION_VERSION.PUBLISHED_WITH_ID }, Keys.PERSONAL_ACCESS_TOKEN_PKEY, new TableField[] { PersonalAccessToken.PERSONAL_ACCESS_TOKEN.ID }, true); public static final ForeignKey EXTENSION_VERSION__FKKHS1EC9S9J08FGICQ9PMWU6BT = Internal.createForeignKey(ExtensionVersion.EXTENSION_VERSION, DSL.name("fkkhs1ec9s9j08fgicq9pmwu6bt"), new TableField[] { ExtensionVersion.EXTENSION_VERSION.EXTENSION_ID }, Keys.EXTENSION_PKEY, new TableField[] { Extension.EXTENSION.ID }, true); public static final ForeignKey FILE_RESOURCE__FILE_RESOURCE_EXTENSION_FKEY = Internal.createForeignKey(FileResource.FILE_RESOURCE, DSL.name("file_resource_extension_fkey"), new TableField[] { FileResource.FILE_RESOURCE.EXTENSION_ID }, Keys.EXTENSION_VERSION_PKEY, new TableField[] { ExtensionVersion.EXTENSION_VERSION.ID }, true); diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Public.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Public.java index e7c95fcb4..dc30fa262 100644 --- a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Public.java +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Public.java @@ -35,6 +35,7 @@ import org.eclipse.openvsx.jooq.tables.PersistedLog; import org.eclipse.openvsx.jooq.tables.PersonalAccessToken; import org.eclipse.openvsx.jooq.tables.Shedlock; +import org.eclipse.openvsx.jooq.tables.SignatureKeyPair; import org.eclipse.openvsx.jooq.tables.SpringSession; import org.eclipse.openvsx.jooq.tables.SpringSessionAttributes; import org.eclipse.openvsx.jooq.tables.UserData; @@ -197,6 +198,11 @@ public class Public extends SchemaImpl { */ public final Shedlock SHEDLOCK = Shedlock.SHEDLOCK; + /** + * The table public.signature_key_pair. + */ + public final SignatureKeyPair SIGNATURE_KEY_PAIR = SignatureKeyPair.SIGNATURE_KEY_PAIR; + /** * The table public.spring_session. */ @@ -265,6 +271,7 @@ public final List> getTables() { PersistedLog.PERSISTED_LOG, PersonalAccessToken.PERSONAL_ACCESS_TOKEN, Shedlock.SHEDLOCK, + SignatureKeyPair.SIGNATURE_KEY_PAIR, SpringSession.SPRING_SESSION, SpringSessionAttributes.SPRING_SESSION_ATTRIBUTES, UserData.USER_DATA); diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Tables.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Tables.java index d17c77f0c..0b40132a9 100644 --- a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Tables.java +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/Tables.java @@ -32,6 +32,7 @@ import org.eclipse.openvsx.jooq.tables.PersistedLog; import org.eclipse.openvsx.jooq.tables.PersonalAccessToken; import org.eclipse.openvsx.jooq.tables.Shedlock; +import org.eclipse.openvsx.jooq.tables.SignatureKeyPair; import org.eclipse.openvsx.jooq.tables.SpringSession; import org.eclipse.openvsx.jooq.tables.SpringSessionAttributes; import org.eclipse.openvsx.jooq.tables.UserData; @@ -183,6 +184,11 @@ public class Tables { */ public static final Shedlock SHEDLOCK = Shedlock.SHEDLOCK; + /** + * The table public.signature_key_pair. + */ + public static final SignatureKeyPair SIGNATURE_KEY_PAIR = SignatureKeyPair.SIGNATURE_KEY_PAIR; + /** * The table public.spring_session. */ diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/ExtensionVersion.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/ExtensionVersion.java index 46485d389..2fc2a8eff 100644 --- a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/ExtensionVersion.java +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/ExtensionVersion.java @@ -183,6 +183,11 @@ public Class getRecordType() { */ public final TableField SPONSOR_LINK = createField(DSL.name("sponsor_link"), SQLDataType.VARCHAR(255), this, ""); + /** + * The column public.extension_version.signature_key_pair_id. + */ + public final TableField SIGNATURE_KEY_PAIR_ID = createField(DSL.name("signature_key_pair_id"), SQLDataType.BIGINT, this, ""); + private ExtensionVersion(Name alias, Table aliased) { this(alias, aliased, null); } @@ -238,11 +243,12 @@ public List> getKeys() { @Override public List> getReferences() { - return Arrays.>asList(Keys.EXTENSION_VERSION__FKKHS1EC9S9J08FGICQ9PMWU6BT, Keys.EXTENSION_VERSION__FK70KHJ8PM0VACASUIIAQ0W0R80); + return Arrays.>asList(Keys.EXTENSION_VERSION__FKKHS1EC9S9J08FGICQ9PMWU6BT, Keys.EXTENSION_VERSION__FK70KHJ8PM0VACASUIIAQ0W0R80, Keys.EXTENSION_VERSION__EXTENSION_VERSION_SIGNATURE_KEY_PAIR_FKEY); } private transient Extension _extension; private transient PersonalAccessToken _personalAccessToken; + private transient SignatureKeyPair _signatureKeyPair; public Extension extension() { if (_extension == null) @@ -258,6 +264,13 @@ public PersonalAccessToken personalAccessToken() { return _personalAccessToken; } + public SignatureKeyPair signatureKeyPair() { + if (_signatureKeyPair == null) + _signatureKeyPair = new SignatureKeyPair(this, Keys.EXTENSION_VERSION__EXTENSION_VERSION_SIGNATURE_KEY_PAIR_FKEY); + + return _signatureKeyPair; + } + @Override public ExtensionVersion as(String alias) { return new ExtensionVersion(DSL.name(alias), this); diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/SignatureKeyPair.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/SignatureKeyPair.java new file mode 100644 index 000000000..d20d2ed0b --- /dev/null +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/SignatureKeyPair.java @@ -0,0 +1,162 @@ +/* + * This file is generated by jOOQ. + */ +package org.eclipse.openvsx.jooq.tables; + + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.openvsx.jooq.Keys; +import org.eclipse.openvsx.jooq.Public; +import org.eclipse.openvsx.jooq.tables.records.SignatureKeyPairRecord; +import org.jooq.Field; +import org.jooq.ForeignKey; +import org.jooq.Name; +import org.jooq.Record; +import org.jooq.Row6; +import org.jooq.Schema; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.UniqueKey; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.TableImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class SignatureKeyPair extends TableImpl { + + private static final long serialVersionUID = 1L; + + /** + * The reference instance of public.signature_key_pair + */ + public static final SignatureKeyPair SIGNATURE_KEY_PAIR = new SignatureKeyPair(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return SignatureKeyPairRecord.class; + } + + /** + * The column public.signature_key_pair.id. + */ + public final TableField ID = createField(DSL.name("id"), SQLDataType.BIGINT.nullable(false), this, ""); + + /** + * The column public.signature_key_pair.public_id. + */ + public final TableField PUBLIC_ID = createField(DSL.name("public_id"), SQLDataType.VARCHAR(128), this, ""); + + /** + * The column public.signature_key_pair.private_key. + */ + public final TableField PRIVATE_KEY = createField(DSL.name("private_key"), SQLDataType.BLOB.nullable(false), this, ""); + + /** + * The column public.signature_key_pair.public_key_text. + */ + public final TableField PUBLIC_KEY_TEXT = createField(DSL.name("public_key_text"), SQLDataType.VARCHAR(255).nullable(false), this, ""); + + /** + * The column public.signature_key_pair.created. + */ + public final TableField CREATED = createField(DSL.name("created"), SQLDataType.LOCALDATETIME(6), this, ""); + + /** + * The column public.signature_key_pair.active. + */ + public final TableField ACTIVE = createField(DSL.name("active"), SQLDataType.BOOLEAN.nullable(false), this, ""); + + private SignatureKeyPair(Name alias, Table aliased) { + this(alias, aliased, null); + } + + private SignatureKeyPair(Name alias, Table aliased, Field[] parameters) { + super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table()); + } + + /** + * Create an aliased public.signature_key_pair table reference + */ + public SignatureKeyPair(String alias) { + this(DSL.name(alias), SIGNATURE_KEY_PAIR); + } + + /** + * Create an aliased public.signature_key_pair table reference + */ + public SignatureKeyPair(Name alias) { + this(alias, SIGNATURE_KEY_PAIR); + } + + /** + * Create a public.signature_key_pair table reference + */ + public SignatureKeyPair() { + this(DSL.name("signature_key_pair"), null); + } + + public SignatureKeyPair(Table child, ForeignKey key) { + super(child, key, SIGNATURE_KEY_PAIR); + } + + @Override + public Schema getSchema() { + return Public.PUBLIC; + } + + @Override + public UniqueKey getPrimaryKey() { + return Keys.SIGNATURE_KEY_PAIR_PKEY; + } + + @Override + public List> getKeys() { + return Arrays.>asList(Keys.SIGNATURE_KEY_PAIR_PKEY); + } + + @Override + public SignatureKeyPair as(String alias) { + return new SignatureKeyPair(DSL.name(alias), this); + } + + @Override + public SignatureKeyPair as(Name alias) { + return new SignatureKeyPair(alias, this); + } + + /** + * Rename this table + */ + @Override + public SignatureKeyPair rename(String name) { + return new SignatureKeyPair(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public SignatureKeyPair rename(Name name) { + return new SignatureKeyPair(name, null); + } + + // ------------------------------------------------------------------------- + // Row6 type methods + // ------------------------------------------------------------------------- + + @Override + public Row6 fieldsRow() { + return (Row6) super.fieldsRow(); + } +} diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/ExtensionVersionRecord.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/ExtensionVersionRecord.java index 2a7c456da..b822a7f2d 100644 --- a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/ExtensionVersionRecord.java +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/ExtensionVersionRecord.java @@ -397,6 +397,20 @@ public String getSponsorLink() { return (String) get(26); } + /** + * Setter for public.extension_version.signature_key_pair_id. + */ + public void setSignatureKeyPairId(Long value) { + set(27, value); + } + + /** + * Getter for public.extension_version.signature_key_pair_id. + */ + public Long getSignatureKeyPairId() { + return (Long) get(27); + } + // ------------------------------------------------------------------------- // Primary key information // ------------------------------------------------------------------------- @@ -420,7 +434,7 @@ public ExtensionVersionRecord() { /** * Create a detached, initialised ExtensionVersionRecord */ - public ExtensionVersionRecord(Long id, String bugs, String description, String displayName, String galleryColor, String galleryTheme, String homepage, String license, String markdown, Boolean preview, String qna, String repository, LocalDateTime timestamp, String version, Long extensionId, Long publishedWithId, Boolean active, String dependencies, String bundledExtensions, String engines, String categories, String tags, String extensionKind, Boolean preRelease, String targetPlatform, String localizedLanguages, String sponsorLink) { + public ExtensionVersionRecord(Long id, String bugs, String description, String displayName, String galleryColor, String galleryTheme, String homepage, String license, String markdown, Boolean preview, String qna, String repository, LocalDateTime timestamp, String version, Long extensionId, Long publishedWithId, Boolean active, String dependencies, String bundledExtensions, String engines, String categories, String tags, String extensionKind, Boolean preRelease, String targetPlatform, String localizedLanguages, String sponsorLink, Long signatureKeyPairId) { super(ExtensionVersion.EXTENSION_VERSION); setId(id); @@ -450,5 +464,6 @@ public ExtensionVersionRecord(Long id, String bugs, String description, String d setTargetPlatform(targetPlatform); setLocalizedLanguages(localizedLanguages); setSponsorLink(sponsorLink); + setSignatureKeyPairId(signatureKeyPairId); } } diff --git a/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/SignatureKeyPairRecord.java b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/SignatureKeyPairRecord.java new file mode 100644 index 000000000..e41b82c6a --- /dev/null +++ b/server/src/main/jooq-gen/org/eclipse/openvsx/jooq/tables/records/SignatureKeyPairRecord.java @@ -0,0 +1,293 @@ +/* + * This file is generated by jOOQ. + */ +package org.eclipse.openvsx.jooq.tables.records; + + +import java.time.LocalDateTime; + +import org.eclipse.openvsx.jooq.tables.SignatureKeyPair; +import org.jooq.Field; +import org.jooq.Record1; +import org.jooq.Record6; +import org.jooq.Row6; +import org.jooq.impl.UpdatableRecordImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class SignatureKeyPairRecord extends UpdatableRecordImpl implements Record6 { + + private static final long serialVersionUID = 1L; + + /** + * Setter for public.signature_key_pair.id. + */ + public void setId(Long value) { + set(0, value); + } + + /** + * Getter for public.signature_key_pair.id. + */ + public Long getId() { + return (Long) get(0); + } + + /** + * Setter for public.signature_key_pair.public_id. + */ + public void setPublicId(String value) { + set(1, value); + } + + /** + * Getter for public.signature_key_pair.public_id. + */ + public String getPublicId() { + return (String) get(1); + } + + /** + * Setter for public.signature_key_pair.private_key. + */ + public void setPrivateKey(byte[] value) { + set(2, value); + } + + /** + * Getter for public.signature_key_pair.private_key. + */ + public byte[] getPrivateKey() { + return (byte[]) get(2); + } + + /** + * Setter for public.signature_key_pair.public_key_text. + */ + public void setPublicKeyText(String value) { + set(3, value); + } + + /** + * Getter for public.signature_key_pair.public_key_text. + */ + public String getPublicKeyText() { + return (String) get(3); + } + + /** + * Setter for public.signature_key_pair.created. + */ + public void setCreated(LocalDateTime value) { + set(4, value); + } + + /** + * Getter for public.signature_key_pair.created. + */ + public LocalDateTime getCreated() { + return (LocalDateTime) get(4); + } + + /** + * Setter for public.signature_key_pair.active. + */ + public void setActive(Boolean value) { + set(5, value); + } + + /** + * Getter for public.signature_key_pair.active. + */ + public Boolean getActive() { + return (Boolean) get(5); + } + + // ------------------------------------------------------------------------- + // Primary key information + // ------------------------------------------------------------------------- + + @Override + public Record1 key() { + return (Record1) super.key(); + } + + // ------------------------------------------------------------------------- + // Record6 type implementation + // ------------------------------------------------------------------------- + + @Override + public Row6 fieldsRow() { + return (Row6) super.fieldsRow(); + } + + @Override + public Row6 valuesRow() { + return (Row6) super.valuesRow(); + } + + @Override + public Field field1() { + return SignatureKeyPair.SIGNATURE_KEY_PAIR.ID; + } + + @Override + public Field field2() { + return SignatureKeyPair.SIGNATURE_KEY_PAIR.PUBLIC_ID; + } + + @Override + public Field field3() { + return SignatureKeyPair.SIGNATURE_KEY_PAIR.PRIVATE_KEY; + } + + @Override + public Field field4() { + return SignatureKeyPair.SIGNATURE_KEY_PAIR.PUBLIC_KEY_TEXT; + } + + @Override + public Field field5() { + return SignatureKeyPair.SIGNATURE_KEY_PAIR.CREATED; + } + + @Override + public Field field6() { + return SignatureKeyPair.SIGNATURE_KEY_PAIR.ACTIVE; + } + + @Override + public Long component1() { + return getId(); + } + + @Override + public String component2() { + return getPublicId(); + } + + @Override + public byte[] component3() { + return getPrivateKey(); + } + + @Override + public String component4() { + return getPublicKeyText(); + } + + @Override + public LocalDateTime component5() { + return getCreated(); + } + + @Override + public Boolean component6() { + return getActive(); + } + + @Override + public Long value1() { + return getId(); + } + + @Override + public String value2() { + return getPublicId(); + } + + @Override + public byte[] value3() { + return getPrivateKey(); + } + + @Override + public String value4() { + return getPublicKeyText(); + } + + @Override + public LocalDateTime value5() { + return getCreated(); + } + + @Override + public Boolean value6() { + return getActive(); + } + + @Override + public SignatureKeyPairRecord value1(Long value) { + setId(value); + return this; + } + + @Override + public SignatureKeyPairRecord value2(String value) { + setPublicId(value); + return this; + } + + @Override + public SignatureKeyPairRecord value3(byte[] value) { + setPrivateKey(value); + return this; + } + + @Override + public SignatureKeyPairRecord value4(String value) { + setPublicKeyText(value); + return this; + } + + @Override + public SignatureKeyPairRecord value5(LocalDateTime value) { + setCreated(value); + return this; + } + + @Override + public SignatureKeyPairRecord value6(Boolean value) { + setActive(value); + return this; + } + + @Override + public SignatureKeyPairRecord values(Long value1, String value2, byte[] value3, String value4, LocalDateTime value5, Boolean value6) { + value1(value1); + value2(value2); + value3(value3); + value4(value4); + value5(value5); + value6(value6); + return this; + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached SignatureKeyPairRecord + */ + public SignatureKeyPairRecord() { + super(SignatureKeyPair.SIGNATURE_KEY_PAIR); + } + + /** + * Create a detached, initialised SignatureKeyPairRecord + */ + public SignatureKeyPairRecord(Long id, String publicId, byte[] privateKey, String publicKeyText, LocalDateTime created, Boolean active) { + super(SignatureKeyPair.SIGNATURE_KEY_PAIR); + + setId(id); + setPublicId(publicId); + setPrivateKey(privateKey); + setPublicKeyText(publicKeyText); + setCreated(created); + setActive(active); + } +} diff --git a/server/src/main/resources/db/migration/V1_36__SignatureKeyPair.sql b/server/src/main/resources/db/migration/V1_36__SignatureKeyPair.sql new file mode 100644 index 000000000..e7d9bf985 --- /dev/null +++ b/server/src/main/resources/db/migration/V1_36__SignatureKeyPair.sql @@ -0,0 +1,13 @@ +CREATE TABLE signature_key_pair ( + id BIGINT NOT NULL, + public_id CHARACTER VARYING(128), + private_key BYTEA NOT NULL, + public_key_text CHARACTER VARYING(255) NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE, + active BOOLEAN NOT NULL, + CONSTRAINT signature_key_pair_pkey PRIMARY KEY (id) +); + +ALTER TABLE extension_version ADD COLUMN signature_key_pair_id BIGINT; +ALTER TABLE ONLY public.extension_version + ADD CONSTRAINT extension_version_signature_key_pair_fkey FOREIGN KEY (signature_key_pair_id) REFERENCES signature_key_pair(id); diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index 8fd02aef1..11e14c4ad 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -9,26 +9,6 @@ ********************************************************************************/ package org.eclipse.openvsx; -import static org.eclipse.openvsx.entities.FileResource.*; -import static org.mockito.ArgumentMatchers.*; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.*; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import javax.persistence.EntityManager; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -39,6 +19,7 @@ import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.publish.PublishExtensionVersionService; import org.eclipse.openvsx.repositories.RepositoryService; @@ -74,6 +55,25 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.support.TransactionTemplate; +import javax.persistence.EntityManager; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.eclipse.openvsx.entities.FileResource.*; +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @WebMvcTest(RegistryAPI.class) @AutoConfigureWebClient @MockBean({ @@ -92,6 +92,9 @@ public class RegistryAPITest { @MockBean SearchUtilService search; + @MockBean + ExtensionVersionIntegrityService integrityService; + @MockBean EntityManager entityManager; @@ -157,6 +160,23 @@ public void testExtension() throws Exception { }))); } + @Test + public void testExtensionWithPublicKey() throws Exception { + Mockito.when(integrityService.isEnabled()).thenReturn(true); + var extVersion = mockExtensionWithSignature(); + var keyPair = new SignatureKeyPair(); + keyPair.setPublicId("123-456-7890"); + extVersion.setSignatureKeyPair(keyPair); + mockMvc.perform(get("/api/{namespace}/{extension}", "foo", "bar")) + .andExpect(status().isOk()) + .andExpect(content().json(extensionJson(e -> { + e.namespace = "foo"; + e.name = "bar"; + e.version = "1.0.0"; + e.files = Map.of("publicKey", "http://localhost/api/-/public-key/" + keyPair.getPublicId()); + }))); + } + @Test public void testExtensionNonDefaultTarget() throws Exception { var extVersion = mockExtension("alpine-x64"); @@ -1368,7 +1388,7 @@ public void testPublishSameVersionDifferentTargetPlatformPreRelease() throws Exc .contentType(MediaType.APPLICATION_OCTET_STREAM) .content(bytes)) .andExpect(status().isCreated()) - .andExpect(content().json(warningJson("A stable release already exists for foo.bar-1.0.0.\n" + + .andExpect(content().json(warningJson("A stable release already exists for foo.bar 1.0.0.\n" + "To prevent update conflicts, we recommend that this pre-release uses 1.1.0 as its version instead."))); } @@ -1387,7 +1407,7 @@ public void testPublishSameVersionDifferentTargetPlatformStableRelease() throws .contentType(MediaType.APPLICATION_OCTET_STREAM) .content(bytes)) .andExpect(status().isCreated()) - .andExpect(content().json(warningJson("A pre-release already exists for foo.bar-1.5.0.\n" + + .andExpect(content().json(warningJson("A pre-release already exists for foo.bar 1.5.0.\n" + "To prevent update conflicts, we recommend that this stable release uses 1.6.0 as its version instead."))); } @@ -1732,11 +1752,19 @@ private ExtensionVersion mockExtensionVersion() { return extVersion; } + private ExtensionVersion mockExtensionWithSignature() { + return mockExtension(TargetPlatform.NAME_UNIVERSAL, true); + } + private ExtensionVersion mockExtension() { return mockExtension(TargetPlatform.NAME_UNIVERSAL); } private ExtensionVersion mockExtension(String targetPlatform) { + return mockExtension(targetPlatform, false); + } + + private ExtensionVersion mockExtension(String targetPlatform, boolean withSignature) { var namespace = new Namespace(); namespace.setName("foo"); namespace.setPublicId("1234"); @@ -1781,13 +1809,30 @@ private ExtensionVersion mockExtension(String targetPlatform) { download.setType(DOWNLOAD); download.setStorageType(STORAGE_DB); download.setName("extension-1.0.0.vsix"); + var signature = new FileResource(); + if(withSignature) { + signature.setExtension(extVersion); + signature.setType(DOWNLOAD_SIG); + signature.setStorageType(STORAGE_DB); + signature.setName("extension-1.0.0.sigzip"); + } Mockito.when(entityManager.merge(download)).thenReturn(download); Mockito.when(repositories.findFilesByType(anyCollection(), anyCollection())).thenAnswer(invocation -> { Collection extVersions = invocation.getArgument(0); Collection types = invocation.getArgument(1); - return types.contains(DOWNLOAD) && extVersions.iterator().hasNext() && download.getExtension().equals(extVersions.iterator().next()) - ? List.of(download) - : Collections.emptyList(); + var extensionVersion = extVersions.iterator().hasNext() + ? extVersions.iterator().next() + : null; + + var files = new ArrayList<>(); + if(types.contains(DOWNLOAD) && download.getExtension().equals(extensionVersion)) { + files.add(download); + } + if(withSignature && types.contains(DOWNLOAD_SIG) && signature.getExtension().equals(extensionVersion)) { + files.add(signature); + } + + return files; }); return extVersion; diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java index 56c653f1d..e95308ce9 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -37,6 +37,7 @@ import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.search.ExtensionSearch; import org.eclipse.openvsx.search.ISearchService; @@ -85,6 +86,9 @@ public class VSCodeAPITest { @MockBean SearchUtilService search; + @MockBean + ExtensionVersionIntegrityService integrityService; + @Autowired MockMvc mockMvc; @@ -642,6 +646,8 @@ private Extension mockSearch(String targetPlatform, String namespaceName, boolea var searchHits = new SearchHitsImpl<>(searchResults.size(), TotalHitsRelation.EQUAL_TO, 1.0f, "1", searchResults, null, null); + Mockito.when(integrityService.isEnabled()) + .thenReturn(true); Mockito.when(search.isEnabled()) .thenReturn(true); var searchOptions = new ISearchService.Options("yaml", null, targetPlatform, 50, 0, "desc", "relevance", false, builtInExtensionNamespace); @@ -715,12 +721,16 @@ private ExtensionVersion mockExtensionVersion(Extension extension, long id, Stri extVersion.setLocalizedLanguages(Collections.emptyList()); extVersion.setExtension(extension); + var keyPair = new SignatureKeyPair(); + keyPair.setPublicId("123-456-789"); + extVersion.setSignatureKeyPair(keyPair); + mockFileResources(List.of(extVersion)); return extVersion; } private void mockFileResources(List extensionVersions) { - var types = List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, CHANGELOG, VSIXMANIFEST); + var types = List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, CHANGELOG, VSIXMANIFEST, DOWNLOAD_SIG); var files = new ArrayList(); for(var extVersion : extensionVersions) { @@ -732,8 +742,9 @@ private void mockFileResources(List extensionVersions) { files.add(mockFileResource(id * 100 + 9, extVersion, "LICENSE.txt", LICENSE)); files.add(mockFileResource(id * 100 + 10, extVersion, "icon128.png", ICON)); files.add(mockFileResource(id * 100 + 11, extVersion, "extension.vsixmanifest", VSIXMANIFEST)); - files.add(mockFileResource(id * 100 + 12, extVersion, "extension/themes/dark.json", RESOURCE)); - files.add(mockFileResource(id * 100 + 13, extVersion, "extension/img/logo.png", RESOURCE)); + files.add(mockFileResource(id * 100 + 12, extVersion, "redhat.vscode-yaml-0.5.2.sigzip", DOWNLOAD_SIG)); + files.add(mockFileResource(id * 100 + 13, extVersion, "extension/themes/dark.json", RESOURCE)); + files.add(mockFileResource(id * 100 + 14, extVersion, "extension/img/logo.png", RESOURCE)); } var ids = extensionVersions.stream().map(ExtensionVersion::getId).collect(Collectors.toSet()); @@ -860,6 +871,14 @@ private ExtensionVersion mockExtensionVersion(String targetPlatform) throws Json Mockito.when(entityManager.merge(vsixManifestFile)).thenReturn(vsixManifestFile); Mockito.when(repositories.findFileByType(extVersion, VSIXMANIFEST)) .thenReturn(vsixManifestFile); + var signatureFile = new FileResource(); + signatureFile.setExtension(extVersion); + signatureFile.setName("redhat.vscode-yaml-0.5.2.sigzip"); + signatureFile.setType(FileResource.DOWNLOAD_SIG); + signatureFile.setStorageType(FileResource.STORAGE_DB); + Mockito.when(entityManager.merge(signatureFile)).thenReturn(signatureFile); + Mockito.when(repositories.findFileByType(extVersion, FileResource.DOWNLOAD_SIG)) + .thenReturn(signatureFile); var webResourceFile = new FileResource(); webResourceFile.setExtension(extVersion); webResourceFile.setName("extension/img/logo.png"); @@ -872,9 +891,9 @@ private ExtensionVersion mockExtensionVersion(String targetPlatform) throws Json Mockito.when(repositories.findFilesByType(anyCollection(), anyCollection())).thenAnswer(invocation -> { Collection extVersions = invocation.getArgument(0); var types = invocation.getArgument(1); - var expectedTypes = Arrays.asList(FileResource.MANIFEST, FileResource.README, FileResource.LICENSE, FileResource.ICON, FileResource.DOWNLOAD, FileResource.CHANGELOG, VSIXMANIFEST); + var expectedTypes = Arrays.asList(FileResource.MANIFEST, FileResource.README, FileResource.LICENSE, FileResource.ICON, FileResource.DOWNLOAD, FileResource.CHANGELOG, VSIXMANIFEST, DOWNLOAD_SIG); return types.equals(expectedTypes) && extVersions.iterator().hasNext() && extVersion.equals(extVersions.iterator().next()) - ? Streamable.of(manifestFile, readmeFile, licenseFile, iconFile, extensionFile, changelogFile, vsixManifestFile) + ? Streamable.of(manifestFile, readmeFile, licenseFile, iconFile, extensionFile, changelogFile, vsixManifestFile, signatureFile) : Streamable.empty(); }); diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java index 3465a743c..194f61fd8 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -19,6 +19,7 @@ import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.search.SearchUtilService; @@ -85,6 +86,9 @@ public class AdminAPITest { @MockBean EntityManager entityManager; + @MockBean + ExtensionVersionIntegrityService integrityService; + @Autowired MockMvc mockMvc; @@ -305,7 +309,7 @@ public void testDeleteBundledExtension() throws Exception { .with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN")))) .with(csrf().asHeader())) .andExpect(status().isBadRequest()) - .andExpect(content().json(errorJson("Extension foobar.baz is bundled by the following extension packs: foobar.bundle@1"))); + .andExpect(content().json(errorJson("Extension foobar.baz is bundled by the following extension packs: foobar.bundle-1"))); } @Test @@ -316,7 +320,7 @@ public void testDeleteDependingExtension() throws Exception { .with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN")))) .with(csrf().asHeader())) .andExpect(status().isBadRequest()) - .andExpect(content().json(errorJson("The following extensions have a dependency on foobar.baz: foobar.dependant@1"))); + .andExpect(content().json(errorJson("The following extensions have a dependency on foobar.baz: foobar.dependant-1"))); } @Test diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index 956262c86..02ac81235 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -21,11 +21,7 @@ import javax.persistence.EntityManager; import javax.transaction.Transactional; -import org.eclipse.openvsx.entities.Extension; -import org.eclipse.openvsx.entities.ExtensionVersion; -import org.eclipse.openvsx.entities.Namespace; -import org.eclipse.openvsx.entities.PersonalAccessToken; -import org.eclipse.openvsx.entities.UserData; +import org.eclipse.openvsx.entities.*; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.invocation.Invocation; @@ -64,7 +60,10 @@ void testExecuteQueries() { var extVersion = new ExtensionVersion(); extVersion.setTargetPlatform("targetPlatform"); var personalAccessToken = new PersonalAccessToken(); - Stream.of(extension, namespace, userData, extVersion, personalAccessToken).forEach(em::persist); + var keyPair = new SignatureKeyPair(); + keyPair.setPrivateKey(new byte[0]); + keyPair.setPublicKeyText(""); + Stream.of(extension, namespace, userData, extVersion, personalAccessToken, keyPair).forEach(em::persist); em.flush(); // record executed queries @@ -168,7 +167,14 @@ void testExecuteQueries() { () -> repositories.findAllNotMatchingByExtensionId(STRING_LIST), () -> repositories.getAverageReviewRating(null), () -> repositories.getAverageReviewRating(), - () -> repositories.findFileResources(null) + () -> repositories.findFileResources(null), + () -> repositories.findKeyPair(null), + () -> repositories.findActiveKeyPair(), + () -> repositories.findFilesByType(null), + () -> repositories.findVersions(), + () -> repositories.findVersionsWithout(keyPair), + () -> repositories.deleteDownloadSigFiles(), + () -> repositories.deleteAllKeyPairs() ); // check that we did not miss anything diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response-alpine.json b/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response-alpine.json index bf9c9f8b5..39b43dc02 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response-alpine.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response-alpine.json @@ -49,6 +49,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/alpine-arm64/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/alpine-arm64/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response.json b/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response.json index 76d4effe0..9d7a7cc33 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/findid-yaml-response.json @@ -48,6 +48,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response-linux.json b/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response-linux.json index e346dbb0f..0751db9a2 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response-linux.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response-linux.json @@ -49,6 +49,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/linux-x64/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/linux-x64/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response.json b/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response.json index 76d4effe0..9d7a7cc33 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/findname-yaml-response.json @@ -48,6 +48,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-darwin.json b/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-darwin.json index 80d7898fc..81f5df16e 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-darwin.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-darwin.json @@ -49,6 +49,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/darwin-x64/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/darwin-x64/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-targets.json b/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-targets.json index 082274e88..c16d4386c 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-targets.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response-targets.json @@ -49,6 +49,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/darwin-x64/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/darwin-x64/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ @@ -108,6 +116,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/linux-x64/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/linux-x64/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ @@ -167,6 +183,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/alpine-arm64/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/alpine-arm64/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [ diff --git a/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response.json b/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response.json index 76d4effe0..9d7a7cc33 100644 --- a/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response.json +++ b/server/src/test/resources/org/eclipse/openvsx/adapter/search-yaml-response.json @@ -48,6 +48,14 @@ { "assetType": "Microsoft.VisualStudio.Services.VsixManifest", "source": "http://localhost/api/redhat/vscode-yaml/0.5.2/file/extension.vsixmanifest" + }, + { + "assetType": "Microsoft.VisualStudio.Services.VsixSignature", + "source": "http://localhost/api/redhat/vscode-yaml/0.5.2/file/redhat.vscode-yaml-0.5.2.sigzip" + }, + { + "assetType": "Microsoft.VisualStudio.Services.PublicKey", + "source": "http://localhost/api/-/public-key/123-456-789" } ], "properties": [