Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ public ExtensionJson toExtensionJson() {
if (this.getPublishedBy() != null) {
json.setPublishedBy(this.getPublishedBy().toUserJson());
}
json.setTrustedPublisher(getPublishedWithTt() != null && getPublishedWithTt() == PersonalAccessTokenType.TPT);
json.setPublishedWithTrustedPublishing(
getPublishedWithTt() != null && getPublishedWithTt() == PersonalAccessTokenType.TPT);
if (this.getDependencies() != null) {
json.setDependencies(toExtensionReferenceJson(this.getDependencies()));
}
Expand Down
12 changes: 6 additions & 6 deletions server/src/main/java/org/eclipse/openvsx/json/ExtensionJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ public static ExtensionJson error(String message) {
@NotNull
private UserJson publishedBy;

@Schema(description = "Indicates whether this version was published using trusted publisher")
@Schema(description = "Indicates whether this version was published using trusted publishing")
@NotNull
private Boolean trustedPublisher;
private Boolean publishedWithTrustedPublishing;

@Schema(hidden = true)
private Boolean active;
Expand Down Expand Up @@ -307,12 +307,12 @@ public void setPublishedBy(UserJson publishedBy) {
this.publishedBy = publishedBy;
}

public Boolean getTrustedPublisher() {
return trustedPublisher;
public Boolean getPublishedWithTrustedPublishing() {
return publishedWithTrustedPublishing;
}

public void setTrustedPublisher(Boolean trustedPublisher) {
this.trustedPublisher = trustedPublisher;
public void setPublishedWithTrustedPublishing(Boolean publishedWithTrustedPublishing) {
this.publishedWithTrustedPublishing = publishedWithTrustedPublishing;
}

public Boolean getActive() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/
package org.eclipse.openvsx;
package org.eclipse.openvsx.trustedpublishing;

import java.util.Objects;

Expand All @@ -25,6 +25,7 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import org.eclipse.openvsx.UserService;
import org.eclipse.openvsx.eclipse.EclipseService;
import org.eclipse.openvsx.entities.TrustedPublisher;
import org.eclipse.openvsx.json.AccessTokenJson;
Expand All @@ -35,7 +36,6 @@
import org.eclipse.openvsx.json.TrustedPublisherStatusJson;
import org.eclipse.openvsx.json.TrustedPublisherTokenRequestJson;
import org.eclipse.openvsx.settings.MutatingOperation;
import org.eclipse.openvsx.trustedpublishing.TrustedPublishingService;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.NotFoundException;

Expand Down Expand Up @@ -69,20 +69,21 @@ public ResponseEntity<TrustedPublisherJson> createTrustedPublisher(
if (user == null) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
eclipseService.checkPublisherAgreement(user);
if (!StringUtils.hasText(request.getProvider())
|| !StringUtils.hasText(request.getNamespace()) || !StringUtils.hasText(request.getExtension())
|| request.getRegistration() == null || request.getRegistration().isEmpty()) {
var json = TrustedPublisherJson
.error("The fields provider, namespace, extension and registration are mandatory.");
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (!Objects.equals(namespace, request.getNamespace())) {
var json = TrustedPublisherJson.error("The namespace in the path and in the request body must match.");
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}

try {
// throws when the user has no signed publisher agreement, and so has to be caught below
eclipseService.checkPublisherAgreement(user);
if (!StringUtils.hasText(request.getProvider())
|| !StringUtils.hasText(request.getNamespace()) || !StringUtils.hasText(request.getExtension())
|| request.getRegistration() == null || request.getRegistration().isEmpty()) {
var json = TrustedPublisherJson
.error("The fields provider, namespace, extension and registration are mandatory.");
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}
if (!Objects.equals(namespace, request.getNamespace())) {
var json = TrustedPublisherJson.error("The namespace in the path and in the request body must match.");
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}

var publisher = trustedPublishing.registerTrustedPublisher(
user,
request.getNamespace(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,30 @@
*****************************************************************************/
package org.eclipse.openvsx.trustedpublishing;

import java.net.URI;
import java.util.List;
import java.util.Map;

import jakarta.annotation.PostConstruct;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import org.eclipse.openvsx.trustedpublishing.TrustedPublishingProperties.GitLabInstance;
import org.eclipse.openvsx.trustedpublishing.github.GitHubTrustedPublishingProvider;

@Configuration
@EnableConfigurationProperties(TrustedPublishingProperties.class)
public class TrustedPublishingConfig {

private final TrustedPublishingProperties properties;

public TrustedPublishingConfig(TrustedPublishingProperties properties) {
this.properties = properties;
}

/**
* Whether trusted publishing is enabled at all.
*/
Expand All @@ -41,7 +56,8 @@ public class TrustedPublishingConfig {
private List<String> forbiddenJwtHeaders;

/**
* The comma separated list of active trusted publishing providers.
* The comma separated list of active trusted publishing providers. An id listed here must be
* {@code github} or one of the configured GitLab instances, see {@link TrustedPublishingProperties}.
* Default: {@code github}.
*/
@Value("${ovsx.trusted-publishing.active-providers:github}")
Expand All @@ -66,6 +82,15 @@ public List<String> getActiveProviders() {
return activeProviders;
}

/**
* The configured GitLab instances, keyed by provider id. Whether an instance can actually be used
* is decided by {@link #getActiveProviders()}.
*/
@NonNull
public Map<String, GitLabInstance> getGitLabInstances() {
return properties.getGitlab();
}

@PostConstruct
public void validate() {
if (enabled) {
Expand All @@ -80,6 +105,37 @@ public void validate() {
throw new IllegalStateException(
"Trusted publishing is enabled, but there are no active providers configured");
}
validateGitLabInstances();
}
}

private void validateGitLabInstances() {
for (var entry : getGitLabInstances().entrySet()) {
var id = entry.getKey();
var instance = entry.getValue();
if (GitHubTrustedPublishingProvider.PROVIDER_ID.equals(id)) {
throw new IllegalStateException(
"GitLab instance '" + id + "' uses the provider id of the GitHub provider");
}
// a configured instance replaces a default one as a whole, so it must carry every field itself
if (instance.getName() == null || instance.getName().isBlank()
|| instance.getUrl() == null || instance.getUrl().isBlank()) {
throw new IllegalStateException("GitLab instance '" + id + "' has no name or no URL configured");
}
for (var url : List.of(instance.getUrl(), instance.getIssuer())) {
if (hostOf(url) == null) {
throw new IllegalStateException("GitLab instance '" + id + "' has a malformed URL: " + url);
}
}
}
}

@Nullable
private static String hostOf(String url) {
try {
return URI.create(url).getHost();
} catch (IllegalArgumentException exc) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/
package org.eclipse.openvsx.trustedpublishing;

import java.util.LinkedHashMap;
import java.util.Map;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import org.eclipse.openvsx.trustedpublishing.gitlab.GitLabTrustedPublishingProvider;

/**
* The trusted publishing provider instances that are known to this registry.
* <p>
* Every GitLab instance behaves the same way, it only differs in its id, name, URL and OIDC issuer,
* so instances are configured rather than coded. Only the public instance is configured out of the box;
* any other one - the Eclipse Foundation instance included - is added by configuration, and becomes
* usable once its id is listed in {@code ovsx.trusted-publishing.active-providers}:
*
* <pre>
* ovsx:
* trusted-publishing:
* active-providers: github,eclipse-gitlab
* gitlab:
* eclipse-gitlab:
* name: Eclipse GitLab
* url: https://gitlab.eclipse.org
* issuer: https://gitlab.eclipse.org # optional, defaults to the URL
* </pre>
*
* The id is persisted with every registration, so renaming it hides the registrations made for it.
* <p>
* Configuring the id of the default instance replaces it as a whole rather than patching single fields,
* so such an entry has to carry the name and the URL itself.
*/
@ConfigurationProperties(prefix = "ovsx.trusted-publishing")
@Validated
public class TrustedPublishingProperties {

@Valid
private Map<String, GitLabInstance> gitlab = defaultGitLabInstances();

/**
* The known GitLab instances, keyed by provider id. Configured instances are added to the public
* instance, which stays available unless its id is redefined.
*/
@NonNull
public Map<String, GitLabInstance> getGitlab() {
return gitlab;
}

public void setGitlab(Map<String, GitLabInstance> gitlab) {
this.gitlab = gitlab;
}

private static Map<String, GitLabInstance> defaultGitLabInstances() {
var instances = new LinkedHashMap<String, GitLabInstance>();
instances.put(
GitLabTrustedPublishingProvider.PROVIDER_ID,
new GitLabInstance("GitLab", GitLabTrustedPublishingProvider.PROVIDER_URL));
return instances;
}

/**
* A single GitLab instance.
*/
public static class GitLabInstance {

@NotBlank
private String name;

@NotBlank
private String url;

@Nullable
private String issuer;

public GitLabInstance() {
// for configuration property binding
}

public GitLabInstance(String name, String url) {
this.name = name;
this.url = url;
}

/**
* The instance name, for human consumption.
*/
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

/**
* The base URL of the instance; the API and the {@code ci_config_ref_uri} claim are derived from it.
*/
public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

/**
* The issuer to expect in the {@code iss} claim of issued OIDC ID tokens. GitLab issues tokens
* under its own base URL, so this defaults to {@link #getUrl()}.
*/
public String getIssuer() {
return issuer == null || issuer.isBlank() ? url : issuer;
}

public void setIssuer(@Nullable String issuer) {
this.issuer = issuer;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@
import org.eclipse.openvsx.json.ResultJson;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.trustedpublishing.github.GitHubTrustedPublishingProvider;
import org.eclipse.openvsx.trustedpublishing.gitlab.EclipseGitLabTrustedPublishingProvider;
import org.eclipse.openvsx.trustedpublishing.gitlab.GitLabTrustedPublishingProvider;
import org.eclipse.openvsx.trustedpublishing.gitlab.GitLabTrustedPublishingProviderSupport;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.NotFoundException;
import org.eclipse.openvsx.util.TimeUtil;
Expand Down Expand Up @@ -71,18 +69,45 @@ public TrustedPublishingService(
this.entityManager = requireNonNull(entityManager);

if (config.isEnabled()) {
this.providers = Map.of(
GitHubTrustedPublishingProvider.PROVIDER_ID,
new GitHubTrustedPublishingProvider(config),
GitLabTrustedPublishingProvider.PROVIDER_ID,
new GitLabTrustedPublishingProvider(config),
EclipseGitLabTrustedPublishingProvider.PROVIDER_ID,
new EclipseGitLabTrustedPublishingProvider(config));
this.providers = createProviders(config);
warnAboutUnknownActiveProviders(config);
} else {
this.providers = Map.of();
}
}

/**
* GitHub is a single, hard-wired provider; every configured GitLab instance becomes one of its own.
*/
private static Map<String, TrustedPublishingProviderSupport> createProviders(TrustedPublishingConfig config) {
var providers = new HashMap<String, TrustedPublishingProviderSupport>();
providers.put(GitHubTrustedPublishingProvider.PROVIDER_ID, new GitHubTrustedPublishingProvider(config));
config.getGitLabInstances()
.forEach(
(providerId, instance) -> providers.put(
providerId,
new GitLabTrustedPublishingProvider(
config,
providerId,
instance.getName(),
instance.getUrl(),
instance.getIssuer())));
return Map.copyOf(providers);
}

/**
* An active provider without a matching definition is silently unusable, which is hard to tell apart
* from a working setup, so say so at startup.
*/
private void warnAboutUnknownActiveProviders(TrustedPublishingConfig config) {
var unknown = config.getActiveProviders().stream().filter(id -> !providers.containsKey(id)).toList();
if (!unknown.isEmpty()) {
logger.warn(
"Trusted publishing lists active providers that are not configured and stay unusable: {}",
unknown);
}
}

public boolean isEnabled() {
return config.isEnabled();
}
Expand Down
Loading