From 049320a0682ef30b3db771dbae252f81f30cf57c Mon Sep 17 00:00:00 2001 From: Bartosz Spyrko-Smietanko Date: Thu, 5 Sep 2024 16:08:04 +0100 Subject: [PATCH] Move signature validation from ChannelSession to the VersionResolver --- .../java/org/wildfly/channel/ChannelImpl.java | 175 +------------- .../channel/ChannelManifestCoordinate.java | 2 +- .../org/wildfly/channel/ChannelSession.java | 127 +++++++--- .../channel/SignedVersionResolverWrapper.java | 222 ++++++++++++++++++ .../channel/spi/MavenVersionsResolver.java | 11 +- .../channel/ChannelSessionInitTestCase.java | 104 +------- .../channel/ChannelSessionTestCase.java | 120 +++++++++- ...essionWithSignatureValidationTestCase.java | 175 -------------- .../channel/ChannelWithBlocklistTestCase.java | 102 ++++---- .../ChannelWithRequirementsTestCase.java | 38 +-- .../SignedVersionResolverWrapperTest.java | 186 +++++++++++++++ .../channel/mapping/ChannelTestCase.java | 2 +- doc/spec.adoc | 3 +- .../channel/maven/VersionResolverFactory.java | 30 ++- .../maven/VersionResolverFactoryTest.java | 26 +- 15 files changed, 732 insertions(+), 591 deletions(-) create mode 100644 core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java delete mode 100644 core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java create mode 100644 core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java diff --git a/core/src/main/java/org/wildfly/channel/ChannelImpl.java b/core/src/main/java/org/wildfly/channel/ChannelImpl.java index 1b773531..bb52f1a4 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelImpl.java +++ b/core/src/main/java/org/wildfly/channel/ChannelImpl.java @@ -16,15 +16,10 @@ */ package org.wildfly.channel; -import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; 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; @@ -37,9 +32,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; /** @@ -48,7 +40,6 @@ class ChannelImpl implements AutoCloseable { private static final Logger LOG = Logger.getLogger(ChannelImpl.class); - protected static final String SIGNATURE_FILE_SUFFIX = ".asc"; private Channel channelDefinition; @@ -62,7 +53,6 @@ class ChannelImpl implements AutoCloseable { private boolean dependency = false; public Optional blocklist = Optional.empty(); - private SignatureValidator signatureValidator; public ChannelManifest getManifest() { return channelManifest; @@ -77,18 +67,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); if (channelDefinition.getManifestCoordinate() != null) { channelManifest = resolveManifest(channelDefinition.getManifestCoordinate()); @@ -107,7 +95,8 @@ void init(MavenVersionsResolver.Factory factory, List channels, Sig if (channelDefinition.getBlocklistCoordinate() != null) { BlocklistCoordinate blocklistCoordinate = channelDefinition.getBlocklistCoordinate(); - this.blocklist = resolveChannelMetadata(List.of(blocklistCoordinate), true).stream() + final List urls = resolver.resolveChannelMetadata(List.of(blocklistCoordinate)); + this.blocklist = urls.stream() .map(Blocklist::from) .findFirst(); } @@ -117,7 +106,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; @@ -159,7 +148,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); } @@ -212,92 +201,12 @@ static class ResolveLatestVersionResult { } private ChannelManifest resolveManifest(ChannelManifestCoordinate manifestCoordinate) throws UnresolvedMavenArtifactException { - return resolveChannelMetadata(List.of(manifestCoordinate), false) + return resolver.resolveChannelMetadata(List.of(manifestCoordinate)) .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); @@ -412,79 +321,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 fb2b3046..3da4fca6 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()); @@ -196,9 +176,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::getChannelDefinition) + .flatMap(cd->cd.getRepositories().stream()) + .collect(Collectors.toSet())); + } } /** @@ -216,16 +224,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.getChannelDefinition().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.getChannelDefinition().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::getChannelDefinition) + .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; } @@ -251,7 +307,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 366cbda0..dbda2a4c 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java @@ -21,16 +21,13 @@ 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; -import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -40,7 +37,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 @@ -125,8 +121,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() @@ -364,102 +359,23 @@ public void duplicatedManifestIDsAreDetected() throws Exception { assertThrows(RuntimeException.class, () -> new ChannelSession(channels, factory)); } - @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 6e189845..4f119aa0 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java @@ -43,10 +43,13 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; 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; @@ -78,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 { @@ -96,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; } @@ -127,7 +130,7 @@ public void testFindLatestMavenArtifactVersionThrowsUnresolvedMavenArtifactExcep } } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -162,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 @@ -191,7 +194,7 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( } } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -230,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 @@ -279,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 @@ -341,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 @@ -372,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); @@ -385,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 91ff1328..396d96ca 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; @@ -80,8 +79,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .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)) @@ -92,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 @@ -122,10 +124,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")))) + .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)) @@ -136,7 +141,7 @@ public void testFindLatestMavenArtifactVersionBlocklistDoesntExist() throws Exce assertEquals("25.0.1.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -166,8 +171,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist-with-wildcards.yaml"))); when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) @@ -178,7 +186,7 @@ public void testFindLatestMavenArtifactVersionWithWildcardBlocklist() throws Exc assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -208,9 +216,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .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)).thenReturn(new HashSet<>(singleton("25.0.1.Final"))); @@ -224,7 +234,7 @@ public void testFindLatestMavenArtifactVersionBlocklistsAllVersionsException() t } } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -254,9 +264,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); File resolvedArtifactFile = mock(File.class); @@ -277,7 +289,7 @@ public void testResolveLatestMavenArtifact() throws Exception { assertEquals(resolvedArtifactFile, artifact.getFile()); } - verify(resolver, times(2)).close(); + verify(resolver, times(1)).close(); } @Test @@ -307,8 +319,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .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)).thenReturn(new HashSet<>(Set.of("25.0.1.Final","26.0.0.Final"))); @@ -322,28 +337,7 @@ 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())); - } - - private void mockManifest(MavenVersionsResolver resolver, String groupId, String artifactId, String manifestFileName) { - when(resolver.getAllVersions(groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER)) - .thenReturn(Set.of("1.0.0")); - when(resolver.resolveArtifact(groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenReturn(tempDir.resolve(manifestFileName).toFile()); + verify(resolver, times(1)).close(); } @Test @@ -376,9 +370,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); - mockBlocklist(resolver, "channels/test-blocklist.yaml", "org.wildfly", "wildfly-blocklist", null); + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); File resolvedArtifactFile1 = mock(File.class); File resolvedArtifactFile2 = mock(File.class); @@ -413,7 +409,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 @@ -440,8 +436,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" + @@ -450,8 +446,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)) @@ -462,7 +461,7 @@ public void testFindLatestMavenArtifactVersionInRequiredChannel() throws Excepti assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(3)).close(); + verify(resolver, times(2)).close(); } @Test @@ -492,8 +491,11 @@ 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")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("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 29bbfe23..54833b16 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 @@ -262,6 +262,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");