Skip to content

Commit

Permalink
fix(maven): Correctly convert repositories
Browse files Browse the repository at this point in the history
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 <oliver.heger@bosch.io>
  • Loading branch information
oheger-bosch authored and sschuberth committed Oct 14, 2024
1 parent b4523c9 commit acfb440
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<RepositorySystemSession>()
val artifactRepository = mockk<org.apache.maven.artifact.repository.ArtifactRepository>()
val repositorySystem = mockk<MavenRepositorySystem> {
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<RepositorySystemSession>()
val artifactRepository = mockk<org.apache.maven.artifact.repository.ArtifactRepository> {
every { proxy = any() } just runs
}

val repositorySystem = mockk<MavenRepositorySystem> {
every {
createRepository(any(), any(), true, null, true, null, null)
} returns artifactRepository
}

repository.toArtifactRepository(session, repositorySystem, "id") shouldBe artifactRepository

val slotProxy = slot<MavenProxy>()
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<RepositorySystemSession>()
val artifactRepository = mockk<org.apache.maven.artifact.repository.ArtifactRepository> {
every { authentication = any() } just runs
}

val repositorySystem = mockk<MavenRepositorySystem> {
every {
createRepository(any(), any(), true, null, true, null, null)
} returns artifactRepository
}

repository.toArtifactRepository(session, repositorySystem, "id") shouldBe artifactRepository

val slotAuth = slot<Authentication>()
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<RepositorySystemSession>()
val artifactRepository = mockk<org.apache.maven.artifact.repository.ArtifactRepository> {
every { proxy = any() } just runs
}

val repositorySystem = mockk<MavenRepositorySystem> {
every {
createRepository(any(), any(), true, null, true, null, null)
} returns artifactRepository
}

repository.toArtifactRepository(session, repositorySystem, "id") shouldBe artifactRepository

val slotProxy = slot<MavenProxy>()
verify {
artifactRepository.proxy = capture(slotProxy)
}

with(slotProxy.captured) {
userName shouldBe "proxyUser"
password shouldBe "proxyPassword"
}
}
}
})

0 comments on commit acfb440

Please sign in to comment.