From acfb44031871190215585b63ce331595437357fe Mon Sep 17 00:00:00 2001 From: Oliver Heger Date: Wed, 9 Oct 2024 15:13:19 +0200 Subject: [PATCH] fix(maven): Correctly convert repositories When resolving artifacts using a `ProjectBuilder`, remote repositories from the Eclipse Aether library have to be converted to the model used by the Maven repository system. So far, only the most prominent properties have been converted. In certain constellations, this is not sufficient. In a concrete case, resolution failed for an artifact located in a repository that was defined in a pom of a transitive dependency. For this repository the information about the required proxy was lost, and so network requests caused exceptions. Fix this by also dealing with the properties related to the proxy and authentication. Signed-off-by: Oliver Heger --- .../src/main/kotlin/utils/MavenSupport.kt | 56 +++++++- .../src/test/kotlin/utils/MavenSupportTest.kt | 134 ++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) diff --git a/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt b/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt index 518d7e10e2f3a..ee2ae231dc311 100644 --- a/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt +++ b/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt @@ -25,6 +25,7 @@ import java.net.URI import kotlin.time.Duration.Companion.hours import org.apache.logging.log4j.kotlin.logger +import org.apache.maven.artifact.repository.Authentication import org.apache.maven.artifact.repository.LegacyLocalRepositoryManager import org.apache.maven.bridge.MavenRepositorySystem import org.apache.maven.execution.DefaultMavenExecutionRequest @@ -42,6 +43,7 @@ import org.apache.maven.project.ProjectBuildingException import org.apache.maven.project.ProjectBuildingRequest import org.apache.maven.project.ProjectBuildingResult import org.apache.maven.properties.internal.EnvironmentUtils +import org.apache.maven.repository.Proxy import org.apache.maven.session.scope.internal.SessionScope import org.codehaus.plexus.DefaultContainerConfiguration @@ -58,6 +60,7 @@ import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.impl.RemoteRepositoryManager import org.eclipse.aether.impl.RepositoryConnectorProvider +import org.eclipse.aether.repository.AuthenticationContext import org.eclipse.aether.repository.MirrorSelector import org.eclipse.aether.repository.RemoteRepository import org.eclipse.aether.repository.WorkspaceReader @@ -310,6 +313,57 @@ class MavenSupport(private val workspaceReader: WorkspaceReader) { runCatching { Hash(it, algorithm) }.getOrNull() } ?: Hash.NONE + /** + * Convert this [RemoteRepository] to a repository in the format used by the Maven Repository System. + * Make sure that all relevant properties are set, especially the proxy and authentication. + */ + internal fun RemoteRepository.toArtifactRepository( + repositorySystemSession: RepositorySystemSession, + repositorySystem: MavenRepositorySystem, + id: String + ) = repositorySystem.createRepository(url, id, true, null, true, null, null).apply { + this@toArtifactRepository.proxy?.also { repoProxy -> + proxy = Proxy().apply { + host = repoProxy.host + port = repoProxy.port + protocol = repoProxy.type + toMavenAuthentication( + AuthenticationContext.forProxy( + repositorySystemSession, + this@toArtifactRepository + ) + )?.also { authentication -> + userName = authentication.username + password = authentication.password + } + } + } + + this@toArtifactRepository.authentication?.also { + authentication = toMavenAuthentication( + AuthenticationContext.forRepository( + repositorySystemSession, + this@toArtifactRepository + ) + ) + } + } + + /** + * Return authentication information for an artifact repository based on the given [authContext]. The + * libraries involved use different approaches to model authentication. + */ + private fun toMavenAuthentication(authContext: AuthenticationContext?): Authentication? = + authContext?.let { + Authentication( + it[AuthenticationContext.USERNAME], + it[AuthenticationContext.PASSWORD] + ).apply { + passphrase = it[AuthenticationContext.PRIVATE_KEY_PASSPHRASE] + privateKey = it[AuthenticationContext.PRIVATE_KEY_PATH] + } + } + /** * Return true if an artifact that has not been requested from Maven Central is also available on Maven Central * but with a different hash, otherwise return false. @@ -683,7 +737,7 @@ class MavenSupport(private val workspaceReader: WorkspaceReader) { // As the ID might be used as the key when generating a metadata file name, avoid the URL being used as the // ID as the URL is likely to contain characters like ":" which not all file systems support. val id = repo.id.takeUnless { it == repo.url } ?: repo.host - mavenRepositorySystem.createRepository(repo.url, id, true, null, true, null, null) + repo.toArtifactRepository(repositorySystemSession, mavenRepositorySystem, id) } + projectBuildingRequest.remoteRepositories val localProject = localProjects[artifact.identifier()] diff --git a/plugins/package-managers/maven/src/test/kotlin/utils/MavenSupportTest.kt b/plugins/package-managers/maven/src/test/kotlin/utils/MavenSupportTest.kt index 68350d396fd1d..7fc052eaf2a67 100644 --- a/plugins/package-managers/maven/src/test/kotlin/utils/MavenSupportTest.kt +++ b/plugins/package-managers/maven/src/test/kotlin/utils/MavenSupportTest.kt @@ -22,12 +22,28 @@ package org.ossreviewtoolkit.plugins.packagemanagers.maven.utils import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify + +import org.apache.maven.artifact.repository.Authentication +import org.apache.maven.bridge.MavenRepositorySystem import org.apache.maven.model.Scm import org.apache.maven.project.MavenProject +import org.apache.maven.repository.Proxy as MavenProxy + +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.repository.Proxy +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.util.repository.AuthenticationBuilder import org.ossreviewtoolkit.model.Hash import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.VcsType +import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.MavenSupport.Companion.toArtifactRepository class MavenSupportTest : WordSpec({ "getOriginalScm()" should { @@ -210,4 +226,122 @@ class MavenSupportTest : WordSpec({ ) shouldBe Hash("868c0792233fc78d8c9bac29ac79ade988301318", "SHA1") } } + + @Suppress("DEPRECATION") // For deprecated ArtifactRepository interface. + "toArtifactRepository()" should { + "create a plain artifact repository from a remote repository" { + val repositoryId = "aTestRepository" + val repositoryUrl = "https://example.com/repo" + val repository = RemoteRepository.Builder("ignoredId", null, repositoryUrl).build() + + val session = mockk() + val artifactRepository = mockk() + val repositorySystem = mockk { + every { + createRepository(repositoryUrl, repositoryId, true, null, true, null, null) + } returns artifactRepository + } + + repository.toArtifactRepository(session, repositorySystem, repositoryId) shouldBe artifactRepository + } + + "create an artifact repository with a configured proxy" { + val repository = RemoteRepository.Builder("someId", "someType", "https://example.com/repo") + .setProxy(Proxy("http", "proxy.example.com", 8080)) + .build() + + val session = mockk() + val artifactRepository = mockk { + every { proxy = any() } just runs + } + + val repositorySystem = mockk { + every { + createRepository(any(), any(), true, null, true, null, null) + } returns artifactRepository + } + + repository.toArtifactRepository(session, repositorySystem, "id") shouldBe artifactRepository + + val slotProxy = slot() + verify { + artifactRepository.proxy = capture(slotProxy) + } + + with(slotProxy.captured) { + host shouldBe "proxy.example.com" + port shouldBe 8080 + protocol shouldBe "http" + } + } + + "create an artifact repository with authentication" { + val repository = RemoteRepository.Builder("someId", "someType", "https://example.com/repo") + .setAuthentication( + AuthenticationBuilder() + .addUsername("scott") + .addPassword("tiger".toCharArray()) + .addPrivateKey("privateKeyPath", "passphrase") + .build() + ).build() + + val session = mockk() + val artifactRepository = mockk { + every { authentication = any() } just runs + } + + val repositorySystem = mockk { + every { + createRepository(any(), any(), true, null, true, null, null) + } returns artifactRepository + } + + repository.toArtifactRepository(session, repositorySystem, "id") shouldBe artifactRepository + + val slotAuth = slot() + verify { + artifactRepository.authentication = capture(slotAuth) + } + + with(slotAuth.captured) { + username shouldBe "scott" + password shouldBe "tiger" + privateKey shouldBe "privateKeyPath" + passphrase shouldBe "passphrase" + } + } + + "create an artifact repository with a configured proxy that requires authentication" { + val proxyAuth = AuthenticationBuilder() + .addUsername("proxyUser") + .addPassword("proxyPassword".toCharArray()) + .build() + val repository = RemoteRepository.Builder("someId", "someType", "https://example.com/repo") + .setProxy(Proxy("http", "proxy.example.com", 8080, proxyAuth)) + .build() + + val session = mockk() + val artifactRepository = mockk { + every { proxy = any() } just runs + } + + val repositorySystem = mockk { + every { + createRepository(any(), any(), true, null, true, null, null) + } returns artifactRepository + } + + repository.toArtifactRepository(session, repositorySystem, "id") shouldBe artifactRepository + + val slotProxy = slot() + verify { + artifactRepository.proxy = capture(slotProxy) + } + + with(slotProxy.captured) { + userName shouldBe "proxyUser" + password shouldBe "proxyPassword" + } + } + } })