diff --git a/core/src/main/java/org/wildfly/channel/ChannelImpl.java b/core/src/main/java/org/wildfly/channel/ChannelImpl.java index ff31f863..51f13630 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelImpl.java +++ b/core/src/main/java/org/wildfly/channel/ChannelImpl.java @@ -21,10 +21,6 @@ import static org.wildfly.channel.version.VersionMatcher.COMPARATOR; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; @@ -38,9 +34,6 @@ import org.jboss.logging.Logger; import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ArtifactIdentifier; import org.wildfly.channel.version.VersionMatcher; /** @@ -49,7 +42,6 @@ class ChannelImpl implements AutoCloseable { private static final Logger LOG = Logger.getLogger(ChannelImpl.class); - protected static final String SIGNATURE_FILE_SUFFIX = ".asc"; private final Channel channelDefinition; private Channel resolvedChannel; @@ -64,11 +56,8 @@ class ChannelImpl implements AutoCloseable { private boolean dependency = false; public Optional blocklist = Optional.empty(); - private ChannelManifestCoordinate resolvedCoordinate; - private SignatureValidator signatureValidator; - public ChannelManifest getManifest() { return channelManifest; } @@ -82,18 +71,16 @@ public ChannelImpl(Channel channelDefinition) { * * @param factory * @param channels - * @param signatureValidator - the validator used to check the signatures of resolved artifacts * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels * @throws CyclicDependencyException - if the required manifests form a cyclic dependency */ - void init(MavenVersionsResolver.Factory factory, List channels, SignatureValidator signatureValidator) { + void init(MavenVersionsResolver.Factory factory, List channels) { if (resolver != null) { //already initialized return; } - this.signatureValidator = signatureValidator; - resolver = factory.create(channelDefinition.getRepositories()); + resolver = factory.create(channelDefinition); final Channel.Builder resolvedChannelBuilder = new Channel.Builder(channelDefinition); if (channelDefinition.getManifestCoordinate() != null) { @@ -117,7 +104,7 @@ void init(MavenVersionsResolver.Factory factory, List channels, Sig BlocklistCoordinate blocklistCoordinate = resolveBlocklistVersion(channelDefinition); if (blocklistCoordinate != null) { resolvedChannelBuilder.setBlocklistCoordinate(blocklistCoordinate); - final List urls = resolveChannelMetadata(List.of(blocklistCoordinate), true); + final List urls = resolver.resolveChannelMetadata(List.of(blocklistCoordinate)); this.blocklist = urls.stream() .map(Blocklist::from) .findFirst(); @@ -131,7 +118,7 @@ private ChannelImpl findRequiredChannel(MavenVersionsResolver.Factory factory, L ChannelImpl foundChannel = null; for (ChannelImpl c: channels) { if (c.getManifest() == null) { - c.init(factory, channels, signatureValidator); + c.init(factory, channels); } if (manifestRequirement.getId().equals(c.getManifest().getId())) { foundChannel = c; @@ -173,7 +160,7 @@ private ChannelImpl createNewChannelFromMaven(MavenVersionsResolver.Factory fact final ChannelImpl requiredChannel = new ChannelImpl(requiredChannelDefinition); try { - requiredChannel.init(factory, channels, signatureValidator); + requiredChannel.init(factory, channels); } catch (UnresolvedMavenArtifactException e) { throw new UnresolvedRequiredManifestException("Manifest with ID " + manifestRequirement.getId() + " is not available", manifestRequirement.getId(), e); } @@ -229,6 +216,10 @@ static class ResolveLatestVersionResult { } } + private Set attemptedRepositories() { + return new HashSet<>(channelDefinition.getRepositories()); + } + private ChannelManifestCoordinate resolveManifestVersion(Channel baseDefinition) { final ChannelManifestCoordinate manifestCoordinate = baseDefinition.getManifestCoordinate(); @@ -297,92 +288,12 @@ private ChannelManifest resolveManifest(ChannelManifestCoordinate manifestCoordi } else { resolvedCoordinate = manifestCoordinate; } - return resolveChannelMetadata(List.of(resolvedCoordinate), false) + return resolver.resolveChannelMetadata(List.of(resolvedCoordinate)) .stream() .map(ChannelManifestMapper::from) .findFirst().orElseThrow(); } - /** - * Resolve a list of channel metadata artifacts based on the coordinates. - * If the {@code ChannelMetadataCoordinate} contains non-null URL, that URL is returned. - * If the {@code ChannelMetadataCoordinate} contains non-null Maven coordinates, the Maven artifact will be resolved - * and a URL to it will be returned. - * If the Maven coordinates specify only groupId and artifactId, latest available version of matching Maven artifact - * will be resolved. - * - * The order of returned URLs is the same as order of coordinates. - * - * @param coords - list of ChannelMetadataCoordinate. - * @param optional - if artifact is optional, the method will return an empty collection if no versions are found - * - * @return a list of URLs to the metadata files - * - * @throws ArtifactTransferException if any artifacts can not be resolved. - */ - private List resolveChannelMetadata(List coords, boolean optional) throws ArtifactTransferException { - requireNonNull(coords); - - List channels = new ArrayList<>(); - - for (ChannelMetadataCoordinate coord : coords) { - if (coord.getUrl() != null) { - LOG.infof("Resolving channel metadata at %s", coord.getUrl()); - channels.add(coord.getUrl()); - if (channelDefinition.isGpgCheck()) { - try { - final URL signatureUrl; - if (coord.getSignatureUrl() == null) { - signatureUrl = new URL(coord.getUrl().toExternalForm() + SIGNATURE_FILE_SUFFIX); - } else { - signatureUrl = coord.getSignatureUrl(); - } - validateGpgSignature(coord.getUrl(), signatureUrl); - } catch (IOException e) { - throw new InvalidChannelMetadataException("Unable to download a detached signature file from: " + coord.getUrl().toExternalForm()+ SIGNATURE_FILE_SUFFIX, - List.of(e.getMessage()), e); - } - } - continue; - } - - String version = coord.getVersion(); - if (version == null) { - Set versions = resolver.getAllVersions(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier()); - Optional latestVersion = VersionMatcher.getLatestVersion(versions); - if (latestVersion.isPresent()){ - version = latestVersion.get(); - } else if (optional) { - return Collections.emptyList(); - } else { - throw new ArtifactTransferException(String.format("Unable to resolve the latest version of channel metadata %s:%s", coord.getGroupId(), coord.getArtifactId()), - singleton(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), "")), - attemptedRepositories()); - } - } - LOG.infof("Resolving channel metadata from Maven artifact %s:%s:%s", coord.getGroupId(), coord.getArtifactId(), version); - File channelArtifact = resolver.resolveArtifact(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), version); - try { - channels.add(channelArtifact.toURI().toURL()); - if (channelDefinition.isGpgCheck()) { - validateGpgSignature(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), version, channelArtifact); - } - } catch (MalformedURLException e) { - throw new ArtifactTransferException(String.format("Unable to resolve the latest version of channel metadata %s:%s", coord.getGroupId(), coord.getArtifactId()), e, - singleton(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), - coord.getExtension(), coord.getClassifier(), coord.getVersion())), - attemptedRepositories()); - } - } - return channels; - } - - private Set attemptedRepositories() { - return channelDefinition.getRepositories().stream() - .map(r -> new Repository(r.getId(), r.getUrl())) - .collect(Collectors.toSet()); - } - Optional resolveLatestVersion(String groupId, String artifactId, String extension, String classifier, String baseVersion) { requireNonNull(groupId); requireNonNull(artifactId); @@ -497,79 +408,11 @@ ResolveArtifactResult resolveArtifact(String groupId, String artifactId, String } } - final File artifact = resolver.resolveArtifact(groupId, artifactId, extension, classifier, version); - if (channelDefinition.isGpgCheck()) { - validateGpgSignature(groupId, artifactId, extension, classifier, version, artifact); - } - return new ResolveArtifactResult(artifact, this); - } - - private void validateGpgSignature(String groupId, String artifactId, String extension, String classifier, - String version, File artifact) { - final ArtifactIdentifier mavenArtifact = new ArtifactIdentifier.MavenResource(groupId, artifactId, extension, - classifier, version); - try { - final File signature = resolver.resolveArtifact(groupId, artifactId, extension + SIGNATURE_FILE_SUFFIX, - classifier, version); - final SignatureResult signatureResult = signatureValidator.validateSignature( - mavenArtifact, new FileInputStream(artifact), new FileInputStream(signature), - channelDefinition.getGpgUrls()); - if (signatureResult.getResult() != SignatureResult.Result.OK) { - throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); - } - } catch (ArtifactTransferException | FileNotFoundException e) { - throw new SignatureValidator.SignatureException("Unable to find required signature for " + mavenArtifact, - e, SignatureResult.noSignature(mavenArtifact)); - } - } - - private void validateGpgSignature(URL artifactFile, URL signature) throws IOException { - final SignatureResult signatureResult = signatureValidator.validateSignature( - new ArtifactIdentifier.UrlResource(artifactFile), - artifactFile.openStream(), signature.openStream(), - channelDefinition.getGpgUrls() - ); - - if (signatureResult.getResult() != SignatureResult.Result.OK) { - throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); - } + return new ResolveArtifactResult(resolver.resolveArtifact(groupId, artifactId, extension, classifier, version), this); } List resolveArtifacts(List coordinates) throws UnresolvedMavenArtifactException { final List resolvedArtifacts = resolver.resolveArtifacts(coordinates); - - if (channelDefinition.isGpgCheck()) { - try { - final List signatures = resolver.resolveArtifacts(coordinates.stream() - .map(c->new ArtifactCoordinate(c.getGroupId(), c.getArtifactId(), c.getExtension() + SIGNATURE_FILE_SUFFIX, - c.getClassifier(), c.getVersion())) - .collect(Collectors.toList())); - for (int i = 0; i < resolvedArtifacts.size(); i++) { - final File artifact = resolvedArtifacts.get(i); - final ArtifactCoordinate c = coordinates.get(i); - final ArtifactIdentifier.MavenResource mavenArtifact = new ArtifactIdentifier.MavenResource(c.getGroupId(), c.getArtifactId(), - c.getExtension(), c.getClassifier(), c.getVersion()); - final File signature = signatures.get(i); - try { - final SignatureResult signatureResult = signatureValidator.validateSignature(mavenArtifact, - new FileInputStream(artifact), new FileInputStream(signature), channelDefinition.getGpgUrls()); - if (signatureResult.getResult() != SignatureResult.Result.OK) { - throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); - } - } catch (FileNotFoundException e) { - throw new SignatureValidator.SignatureException(String.format("Unable to find required signature for %s:%s:%s", - mavenArtifact.getGroupId(), mavenArtifact.getArtifactId(), mavenArtifact.getVersion()), - SignatureResult.noSignature(mavenArtifact)); - } - } - } catch (ArtifactTransferException e) { - final ArtifactIdentifier.MavenResource artifact = new ArtifactIdentifier.MavenResource(e.getUnresolvedArtifacts().stream().findFirst().get()); - throw new SignatureValidator.SignatureException(String.format("Unable to find required signature for %s:%s:%s", - artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()), - SignatureResult.noSignature(artifact)); - } - } - return resolvedArtifacts.stream().map(f->new ResolveArtifactResult(f, this)).collect(Collectors.toList()); } diff --git a/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java b/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java index 3522cb2a..1f7aa143 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java +++ b/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java @@ -69,7 +69,7 @@ public static ChannelManifestCoordinate create(@JsonProperty(value = "url") Stri return new ChannelManifestCoordinate(gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); } } else { - return new ChannelManifestCoordinate(new URL(url)); + return new ChannelManifestCoordinate(new URL(url), signatureUrl == null ? null : new URL(signatureUrl)); } } diff --git a/core/src/main/java/org/wildfly/channel/ChannelSession.java b/core/src/main/java/org/wildfly/channel/ChannelSession.java index 5e37c659..d1507b73 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelSession.java +++ b/core/src/main/java/org/wildfly/channel/ChannelSession.java @@ -33,9 +33,9 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; +import org.apache.commons.lang3.tuple.Pair; import org.jboss.logging.Logger; import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureValidator; import org.wildfly.channel.version.VersionMatcher; /** @@ -48,12 +48,10 @@ public class ChannelSession implements AutoCloseable { private final List channels; private final ChannelRecorder recorder = new ChannelRecorder(); - // resolver used for direct dependencies only. Uses combination of all repositories in the channels. - private final MavenVersionsResolver combinedResolver; private final int versionResolutionParallelism; /** - * Create a ChannelSession with a default rejecting signature validator. + * Create a ChannelSession. * * @param channelDefinitions the list of channels to resolve Maven artifact * @param factory Factory to create {@code MavenVersionsResolver} that are performing the actual Maven resolution. @@ -61,20 +59,7 @@ public class ChannelSession implements AutoCloseable { * @throws CyclicDependencyException - if the required manifests form a cyclic dependency */ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Factory factory) { - this(channelDefinitions, factory, DEFAULT_SPLIT_ARTIFACT_PARALLELISM, SignatureValidator.REJECTING_VALIDATOR); - } - - /** - * Create a ChannelSession with a default rejecting signature validator. - * - * @param channelDefinitions the list of channels to resolve Maven artifact - * @param factory Factory to create {@code MavenVersionsResolver} that are performing the actual Maven resolution. - * @param signatureValidator Validator to verify signatures of downloaded artifacts - * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels - * @throws CyclicDependencyException - if the required manifests form a cyclic dependency - */ - public ChannelSession(List channelDefinitions, MavenVersionsResolver.Factory factory, SignatureValidator signatureValidator) { - this(channelDefinitions, factory, DEFAULT_SPLIT_ARTIFACT_PARALLELISM, signatureValidator); + this(channelDefinitions, factory, DEFAULT_SPLIT_ARTIFACT_PARALLELISM); } /** @@ -86,18 +71,13 @@ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Fa * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels * @throws CyclicDependencyException - if the required manifests form a cyclic dependency */ - public ChannelSession(List channelDefinitions, MavenVersionsResolver.Factory factory, - int versionResolutionParallelism, SignatureValidator signatureValidator) { + public ChannelSession(List channelDefinitions, MavenVersionsResolver.Factory factory, int versionResolutionParallelism) { requireNonNull(channelDefinitions); requireNonNull(factory); - requireNonNull(signatureValidator); - - final Set repositories = channelDefinitions.stream().flatMap(c -> c.getRepositories().stream()).collect(Collectors.toSet()); - this.combinedResolver = factory.create(repositories); List channelList = channelDefinitions.stream().map(ChannelImpl::new).collect(Collectors.toList()); for (ChannelImpl channel : channelList) { - channel.init(factory, channelList, signatureValidator); + channel.init(factory, channelList); } // filter out channels marked as dependency, so that resolution starts only at top level channels this.channels = channelList.stream().filter(c->!c.isDependency()).collect(Collectors.toList()); @@ -218,9 +198,37 @@ public MavenArtifact resolveDirectMavenArtifact(String groupId, String artifactI requireNonNull(artifactId); requireNonNull(version); - File file = combinedResolver.resolveArtifact(groupId, artifactId, extension, classifier, version); - recorder.recordStream(groupId, artifactId, version); - return new MavenArtifact(groupId, artifactId, extension, classifier, version, file); + /* + * when resolving a direct artifact we don't care if a channel manifest lists that artifact, we resolve it + * if it is present in underlying repositories. + * BUT if the channel requires a GPG check, the artifact has to still be verified. + * Therefore, we're trying to resolve artifact from each available channel and returning the first match. + */ + + File file = null; + UnresolvedMavenArtifactException ex = null; + for (ChannelImpl channel : channels) { + try { + file = channel.resolveArtifact(groupId, artifactId, extension, classifier, version).file; + break; + } catch (UnresolvedMavenArtifactException e) { + ex = e; + } + } + + if (file != null) { + recorder.recordStream(groupId, artifactId, version); + return new MavenArtifact(groupId, artifactId, extension, classifier, version, file); + } else if (ex != null) { + throw ex; + } else { + throw new ArtifactTransferException("Unable to resolve direct artifact.", + Set.of(new ArtifactCoordinate(groupId, artifactId, extension, classifier, version)), + channels.stream() + .map(ChannelImpl::getResolvedChannelDefinition) + .flatMap(cd->cd.getRepositories().stream()) + .collect(Collectors.toSet())); + } } /** @@ -238,16 +246,64 @@ public List resolveDirectMavenArtifacts(List requireNonNull(c.getArtifactId()); requireNonNull(c.getVersion()); }); - final List files = combinedResolver.resolveArtifacts(coordinates); - final ArrayList res = new ArrayList<>(); - for (int i = 0; i < coordinates.size(); i++) { - final ArtifactCoordinate request = coordinates.get(i); - final MavenArtifact resolvedArtifact = new MavenArtifact(request.getGroupId(), request.getArtifactId(), request.getExtension(), request.getClassifier(), request.getVersion(), files.get(i)); + /* + * When resolving a "direct" artifact, we don't care if the artifact is listed in the channel's manifest, + * only if the underlying repositories contain that artifact. + * BUT, we still need to verify the artifact signature if the channel requires it. To achieve that, we're + * going to query each channel in turn, taking the artifacts it was able to resolve and keeping the rest + * to be resolved by remaining channels. At the end we should be left with no un-resolved artifacts, or have + * a list of artifacts not available in any channels. + * NOTE: if the same artifact is available in both GPG-enabled and GPG-disabled channel there is no guarantee + * which channel will be queried first. + */ + + // list of artifacts that are being resolved in this step + List currentQuery = new ArrayList<>(coordinates); + // we need to preserve the ordering of artifacts, but that can be affected by the order of query/resolution between channels + final HashMap> resolvedArtifacts = new HashMap<>(); + for (ChannelImpl channel : channels) { + if (currentQuery.isEmpty()) { + break; + } - recorder.recordStream(resolvedArtifact.getGroupId(), resolvedArtifact.getArtifactId(), resolvedArtifact.getVersion()); - res.add(resolvedArtifact); + try { + final List resolved = channel.resolveArtifacts(currentQuery); + // keep a map of AC -> File + for (int i = 0; i < currentQuery.size(); i++) { + resolvedArtifacts.put(currentQuery.get(i), Pair.of(resolved.get(i).file, channel.getResolvedChannelDefinition().getName())); + } + // all the artifacts were resolved by this point, lets remove all artifacts from the current query + currentQuery = Collections.emptyList(); + } catch (UnresolvedMavenArtifactException e) { + // at the end need to map them into a correct order + final Set unresolvedArtifacts = e.getUnresolvedArtifacts(); + // coordinates - unresolved = it should be possible to resolve those artifacts from this channel + // we need to call resolve again, because the first call threw an exception + currentQuery.removeAll(unresolvedArtifacts); + final List resolved = channel.resolveArtifacts(currentQuery); + for (int i = 0; i < currentQuery.size(); i++) { + resolvedArtifacts.put(currentQuery.get(i), Pair.of(resolved.get(i).file, channel.getResolvedChannelDefinition().getName())); + } + // unresolved - try with another channel, rinse and repeat until run out of channels or resolve all artifacts + currentQuery = new ArrayList<>(unresolvedArtifacts); + } } + if (!currentQuery.isEmpty()) { + throw new ArtifactTransferException("Unable to resolve some direct artifacts", new HashSet<>(currentQuery), + channels.stream() + .map(ChannelImpl::getResolvedChannelDefinition) + .flatMap(cd->cd.getRepositories().stream()) + .collect(Collectors.toSet())); + } + + // finally, build a list of resolved files in a correct order to return and record the streams + final List res = coordinates.stream().map(ac -> new MavenArtifact(ac.getGroupId(), ac.getArtifactId(), ac.getExtension(), + ac.getClassifier(), ac.getVersion(), resolvedArtifacts.get(ac).getLeft(), resolvedArtifacts.get(ac).getRight())).collect(Collectors.toList()); + + res.forEach(resolvedArtifact-> + recorder.recordStream(resolvedArtifact.getGroupId(), resolvedArtifact.getArtifactId(), resolvedArtifact.getVersion()) + ); return res; } @@ -273,7 +329,6 @@ public void close() { for (ChannelImpl channel : channels) { channel.close(); } - combinedResolver.close(); } /** diff --git a/core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java b/core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java new file mode 100644 index 00000000..aa5d060c --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java @@ -0,0 +1,222 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.wildfly.channel.spi.ArtifactIdentifier; +import org.wildfly.channel.spi.MavenVersionsResolver; +import org.wildfly.channel.spi.SignatureResult; +import org.wildfly.channel.spi.SignatureValidator; +import org.wildfly.channel.version.VersionMatcher; + +/** + * Resolve and validate a signature using the wrapped {@code MavenVersionsResolver}. + */ +public class SignedVersionResolverWrapper implements MavenVersionsResolver { + + protected static final String SIGNATURE_FILE_SUFFIX = ".asc"; + private final MavenVersionsResolver wrapped; + private final SignatureValidator signatureValidator; + private final List gpgUrls; + private final Collection repositories; + + public SignedVersionResolverWrapper(MavenVersionsResolver wrapped, Collection repositories, + SignatureValidator signatureValidator, List gpgUrls) { + this.wrapped = wrapped; + this.repositories = repositories; + this.signatureValidator = signatureValidator; + this.gpgUrls = gpgUrls; + } + + private void validateGpgSignature(String groupId, String artifactId, String extension, String classifier, + String version, File artifact) { + final ArtifactIdentifier mavenArtifact = new ArtifactIdentifier.MavenResource(groupId, artifactId, extension, + classifier, version); + try { + final File signature = wrapped.resolveArtifact(groupId, artifactId, extension + SIGNATURE_FILE_SUFFIX, + classifier, version); + final SignatureResult signatureResult = signatureValidator.validateSignature( + mavenArtifact, new FileInputStream(artifact), new FileInputStream(signature), + gpgUrls); + if (signatureResult.getResult() != SignatureResult.Result.OK) { + throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); + } + } catch (ArtifactTransferException | FileNotFoundException e) { + throw new SignatureValidator.SignatureException("Unable to find required signature for " + mavenArtifact, + e, SignatureResult.noSignature(mavenArtifact)); + } + } + + private void validateGpgSignature(URL artifactFile, URL signature) throws IOException { + final SignatureResult signatureResult = signatureValidator.validateSignature( + new ArtifactIdentifier.UrlResource(artifactFile), + artifactFile.openStream(), signature.openStream(), + gpgUrls + ); + + if (signatureResult.getResult() != SignatureResult.Result.OK) { + throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); + } + } + + @Override + public Set getAllVersions(String groupId, String artifactId, String extension, String classifier) { + return wrapped.getAllVersions(groupId, artifactId, extension, classifier); + } + + @Override + public File resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws ArtifactTransferException { + final File artifact = wrapped.resolveArtifact(groupId, artifactId, extension, classifier, version); + + validateGpgSignature(groupId, artifactId, extension, classifier, version, artifact); + + return artifact; + } + + @Override + public List resolveArtifacts(List coordinates) throws ArtifactTransferException { + final List resolvedArtifacts = wrapped.resolveArtifacts(coordinates); + + try { + final List signatures = wrapped.resolveArtifacts(coordinates.stream() + .map(c->new ArtifactCoordinate(c.getGroupId(), c.getArtifactId(), c.getExtension() + SIGNATURE_FILE_SUFFIX, + c.getClassifier(), c.getVersion())) + .collect(Collectors.toList())); + for (int i = 0; i < resolvedArtifacts.size(); i++) { + final File artifact = resolvedArtifacts.get(i); + final ArtifactCoordinate c = coordinates.get(i); + final ArtifactIdentifier.MavenResource mavenArtifact = new ArtifactIdentifier.MavenResource(c.getGroupId(), c.getArtifactId(), + c.getExtension(), c.getClassifier(), c.getVersion()); + final File signature = signatures.get(i); + try { + final SignatureResult signatureResult = signatureValidator.validateSignature(mavenArtifact, + new FileInputStream(artifact), new FileInputStream(signature), gpgUrls); + if (signatureResult.getResult() != SignatureResult.Result.OK) { + throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); + } + } catch (FileNotFoundException e) { + throw new SignatureValidator.SignatureException(String.format("Unable to find required signature for %s:%s:%s", + mavenArtifact.getGroupId(), mavenArtifact.getArtifactId(), mavenArtifact.getVersion()), + SignatureResult.noSignature(mavenArtifact)); + } + } + } catch (ArtifactTransferException e) { + final ArtifactIdentifier.MavenResource artifact = new ArtifactIdentifier.MavenResource(e.getUnresolvedArtifacts().stream().findFirst().get()); + throw new SignatureValidator.SignatureException(String.format("Unable to find required signature for %s:%s:%s", + artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()), + SignatureResult.noSignature(artifact)); + } + + + return resolvedArtifacts; + } + + @Override + public List resolveChannelMetadata(List coords) throws ArtifactTransferException { + requireNonNull(coords); + + final List resolvedMetadata = wrapped.resolveChannelMetadata(coords); + + List signatures = new ArrayList<>(); + + for (ChannelMetadataCoordinate coord : coords) { + if (coord.getUrl() != null) { + try { + final URL signatureUrl; + if (coord.getSignatureUrl() == null) { + signatureUrl = new URL(coord.getUrl().toExternalForm() + SIGNATURE_FILE_SUFFIX); + } else { + signatureUrl = coord.getSignatureUrl(); + } + signatures.add(signatureUrl); + } catch (IOException e) { + throw new InvalidChannelMetadataException("Unable to download a detached signature file from: " + coord.getUrl().toExternalForm()+ SIGNATURE_FILE_SUFFIX, + List.of(e.getMessage()), e); + } + continue; + } + + String version = coord.getVersion(); + if (version == null) { + Set versions = wrapped.getAllVersions(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier()); + Optional latestVersion = VersionMatcher.getLatestVersion(versions); + if (latestVersion.isPresent()){ + version = latestVersion.get(); + } else { + throw new ArtifactTransferException(String.format("Unable to resolve the latest version of channel metadata signature %s:%s", coord.getGroupId(), coord.getArtifactId()), + singleton(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), "")), + attemptedRepositories()); + } + } + + try { + File channelArtifact = wrapped.resolveArtifact(coord.getGroupId(), coord.getArtifactId(), + coord.getExtension() + SIGNATURE_FILE_SUFFIX, coord.getClassifier(), version); + signatures.add(channelArtifact.toURI().toURL()); + } catch (ArtifactTransferException e) { + throw new SignatureValidator.SignatureException("Unable to find required signature for " + coord, + e, SignatureResult.noSignature(new ArtifactIdentifier.MavenResource(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), version))); + } catch (MalformedURLException e) { + throw new ArtifactTransferException(String.format("Unable to resolve the latest version of channel metadata signature %s:%s", coord.getGroupId(), coord.getArtifactId()), e, + singleton(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), + coord.getExtension(), coord.getClassifier(), coord.getVersion())), + attemptedRepositories()); + } + } + + try { + for (int i = 0; i < resolvedMetadata.size(); i++) { + validateGpgSignature(resolvedMetadata.get(i), signatures.get(i)); + } + } catch (IOException e) { + throw new InvalidChannelMetadataException("Unable to read a detached signature file from: " + signatures, + List.of(e.getMessage()), e); + } + return resolvedMetadata; + } + + private Set attemptedRepositories() { + return repositories.stream() + .map(r -> new Repository(r.getId(), r.getUrl())) + .collect(Collectors.toSet()); + } + + @Override + public String getMetadataReleaseVersion(String groupId, String artifactId) { + return wrapped.getMetadataReleaseVersion(groupId, artifactId); + } + + @Override + public String getMetadataLatestVersion(String groupId, String artifactId) { + return wrapped.getMetadataLatestVersion(groupId, artifactId); + } +} diff --git a/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java b/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java index 59c7685f..ecc04c85 100644 --- a/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java +++ b/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java @@ -19,14 +19,13 @@ import java.io.Closeable; import java.io.File; import java.net.URL; -import java.util.Collection; import java.util.List; import java.util.Set; import org.wildfly.channel.ArtifactCoordinate; import org.wildfly.channel.ArtifactTransferException; +import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelMetadataCoordinate; -import org.wildfly.channel.Repository; import org.wildfly.channel.UnresolvedMavenArtifactException; /** @@ -60,8 +59,7 @@ public interface MavenVersionsResolver extends Closeable { * * @throws ArtifactTransferException if the artifact can not be resolved. */ - File resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) - throws ArtifactTransferException; + File resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws ArtifactTransferException; /** * Resolve a list of maven artifacts based on the full coordinates. @@ -92,7 +90,6 @@ File resolveArtifact(String groupId, String artifactId, String extension, String * * @throws ArtifactTransferException if any artifacts can not be resolved. */ - @Deprecated List resolveChannelMetadata(List manifestCoords) throws ArtifactTransferException; /** @@ -128,11 +125,11 @@ default void close() { * * A client of this library is responsible to provide an implementation of the {@link Factory} interface. * - * The {@link #create(Collection)}} method will be called once for every channel. + * The {@link #create(Channel)}} method will be called once for every channel. */ interface Factory extends Closeable { - MavenVersionsResolver create(Collection repositories); + MavenVersionsResolver create(Channel channel); default void close() { } diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java index 8737c1e0..3f1a8d9a 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java @@ -21,12 +21,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ArtifactIdentifier; import java.io.File; import java.io.IOException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -41,7 +39,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.wildfly.channel.ChannelImpl.SIGNATURE_FILE_SUFFIX; public class ChannelSessionInitTestCase { @TempDir @@ -126,8 +123,7 @@ public void throwExceptionRequiredChannelIdNotAvailableAndNotAbleToResolve() thr .build(); mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); - when(resolver.resolveArtifact("test.channels", "i-dont-exist", ChannelManifest.EXTENSION, - ChannelManifest.CLASSIFIER, "1.0.0")) + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test.channels", "i-dont-exist", "1.0.0")))) .thenThrow(ArtifactTransferException.class); List channels = List.of(new Channel.Builder() @@ -405,10 +401,6 @@ public void getVersionOfResolvedBlocklist() throws Exception { mockManifest(resolver, requiredManifest, "test.channels:base-manifest:1.0.0"); when(resolver.getAllVersions("test.channels", "blocklist", BlocklistCoordinate.EXTENSION, BlocklistCoordinate.CLASSIFIER)) .thenReturn(Set.of("1.0.0")); - final File testFile = tempDir.resolve("test.yaml").toFile(); - Files.writeString(testFile.toPath(), "schemaVersion: 1.0.0"); - when(resolver.resolveArtifact("test.channels", "blocklist", BlocklistCoordinate.EXTENSION, BlocklistCoordinate.CLASSIFIER, "1.0.0")) - .thenReturn(testFile); final List channels = List.of(new Channel.Builder() .setName("channel one") @@ -426,102 +418,21 @@ public void getVersionOfResolvedBlocklist() throws Exception { } } - @Test - public void mavenManifestWithoutSignatureCausesError() throws Exception { - MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); - MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - when(factory.create(any())).thenReturn(resolver); - - final ChannelManifest baseManifest = new ManifestBuilder() - .setId("manifest-one") - .build(); - mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); - - // two channels providing base- and required- manifests - List channels = List.of(new Channel.Builder() - .setName("channel one") - .addRepository("test", "test") - .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") - .setGpgCheck(true) - .build() - ); - - when(resolver.resolveArtifact("test.channels", "base-manifest", - ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenThrow(ArtifactTransferException.class); - assertThrows(SignatureValidator.SignatureException.class, () -> new ChannelSession(channels, factory)); - } - - @Test - public void urlManifestWithoutSignatureCausesError() throws Exception { - MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); - MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - when(factory.create(any())).thenReturn(resolver); - - final ChannelManifest baseManifest = new ManifestBuilder() - .setId("manifest-one") - .build(); - final Path manifestFile = tempDir.resolve("test-manifest.yaml"); - Files.writeString(manifestFile, ChannelManifestMapper.toYaml(baseManifest)); - - // two channels providing base- and required- manifests - List channels = List.of(new Channel.Builder() - .setName("channel one") - .addRepository("test", "test") - .setManifestCoordinate(new ChannelManifestCoordinate(manifestFile.toUri().toURL())) - .setGpgCheck(true) - .build() - ); - - when(resolver.resolveArtifact("test.channels", "base-manifest", - ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenThrow(ArtifactTransferException.class); - assertThrows(InvalidChannelMetadataException.class, () -> new ChannelSession(channels, factory)); - } - - @Test - public void invalidSignatureCausesError() throws Exception { - MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); - MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - final SignatureValidator signatureValidator = mock(SignatureValidator.class); - when(factory.create(any())).thenReturn(resolver); - - final ChannelManifest baseManifest = new ManifestBuilder() - .setId("manifest-one") - .build(); - mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); - - // two channels providing base- and required- manifests - List channels = List.of(new Channel.Builder() - .setName("channel one") - .addRepository("test", "test") - .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") - .setGpgCheck(true) - .build() - ); - - when(resolver.resolveArtifact("test.channels", "base-manifest", - ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenReturn(tempDir.resolve("test-manifest.yaml.asc").toFile()); - when(signatureValidator.validateSignature(any(), any(), any(), any())).thenReturn(SignatureResult.invalid(mock(ArtifactIdentifier.class))); - assertThrows(SignatureValidator.SignatureException.class, () -> new ChannelSession(channels, factory)); - } - private void mockManifest(MavenVersionsResolver resolver, ChannelManifest manifest, String gav) throws IOException { mockManifest(resolver, ChannelManifestMapper.toYaml(manifest), gav); } - private void mockManifest(MavenVersionsResolver resolver, String manifest, String gavString) throws IOException { + private void mockManifest(MavenVersionsResolver resolver, String manifest, String gav) throws IOException { Path manifestFile = tempDir.resolve("manifest_" + RandomUtils.nextInt() + ".yaml"); Files.writeString(manifestFile, manifest); + mockManifest(resolver, manifestFile.toUri().toURL(), gav); + } + + private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws IOException { final String[] splitGav = gavString.split(":"); final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); - - when(resolver.getAllVersions(gav.getGroupId(), gav.getArtifactId(), ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER)) - .thenReturn(Set.of(splitGav.length == 3 ? gav.getVersion() : "1.0.0")); - when(resolver.resolveArtifact(gav.getGroupId(), gav.getArtifactId(), ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, - splitGav.length == 3 ? gav.getVersion() : "1.0.0")) - .thenReturn(manifestFile.toFile()); + when(resolver.resolveChannelMetadata(eq(List.of(ChannelManifestCoordinate.create(null, gav))))) + .thenReturn(List.of(manifestUrl)); } } diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java index ee45f64d..3ab970d2 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java @@ -48,6 +48,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.wildfly.channel.spi.MavenVersionsResolver; @@ -79,7 +81,7 @@ public void testFindLatestMavenArtifactVersion() throws Exception { assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } public static List mockChannel(MavenVersionsResolver resolver, Path tempDir, String... manifests) throws IOException { @@ -97,8 +99,8 @@ public static List mockChannel(MavenVersionsResolver resolver, Path tem String manifest = manifests[i]; Path manifestFile = Files.writeString(tempDir.resolve("manifest" + i +".yaml"), manifest); - when(resolver.resolveArtifact("org.channels", "channel" + i, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenReturn(manifestFile.toFile()); + when(resolver.resolveChannelMetadata(eq(List.of(new ChannelManifestCoordinate("org.channels", "channel" + i, "1.0.0"))))) + .thenReturn(List.of(manifestFile.toUri().toURL())); } return channels; } @@ -128,7 +130,7 @@ public void testFindLatestMavenArtifactVersionThrowsUnresolvedMavenArtifactExcep } } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -163,7 +165,7 @@ public void testResolveLatestMavenArtifact() throws Exception { assertEquals("channel-0", artifact.getChannelName().get()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -192,7 +194,7 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( } } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -231,7 +233,7 @@ public void testResolveDirectMavenArtifact() throws Exception { assertEquals(Optional.empty(), artifact.getChannelName(), "The channel name should be null when resolving version directly"); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -280,7 +282,7 @@ public void testResolveMavenArtifactsFromOneChannel() throws Exception { assertEquals("25.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -342,7 +344,7 @@ public List answer(InvocationOnMock invocationOnMock) throws Throwable { assertEquals("25.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(3)).close(); + verify(resolver, times(2)).close(); } @Test @@ -373,8 +375,8 @@ public void testResolveDirectMavenArtifacts() throws Exception { assertNotNull(resolved); final List expected = asList( - new MavenArtifact("org.foo", "foo", null, null, "25.0.0.Final", resolvedArtifactFile1), - new MavenArtifact("org.bar", "bar", null, null, "26.0.0.Final", resolvedArtifactFile2) + new MavenArtifact("org.foo", "foo", null, null, "25.0.0.Final", resolvedArtifactFile1, "channel-0"), + new MavenArtifact("org.bar", "bar", null, null, "26.0.0.Final", resolvedArtifactFile2, "channel-0") ); assertContainsAll(expected, resolved); @@ -386,7 +388,100 @@ public void testResolveDirectMavenArtifacts() throws Exception { assertEquals("25.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); + } + + @Test + public void testResolveDirectMavenArtifactsFromTwoChannels() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.not\n" + + " artifactId: used\n" + + " version: \"1.0.0.Final\""; + + /* + * create two resolvers. The first one will be used by the first channel, the other by the second channel + * Each resolver is only able to resolve one artifact and throws error when searching for both artifacts + */ + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver1 = mock(MavenVersionsResolver.class); + MavenVersionsResolver resolver2 = mock(MavenVersionsResolver.class); + File resolvedArtifactFile1 = mock(File.class); + File resolvedArtifactFile2 = mock(File.class); + + when(factory.create(any())).thenAnswer(inv->{ + final Channel channel = inv.getArgument(0); + if (channel.getName().equals("channel-0")) { + return resolver1; + } else if (channel.getName().equals("channel-1")) { + return resolver2; + } else { + throw new RuntimeException("Unexpected channel " + channel.getName()); + } + }); + + final ArtifactCoordinate fooArtifact = new ArtifactCoordinate("org.foo", "foo", null, null, "25.0.0.Final"); + final ArtifactCoordinate barArtifact = new ArtifactCoordinate("org.bar", "bar", null, null, "26.0.0.Final"); + final List coordinates = asList( + fooArtifact, + barArtifact); + when(resolver1.resolveArtifacts(any())).thenAnswer(inv -> { + final List coords = inv.getArgument(0); + if (coords.size() == 2) { + throw new ArtifactTransferException("", + Set.of(barArtifact), + Set.of(new Repository("test", "http://test.te")) + ); + } else if (coords.get(0).equals(fooArtifact)) { + return List.of(resolvedArtifactFile1); + } else { + throw new RuntimeException("Unexpected query " + coords); + } + }); + when(resolver2.resolveArtifacts(any())).thenAnswer(inv -> { + final List coords = inv.getArgument(0); + if (coords.size() == 2) { + throw new ArtifactTransferException("", + Set.of(fooArtifact), + Set.of(new Repository("test", "http://test.te")) + ); + } else if (coords.get(0).equals(barArtifact)) { + return List.of(resolvedArtifactFile2); + } else { + throw new RuntimeException("Unexpected query " + coords); + } + }); + + /* + * create channel session with two channels. The manifests don't matter, but set different names + */ + final List channels = mockChannel(resolver1, tempDir, manifest); + channels.add(new Channel.Builder(mockChannel(resolver2, tempDir, manifest).get(0)) + .setName("channel-1").build()); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + + List resolved = session.resolveDirectMavenArtifacts(coordinates); + assertNotNull(resolved); + + final List expected = asList( + new MavenArtifact(fooArtifact.getGroupId(), fooArtifact.getArtifactId(), fooArtifact.getExtension(), + fooArtifact.getClassifier(), fooArtifact.getVersion(), resolvedArtifactFile1, "channel-0"), + new MavenArtifact(barArtifact.getGroupId(), barArtifact.getArtifactId(), barArtifact.getExtension(), + barArtifact.getClassifier(), barArtifact.getVersion(), resolvedArtifactFile2, "channel-1") + ); + assertContainsAll(expected, resolved); + + Optional stream = session.getRecordedChannel().findStreamFor("org.bar", "bar"); + assertTrue(stream.isPresent()); + assertEquals("26.0.0.Final", stream.get().getVersion()); + stream = session.getRecordedChannel().findStreamFor("org.foo", "foo"); + assertTrue(stream.isPresent()); + assertEquals("25.0.0.Final", stream.get().getVersion()); + } + + verify(resolver1, times(1)).close(); + verify(resolver2, times(1)).close(); } @Test diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java deleted file mode 100644 index 1a47b080..00000000 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.wildfly.channel; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.wildfly.channel.ChannelImpl.SIGNATURE_FILE_SUFFIX; -import static org.wildfly.channel.ChannelManifestMapper.CURRENT_SCHEMA_VERSION; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ArtifactIdentifier; - -public class ChannelSessionWithSignatureValidationTestCase { - - private static final ArtifactIdentifier.MavenResource ARTIFACT = new ArtifactIdentifier.MavenResource( - "org.wildfly", "wildfly-ee-galleon-pack", "zip", null, "25.0.1.Final"); - - private static final ArtifactIdentifier.MavenResource MANIFEST = new ArtifactIdentifier.MavenResource( - "org.channels", "test-manifest", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.0.0"); - - @TempDir - private Path tempDir; - private MavenVersionsResolver resolver; - private SignatureValidator signatureValidator; - private MavenVersionsResolver.Factory factory; - private File resolvedArtifactFile; - private List channels; - private File signatureFile; - - @BeforeEach - public void setUp() throws Exception { - factory = mock(MavenVersionsResolver.Factory.class); - resolver = mock(MavenVersionsResolver.class); - signatureValidator = mock(SignatureValidator.class); - when(factory.create(any())).thenReturn(resolver); - - // create a manfiest with a versionPattern to test signature od the latest resolved version is downloaded - final String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "streams:\n" + - " - groupId: org.wildfly\n" + - " artifactId: '*'\n" + - " versionPattern: '25\\.\\d+\\.\\d+.Final'"; - // create a channel requiring a gpg check - channels = List.of(new Channel.Builder() - .setName("channel-0") - .setGpgCheck(true) - .setManifestCoordinate(MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.version) - .build()); - - // the resolved files need to exist otherwise we can't create streams from them - resolvedArtifactFile = tempDir.resolve("test-artifact").toFile(); - Files.createFile(resolvedArtifactFile.toPath()); - signatureFile = tempDir.resolve("test-signature.asc").toFile(); - Files.createFile(signatureFile.toPath()); - - - when(resolver.getAllVersions(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier)) - .thenReturn(new HashSet<>(Arrays.asList("25.0.0.Final", ARTIFACT.version))); - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)) - .thenReturn(resolvedArtifactFile); - - - Path manifestFile = Files.writeString(tempDir.resolve("manifest.yaml"), manifest); - when(resolver.resolveArtifact(MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.extension, MANIFEST.classifier, MANIFEST.version)) - .thenReturn(manifestFile.toFile()); - when(resolver.resolveArtifact(MANIFEST.groupId, MANIFEST.artifactId, - MANIFEST.extension + SIGNATURE_FILE_SUFFIX, MANIFEST.classifier, MANIFEST.version)) - .thenReturn(signatureFile); - } - - @Test - public void artifactWithCorrectSignatureIsValidated() throws Exception { - // return signature when resolving it from Maven repository - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, - ARTIFACT.classifier, ARTIFACT.version)) - .thenReturn(signatureFile); - // accept all the validation requests - when(signatureValidator.validateSignature(any(), - any(), any(), any())).thenReturn(SignatureResult.ok()); - - - try (ChannelSession session = new ChannelSession(channels, factory, signatureValidator)) { - MavenArtifact artifact = session.resolveMavenArtifact( - ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, null); - assertNotNull(artifact); - - assertEquals(ARTIFACT.groupId, artifact.getGroupId()); - assertEquals(ARTIFACT.artifactId, artifact.getArtifactId()); - assertEquals(ARTIFACT.extension, artifact.getExtension()); - assertNull(artifact.getClassifier()); - assertEquals(ARTIFACT.version, artifact.getVersion()); - assertEquals(resolvedArtifactFile, artifact.getFile()); - assertEquals("channel-0", artifact.getChannelName().get()); - } - - // validateSignature should have been called for the manifest and the artifact - verify(signatureValidator, times(2)).validateSignature(any(), any(), any(), any()); - } - - @Test - public void artifactWithoutSignatureIsRejected() throws Exception { - // simulate situation where the signature file does not exist in the repository - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, - ARTIFACT.classifier, ARTIFACT.version)) - .thenThrow(ArtifactTransferException.class); - // accept all the validation requests - when(signatureValidator.validateSignature(any(), - any(), any(), any())).thenReturn(SignatureResult.ok()); - - try (ChannelSession session = new ChannelSession(channels, factory, signatureValidator)) { - assertThrows(SignatureValidator.SignatureException.class, () -> session.resolveMavenArtifact( - ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, null)); - } - - // validateSignature should have been called for the manifest only - verify(signatureValidator, times(1)).validateSignature(any(), any(), any(), any()); - } - - @Test - public void failedSignatureValidationThrowsException() throws Exception { - // return signature when resolving it from Maven repository - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, - ARTIFACT.classifier, ARTIFACT.version)) - .thenReturn(signatureFile); - // simulate a valid signature of the channel manifest, and invalid signature of the artifact - when(signatureValidator.validateSignature(eq(new ArtifactIdentifier.MavenResource( - MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.extension, MANIFEST.classifier, MANIFEST.version)), - any(), any(), any())).thenReturn(SignatureResult.ok()); - when(signatureValidator.validateSignature(eq(ARTIFACT), - any(), any(), any())).thenReturn(SignatureResult.invalid(ARTIFACT)); - - - try (ChannelSession session = new ChannelSession(channels, factory, signatureValidator)) { - assertThrows(SignatureValidator.SignatureException.class, () -> session.resolveMavenArtifact("org.wildfly", - "wildfly-ee-galleon-pack", "zip", null, "25.0.0.Final")); - } - - // validateSignature should have been called for the manifest and the artifact - verify(signatureValidator, times(2)).validateSignature(any(), any(), any(), any()); - } -} diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java index 6e0fc770..edd915b6 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java @@ -17,7 +17,6 @@ package org.wildfly.channel; import java.io.File; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -81,8 +80,10 @@ public void testFindLatestMavenArtifactVersion() throws Exception { " - groupId: org.wildfly\n" + " artifactId: wildfly-ee-galleon-pack\n" + " versionPattern: .*"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + mockBlocklistResolution(resolver, "channels/test-blocklist.yaml"); when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) @@ -93,7 +94,7 @@ public void testFindLatestMavenArtifactVersion() throws Exception { assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -124,10 +125,13 @@ public void testFindLatestMavenArtifactVersionBlocklistDoesntExist() throws Exce " - groupId: org.wildfly\n" + " artifactId: '*'\n" + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(Collections.emptyList()); when(factory.create(any())).thenReturn(resolver); - // return empty version list when blocklist is queried when(resolver.getAllVersions("org.wildfly", "wildfly-blocklist", BlocklistCoordinate.EXTENSION, BlocklistCoordinate.CLASSIFIER)).thenReturn(Collections.emptySet()); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) @@ -138,7 +142,7 @@ public void testFindLatestMavenArtifactVersionBlocklistDoesntExist() throws Exce assertEquals("25.0.1.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -169,8 +173,10 @@ public void testFindLatestMavenArtifactVersionWithWildcardBlocklist() throws Exc " - groupId: org.wildfly\n" + " artifactId: '*'\n" + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); - mockBlocklist(resolver, "channels/test-blocklist-with-wildcards.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + mockBlocklistResolution(resolver, "channels/test-blocklist-with-wildcards.yaml"); when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) @@ -181,7 +187,7 @@ public void testFindLatestMavenArtifactVersionWithWildcardBlocklist() throws Exc assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -212,9 +218,10 @@ public void testFindLatestMavenArtifactVersionBlocklistsAllVersionsException() t " - groupId: org.wildfly\n" + " artifactId: '*'\n" + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + mockBlocklistResolution(resolver, "channels/test-blocklist.yaml"); when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(new HashSet<>(singleton("25.0.1.Final"))); @@ -228,7 +235,7 @@ public void testFindLatestMavenArtifactVersionBlocklistsAllVersionsException() t } } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -259,9 +266,10 @@ public void testResolveLatestMavenArtifact() throws Exception { " - groupId: org.wildfly\n" + " artifactId: '*'\n" + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + mockBlocklistResolution(resolver, "channels/test-blocklist.yaml"); File resolvedArtifactFile = mock(File.class); @@ -282,7 +290,7 @@ public void testResolveLatestMavenArtifact() throws Exception { assertEquals(resolvedArtifactFile, artifact.getFile()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -313,8 +321,10 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( " - groupId: org.wildfly\n" + " artifactId: '*'\n" + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + mockBlocklistResolution(resolver, "channels/test-blocklist.yaml"); when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(new HashSet<>(Set.of("25.0.1.Final","26.0.0.Final"))); @@ -328,28 +338,14 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( } } - verify(resolver, times(2)).close(); - } - - private void mockBlocklist(MavenVersionsResolver resolver, String blocklistFileLocation, - String groupId, String artifactId, String version) throws URISyntaxException { - - if (version == null) { - when(resolver.getAllVersions(groupId, artifactId, BlocklistCoordinate.EXTENSION, - BlocklistCoordinate.CLASSIFIER)) - .thenReturn(Set.of("1.0.0")); - version = "1.0.0"; - } - when(resolver.resolveArtifact(groupId, artifactId, BlocklistCoordinate.EXTENSION, - BlocklistCoordinate.CLASSIFIER, version)) - .thenReturn(new File(this.getClass().getClassLoader().getResource(blocklistFileLocation).toURI())); + verify(resolver, times(1)).close(); } - private void mockManifest(MavenVersionsResolver resolver, String groupId, String artifactId, String manifestFileName) { - when(resolver.getAllVersions(groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER)) + private void mockBlocklistResolution(MavenVersionsResolver resolver, String fileName) { + when(resolver.getAllVersions("org.wildfly", "wildfly-blocklist", BlocklistCoordinate.EXTENSION, BlocklistCoordinate.CLASSIFIER)) .thenReturn(Set.of("1.0.0")); - when(resolver.resolveArtifact(groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenReturn(tempDir.resolve(manifestFileName).toFile()); + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist", "1.0.0")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource(fileName))); } @Test @@ -383,9 +379,10 @@ public void testResolveMavenArtifactsFromOneChannel() throws Exception { " - groupId: org.wildfly\n" + " artifactId: wildfly-cli\n" + " version: \"26.0.0.Final\""); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + mockBlocklistResolution(resolver, "channels/test-blocklist.yaml"); File resolvedArtifactFile1 = mock(File.class); File resolvedArtifactFile2 = mock(File.class); @@ -420,7 +417,7 @@ public void testResolveMavenArtifactsFromOneChannel() throws Exception { assertEquals("26.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -447,8 +444,8 @@ public void testFindLatestMavenArtifactVersionInRequiredChannel() throws Excepti " - groupId: org.wildfly\n" + " artifactId: wildfly-ee-galleon-pack\n" + " versionPattern: \".*\""); - mockManifest(resolver, "org.test", "required-manifest", - "required-manifest.yaml"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("org.test", "required-manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("required-manifest.yaml").toUri().toURL())); Files.writeString(tempDir.resolve("manifest.yaml"), "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + @@ -457,8 +454,11 @@ public void testFindLatestMavenArtifactVersionInRequiredChannel() throws Excepti " - groupId: org.wildfly\n" + " artifactId: wildfly-ee-galleon-pack\n" + " versionPattern: \".*\""); - mockManifest(resolver, "org.test", "base-manifest", "manifest.yaml"); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", "1.2.3"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("org.test", "base-manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist", "1.2.3")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) @@ -469,7 +469,7 @@ public void testFindLatestMavenArtifactVersionInRequiredChannel() throws Excepti assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(3)).close(); + verify(resolver, times(2)).close(); } @Test @@ -500,8 +500,10 @@ public void testChannelWithInvalidBlacklist() throws Exception { " - groupId: org.wildfly\n" + " artifactId: '*'\n" + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - mockManifest(resolver, "test", "test.manifest", "manifest.yaml"); - mockBlocklist(resolver, "channels/invalid-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + mockBlocklistResolution(resolver, "channels/invalid-blocklist.yaml"); when(factory.create(any())).thenReturn(resolver); diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java index bace5645..a1b95244 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java @@ -56,20 +56,21 @@ public void testChannelWhichRequiresAnotherChannel() throws Exception { MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + File resolvedArtifactFile = mock(File.class); URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); - File resolvedArtifactFile = new File(resolvedRequiredManifestURL.toURI()); when(factory.create(any())) .thenReturn(resolver); when(resolver.getAllVersions("test.channels", "required-manifest", "yaml", "manifest")) .thenReturn(new HashSet<>(Arrays.asList("1", "2", "3"))); - when(resolver.resolveArtifact("test.channels", "required-manifest", "yaml", "manifest", "3")) + when(resolver.resolveArtifact("org.example", "required-manifest", "yaml", "manifest", "3")) .thenReturn(resolvedArtifactFile); when(resolver.getAllVersions("org.example", "foo-bar", null, null)) .thenReturn(new HashSet<>(Arrays.asList("1.0.0.Final, 1.1.0.Final", "1.2.0.Final"))); when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) .thenReturn(resolvedArtifactFile); + when(resolver.resolveChannelMetadata(any())).thenReturn(List.of(resolvedRequiredManifestURL)); String baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + "name: My manifest\n" + @@ -105,11 +106,6 @@ public void testChannelWhichRequiresAnotherChannel() throws Exception { } } - private static void mockManifest(MavenVersionsResolver resolver, URL resolvedRequiredManifestURL) throws URISyntaxException { - when(resolver.resolveArtifact(any(), any(), eq(ChannelManifest.EXTENSION), eq(ChannelManifest.CLASSIFIER), any())) - .thenReturn(new File(resolvedRequiredManifestURL.toURI())); - } - /** * Test that specific version of required channel is used when required */ @@ -128,7 +124,7 @@ public void testChannelWhichRequiresAnotherVersionedChannel() throws Exception { .thenReturn(resolver); when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) .thenReturn(resolvedArtifactFile); - mockManifest(resolver, resolvedRequiredManifestURL); + when(resolver.resolveChannelMetadata(any())).thenReturn(List.of(resolvedRequiredManifestURL)); String baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + "name: My manifest\n" + @@ -196,7 +192,8 @@ public void testRequiringChannelOverridesStreamFromRequiredChannel() throws Exce .thenReturn(resolvedArtifactFile120Final); when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "2.0.0.Final")) .thenReturn(resolvedArtifactFile200Final); - mockManifest(resolver, resolvedRequiredManifestURL, "test.channels:required-manifest:1.0.0"); + when(resolver.resolveChannelMetadata(eq(List.of(new ChannelManifestCoordinate("test.channels", "required-manifest", "1.0.0"))))) + .thenReturn(List.of(resolvedRequiredManifestURL)); // The requiring channel requires newer version of foo-bar artifact List channels = ChannelMapper.fromString( @@ -568,19 +565,11 @@ public void testChannelMultipleRequirements() throws Exception { } } - private void mockManifest(MavenVersionsResolver resolver, String manifest, String gavString) throws IOException { + private void mockManifest(MavenVersionsResolver resolver, String manifest, String gav) throws IOException { Path manifestFile = tempDir.resolve("manifest_" + RandomUtils.nextInt() + ".yaml"); Files.writeString(manifestFile, manifest); - final String[] splitGav = gavString.split(":"); - final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); - if (gav.getVersion() == null) { - when(resolver.getAllVersions(gav.getGroupId(), gav.getArtifactId(), ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER)) - .thenReturn(Set.of("1.0.0")); - } - when(resolver.resolveArtifact(gav.getGroupId(), gav.getArtifactId(), ChannelManifest.EXTENSION, - ChannelManifest.CLASSIFIER, gav.getVersion() == null ? "1.0.0" : gav.getVersion())) - .thenReturn(manifestFile.toFile()); + mockManifest(resolver, manifestFile.toUri().toURL(), gav); } @Test @@ -629,15 +618,10 @@ public void testRequiredChannelIgnoresNoStreamStrategy() throws Exception { } } - private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws URISyntaxException { + private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws IOException { final String[] splitGav = gavString.split(":"); final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); - if (gav.getVersion() == null) { - when(resolver.getAllVersions(gav.getGroupId(), gav.getArtifactId(), ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER)) - .thenReturn(Set.of("1.0.0")); - } - when(resolver.resolveArtifact(gav.getGroupId(), gav.getArtifactId(), ChannelManifest.EXTENSION, - ChannelManifest.CLASSIFIER, gav.getVersion() == null ? "1.0.0" : gav.getVersion())) - .thenReturn(new File(manifestUrl.toURI())); + when(resolver.resolveChannelMetadata(eq(List.of(ChannelManifestCoordinate.create(null, gav))))) + .thenReturn(List.of(manifestUrl)); } } diff --git a/core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java b/core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java new file mode 100644 index 00000000..d781b6c8 --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.wildfly.channel.SignedVersionResolverWrapper.SIGNATURE_FILE_SUFFIX; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.wildfly.channel.spi.ArtifactIdentifier; +import org.wildfly.channel.spi.MavenVersionsResolver; +import org.wildfly.channel.spi.SignatureResult; +import org.wildfly.channel.spi.SignatureValidator; + +class SignedVersionResolverWrapperTest { + + private static final ArtifactIdentifier.MavenResource ARTIFACT = new ArtifactIdentifier.MavenResource( + "org.wildfly", "wildfly-ee-galleon-pack", "zip", null, "25.0.1.Final"); + + @TempDir + private Path tempDir; + private MavenVersionsResolver resolver; + private SignatureValidator signatureValidator; + private SignedVersionResolverWrapper signedResolver; + + private File signatureFile; + private File resolvedArtifactFile; + + @BeforeEach + public void setUp() throws Exception { + resolver = mock(MavenVersionsResolver.class); + signatureValidator = mock(SignatureValidator.class); + signedResolver = new SignedVersionResolverWrapper(resolver, List.of(new Repository("test", "test")), + signatureValidator, Collections.emptyList()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + when(factory.create(any())).thenReturn(resolver); + + signatureFile = tempDir.resolve("test-signature.asc").toFile(); + Files.createFile(signatureFile.toPath()); + + resolvedArtifactFile = tempDir.resolve("test-artifact").toFile(); + Files.createFile(resolvedArtifactFile.toPath()); + + when(factory.create(any())) + .thenReturn(resolver); + when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) + .thenReturn(resolvedArtifactFile); + when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)) + .thenReturn(resolvedArtifactFile); + } + + @Test + public void invalidSignatureCausesError() throws Exception { + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + Files.createFile(tempDir.resolve("test-manifest.yaml.asc")); + when(resolver.resolveArtifact("test.channels", "base-manifest", + ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) + .thenReturn(tempDir.resolve("test-manifest.yaml.asc").toFile()); + when(signatureValidator.validateSignature(any(), any(), any(), any())).thenReturn(SignatureResult.invalid(mock(ArtifactIdentifier.class))); + assertThrows(SignatureValidator.SignatureException.class, + () -> signedResolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test.channels", "base-manifest", "1.0.0")))); + } + + @Test + public void mvnManifestWithoutSignatureCausesError() throws Exception { + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + final Path manifestFile = tempDir.resolve("test-manifest.yaml"); + Files.writeString(manifestFile, ChannelManifestMapper.toYaml(baseManifest)); + + when(resolver.resolveArtifact("test.channels", "base-manifest", + ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) + .thenThrow(ArtifactTransferException.class); + assertThrows(SignatureValidator.SignatureException.class, + () -> signedResolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test.channels", "base-manifest", "1.0.0")))); + } + + @Test + public void artifactWithCorrectSignatureIsValidated() throws Exception { + // return signature when resolving it from Maven repository + when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, + ARTIFACT.classifier, ARTIFACT.version)) + .thenReturn(signatureFile); + // accept all the validation requests + when(signatureValidator.validateSignature(any(), + any(), any(), any())).thenReturn(SignatureResult.ok()); + + + assertEquals(resolvedArtifactFile, signedResolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)); + + verify(signatureValidator).validateSignature(any(), any(), any(), any()); + } + + @Test + public void artifactWithoutSignatureIsRejected() throws Exception { + // simulate situation where the signature file does not exist in the repository + when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, + ARTIFACT.classifier, ARTIFACT.version)) + .thenThrow(ArtifactTransferException.class); + // accept all the validation requests + when(signatureValidator.validateSignature(any(), + any(), any(), any())).thenReturn(SignatureResult.ok()); + + + assertThrows(SignatureValidator.SignatureException.class, () -> signedResolver.resolveArtifact( + ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)); + + + // validateSignature should not have been called + verify(signatureValidator, never()).validateSignature(any(), any(), any(), any()); + } + + @Test + public void failedSignatureValidationThrowsException() throws Exception { + // return signature when resolving it from Maven repository + when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, + ARTIFACT.classifier, ARTIFACT.version)) + .thenReturn(signatureFile); + when(signatureValidator.validateSignature(eq(ARTIFACT), + any(), any(), any())).thenReturn(SignatureResult.invalid(ARTIFACT)); + + assertThrows(SignatureValidator.SignatureException.class, () -> signedResolver.resolveArtifact(ARTIFACT.groupId, + ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)); + + + verify(signatureValidator).validateSignature(any(), any(), any(), any()); + } + + private void mockManifest(MavenVersionsResolver resolver, ChannelManifest manifest, String gav) throws IOException { + + mockManifest(resolver, ChannelManifestMapper.toYaml(manifest), gav); + } + + private void mockManifest(MavenVersionsResolver resolver, String manifest, String gav) throws IOException { + Path manifestFile = tempDir.resolve("manifest_" + RandomUtils.nextInt() + ".yaml"); + Files.writeString(manifestFile, manifest); + + mockManifest(resolver, manifestFile.toUri().toURL(), gav); + } + + private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws IOException { + final String[] splitGav = gavString.split(":"); + final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); + + when(resolver.resolveChannelMetadata(eq(List.of(ChannelManifestCoordinate.create(null, gav))))) + .thenReturn(List.of(manifestUrl)); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java b/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java index 3c21b78f..4a2d3935 100644 --- a/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java +++ b/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java @@ -71,7 +71,7 @@ public void multipleChannelsTest() throws IOException { } @Test - public void simpleChannelTest() throws IOException { + public void simpleChannelTest() throws MalformedURLException { ClassLoader tccl = Thread.currentThread().getContextClassLoader(); URL file = tccl.getResource("channels/simple-channel.yaml"); diff --git a/doc/spec.adoc b/doc/spec.adoc index fc4ade11..ab7351e9 100644 --- a/doc/spec.adoc +++ b/doc/spec.adoc @@ -77,7 +77,7 @@ A channel is composed of several fields: ** `maven-latest` - a version marked as `latest` in the Maven metadata ** `maven-release` - a version marked as `release` in the Maven metadata ** `none` - do not attempt to resolve versions of artifact not listed in the `streams` collection. Default value if no strategy is provided. -* Optional `gpg-check` boolean indicating if the artifacts resolved from this channel have to have a valid GPG signature. +* Optional `gpg-check` boolean indicating that during artifact resolution, this channel will verify a signature of every artifact. If the artifact signature cannot be found, or cannot be validated, the artifact will not be resolved from the channel. The channel repositories must contain a detached GPG signature paired with the artifact as described below. * Optional `gpg-urls` a list of URLs that the public GPG keys used to validate artifact signatures are resolved from. ### Manifest definition @@ -239,11 +239,11 @@ During artifact version resolution, a stream matching artifact GA is located in ### Verifing artifact PGP signatures -If a channel sets value of `gpg-check` property to `true`, any artifact resolved from it (including the manifest itself) has to have a valid GPG signature. +If a channel sets value of `gpg-check` property to `true`, any artifact resolved from it (including the manifest itself) must have a valid GPG signature. -#### Verifing maven artifact signatures +#### Verifing Maven artifact signatures -Maven artifacts are expected to have their detached GPG signatures available in the channel repositories. The detached artifacts must have the same GAV as the artifact, but append ".asc" suffix to the file expension. The signature file must contain an armoured GPG signature. +Maven artifacts are expected to have their detached GPG signatures available in the channel repositories. The detached artifacts must have the same GAV as the artifact, but append ".asc" suffix to the file extension. The signature file must contain an armoured GPG signature. The signature file is resolved at the same time as the artifact. If the signature is invalid, the public key is not found, the public key is expired or revoked, the artifact must be rejected and a SignatureException must be thrown. @@ -263,6 +263,7 @@ Implementations may provide additional means of providing keys - local stores, r * Adding ability to verify artifact signatures ** Adding `gpg-check` and `gpg-urls` fields to the Channel definition. +** Adding `signature-url` field to the Channel's manifest coordinate definition. ### Version 2.0.0 diff --git a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java index 2861e0f3..17c8d7cd 100644 --- a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java +++ b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java @@ -26,8 +26,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -60,9 +58,9 @@ import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelMapper; import org.wildfly.channel.ChannelMetadataCoordinate; -import org.wildfly.channel.MavenArtifact; import org.wildfly.channel.Repository; import org.wildfly.channel.NoStreamFoundException; +import org.wildfly.channel.SignedVersionResolverWrapper; import org.wildfly.channel.UnresolvedMavenArtifactException; import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.spi.SignatureValidator; @@ -93,28 +91,44 @@ public class VersionResolverFactory implements MavenVersionsResolver.Factory { private final RepositorySystem system; private final RepositorySystemSession session; private final Function repositoryFactory; + private SignatureValidator signatureValidator; public VersionResolverFactory(RepositorySystem system, RepositorySystemSession session) { - this(system, session, DEFAULT_REPOSITORY_MAPPER); + this(system, session, null); } public VersionResolverFactory(RepositorySystem system, RepositorySystemSession session, + SignatureValidator signatureValidator) { + this(system, session, signatureValidator, DEFAULT_REPOSITORY_MAPPER); + } + + public VersionResolverFactory(RepositorySystem system, + RepositorySystemSession session, + SignatureValidator signatureValidator, Function repositoryFactory) { this.system = system; this.session = session; + this.signatureValidator = signatureValidator; this.repositoryFactory = repositoryFactory; } @Override - public MavenVersionsResolver create(Collection repositories) { - Objects.requireNonNull(repositories); + public MavenVersionsResolver create(Channel channel) { + Objects.requireNonNull(channel); - final List mvnRepositories = repositories.stream() + final List mvnRepositories = channel.getRepositories().stream() .map(repositoryFactory::apply) .collect(Collectors.toList()); - return create(mvnRepositories); + if (channel.isGpgCheck()) { + if (signatureValidator == null) { + throw new RuntimeException("The channel %s requires GPG verification, but the signature validator is not configured."); + } + return new SignedVersionResolverWrapper(create(mvnRepositories), channel.getRepositories(), signatureValidator, channel.getGpgUrls()); + } else { + return create(mvnRepositories); + } } private MavenResolverImpl create(List mvnRepositories) { diff --git a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java index 0793f9e5..0ab75b4b 100644 --- a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java +++ b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java @@ -88,7 +88,7 @@ public void testResolverGetAllVersions() throws VersionRangeResolutionException when(system.resolveVersionRange(eq(session), any(VersionRangeRequest.class))).thenReturn(versionRangeResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.singletonList(testRepository)); + MavenVersionsResolver resolver = factory.create(new Channel.Builder().addRepository(testRepository.getId(), testRepository.getUrl()).build()); Set allVersions = resolver.getAllVersions("org.foo", "bar", null, null); assertEquals(3, allVersions.size()); @@ -111,7 +111,7 @@ public void testResolverResolveArtifact() throws ArtifactResolutionException { when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenReturn(artifactResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); File resolvedArtifact = resolver.resolveArtifact("org.foo", "bar", null, null, "1.0.0"); assertEquals(artifactFile, resolvedArtifact); @@ -125,7 +125,7 @@ public void testResolverCanNotResolveArtifact() throws ArtifactResolutionExcepti when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenThrow(ArtifactResolutionException.class); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.resolveArtifact("org.foo", "does-not-exist", null, null, "1.0.0"); @@ -179,7 +179,7 @@ public void testResolverResolveAllArtifacts() throws ArtifactResolutionException when(system.resolveArtifacts(eq(session), any(List.class))).thenReturn(Arrays.asList(artifactResult1, artifactResult2)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); final List coordinates = asList( new ArtifactCoordinate("org.foo", "bar", null, null, "1.0.0"), @@ -197,7 +197,7 @@ public void testResolverResolveMetadataUsingUrl() throws ArtifactResolutionExcep RepositorySystemSession session = mock(RepositorySystemSession.class); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate(new URL("http://test.channel")))); assertEquals(new URL("http://test.channel"), resolvedURL.get(0)); @@ -231,7 +231,7 @@ public void testResolverResolveMetadataUsingGa() throws ArtifactResolutionExcept when(system.resolveArtifact(eq(session), artifactRequestArgumentCaptor.capture())).thenReturn(artifactResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.singletonList(testRepository)); + MavenVersionsResolver resolver = factory.create(new Channel()); List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate("org.test", "channel"))); assertEquals(artifactFile.toURI().toURL(), resolvedURL.get(0)); @@ -253,7 +253,7 @@ public void testResolverResolveMetadataUsingGav() throws ArtifactResolutionExcep when(system.resolveArtifact(eq(session), artifactRequestArgumentCaptor.capture())).thenReturn(artifactResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate("org.test", "channel", "1.0.0"))); assertEquals(artifactFile.toURI().toURL(), resolvedURL.get(0)); @@ -265,9 +265,11 @@ public void testRepositoryFactory() throws Exception { RepositorySystem system = mock(RepositorySystem.class); RepositorySystemSession session = mock(RepositorySystemSession.class); - VersionResolverFactory factory = new VersionResolverFactory(system, session, + VersionResolverFactory factory = new VersionResolverFactory(system, session, null, r -> new RemoteRepository.Builder(r.getId(), "default", r.getUrl() + ".new").build()); - MavenVersionsResolver resolver = factory.create(List.of(new Repository("test_1", "http://test_1"))); + MavenVersionsResolver resolver = factory.create(new Channel.Builder() + .addRepository("test_1", "http://test_1") + .build()); File artifactFile = new File("test"); ArtifactResult artifactResult = new ArtifactResult(new ArtifactRequest()); @@ -312,7 +314,7 @@ public void testResolveLatestFromMetadata() throws Exception { when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result1, result2)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); final String res = resolver.getMetadataLatestVersion("org.foo", "bar"); @@ -332,7 +334,7 @@ public void testResolveLatestFromMetadataNoVersioning() throws Exception { when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.getMetadataLatestVersion("org.foo", "bar"); @@ -348,7 +350,7 @@ public void testResolveLatestFromMetadataNoLatestVersion() throws Exception { when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + MavenVersionsResolver resolver = factory.create(new Channel()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.getMetadataLatestVersion("org.foo", "bar");