[Scripting] Provide better errors reporting for authentication issues
^KT-50357
This commit is contained in:
+82
-30
@@ -7,14 +7,13 @@ package kotlin.script.experimental.dependencies.maven
|
|||||||
|
|
||||||
import org.eclipse.aether.artifact.DefaultArtifact
|
import org.eclipse.aether.artifact.DefaultArtifact
|
||||||
import org.eclipse.aether.repository.RemoteRepository
|
import org.eclipse.aether.repository.RemoteRepository
|
||||||
|
import org.eclipse.aether.resolution.ArtifactResolutionException
|
||||||
import org.eclipse.aether.resolution.DependencyResolutionException
|
import org.eclipse.aether.resolution.DependencyResolutionException
|
||||||
import org.eclipse.aether.util.artifact.JavaScopes
|
import org.eclipse.aether.util.artifact.JavaScopes
|
||||||
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.script.experimental.api.ResultWithDiagnostics
|
import kotlin.script.experimental.api.*
|
||||||
import kotlin.script.experimental.api.SourceCode
|
|
||||||
import kotlin.script.experimental.api.asSuccess
|
|
||||||
import kotlin.script.experimental.dependencies.ExternalDependenciesResolver
|
import kotlin.script.experimental.dependencies.ExternalDependenciesResolver
|
||||||
import kotlin.script.experimental.dependencies.RepositoryCoordinates
|
import kotlin.script.experimental.dependencies.RepositoryCoordinates
|
||||||
import kotlin.script.experimental.dependencies.impl.*
|
import kotlin.script.experimental.dependencies.impl.*
|
||||||
@@ -71,13 +70,30 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
|
|||||||
)
|
)
|
||||||
ResultWithDiagnostics.Success(deps.map { it.file })
|
ResultWithDiagnostics.Success(deps.map { it.file })
|
||||||
} catch (e: DependencyResolutionException) {
|
} catch (e: DependencyResolutionException) {
|
||||||
makeResolveFailureResult(e.message ?: "unknown error", sourceCodeLocation)
|
makeResolveFailureResult(e, sourceCodeLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryResolveEnvironmentVariable(str: String) =
|
private fun tryResolveEnvironmentVariable(
|
||||||
if (str.startsWith("$")) System.getenv(str.substring(1)) ?: str
|
str: String?,
|
||||||
else str
|
optionName: String,
|
||||||
|
location: SourceCode.LocationWithId?
|
||||||
|
): ResultWithDiagnostics<String?> {
|
||||||
|
if (str == null) return null.asSuccess()
|
||||||
|
if (!str.startsWith("$")) return str.asSuccess()
|
||||||
|
val envName = str.substring(1)
|
||||||
|
val envValue: String? = System.getenv(envName)
|
||||||
|
if (envValue.isNullOrEmpty()) return ResultWithDiagnostics.Failure(
|
||||||
|
ScriptDiagnostic(
|
||||||
|
ScriptDiagnostic.unspecifiedError,
|
||||||
|
"Environment variable `$envName` for $optionName is not set",
|
||||||
|
ScriptDiagnostic.Severity.ERROR,
|
||||||
|
location
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return envValue.asSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun addRepository(
|
override fun addRepository(
|
||||||
repositoryCoordinates: RepositoryCoordinates,
|
repositoryCoordinates: RepositoryCoordinates,
|
||||||
@@ -87,30 +103,48 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
|
|||||||
val url = repositoryCoordinates.toRepositoryUrlOrNull()
|
val url = repositoryCoordinates.toRepositoryUrlOrNull()
|
||||||
?: return false.asSuccess()
|
?: return false.asSuccess()
|
||||||
val repoId = repositoryCoordinates.string.replace(FORBIDDEN_CHARS, "_")
|
val repoId = repositoryCoordinates.string.replace(FORBIDDEN_CHARS, "_")
|
||||||
val repo = RemoteRepository.Builder(repoId, "default", url.toString()).apply {
|
|
||||||
/**
|
@Suppress("DEPRECATION")
|
||||||
* Here we set all the authentication information we have, unconditionally.
|
val mavenRepo = repositoryCoordinates as? MavenRepositoryCoordinates
|
||||||
* Actual information that will be used (as well as lower-level checks,
|
val usernameRaw = options.username ?: mavenRepo?.username
|
||||||
* such as nullability or emptiness) is determined by implementation.
|
val passwordRaw = options.password ?: mavenRepo?.password
|
||||||
*
|
|
||||||
* @see org.eclipse.aether.transport.wagon.WagonTransporter.getProxy
|
val reports = mutableListOf<ScriptDiagnostic>()
|
||||||
* @see org.apache.maven.wagon.shared.http.AbstractHttpClientWagon.openConnectionInternal
|
fun getFinalValue(optionName: String, rawValue: String?): String? {
|
||||||
*/
|
return tryResolveEnvironmentVariable(rawValue, optionName, sourceCodeLocation)
|
||||||
setAuthentication(
|
.onFailure { reports.addAll(it.reports) }
|
||||||
AuthenticationBuilder().apply {
|
.valueOrNull()
|
||||||
@Suppress("DEPRECATION")
|
}
|
||||||
val mavenRepo = repositoryCoordinates as? MavenRepositoryCoordinates
|
|
||||||
val username = options.username ?: mavenRepo?.username
|
val username = getFinalValue("username", usernameRaw)
|
||||||
val password = options.password ?: mavenRepo?.password
|
val password = getFinalValue("password", passwordRaw)
|
||||||
addUsername(username?.let(::tryResolveEnvironmentVariable))
|
val privateKeyFile = getFinalValue("private key file", options.privateKeyFile)
|
||||||
addPassword(password?.let(::tryResolveEnvironmentVariable))
|
val privateKeyPassphrase = getFinalValue("private key passphrase", options.privateKeyPassphrase)
|
||||||
addPrivateKey(
|
|
||||||
options.privateKeyFile?.let(::tryResolveEnvironmentVariable),
|
if (reports.isNotEmpty()) {
|
||||||
options.privateKeyPassphrase?.let(::tryResolveEnvironmentVariable)
|
return ResultWithDiagnostics.Failure(reports)
|
||||||
)
|
}
|
||||||
}.build()
|
|
||||||
|
/**
|
||||||
|
* Here we set all the authentication information we have, unconditionally.
|
||||||
|
* Actual information that will be used (as well as lower-level checks,
|
||||||
|
* such as nullability or emptiness) is determined by implementation.
|
||||||
|
*
|
||||||
|
* @see org.eclipse.aether.transport.wagon.WagonTransporter.getProxy
|
||||||
|
* @see org.apache.maven.wagon.shared.http.AbstractHttpClientWagon.openConnectionInternal
|
||||||
|
*/
|
||||||
|
val auth = AuthenticationBuilder()
|
||||||
|
.addUsername(username)
|
||||||
|
.addPassword(password)
|
||||||
|
.addPrivateKey(
|
||||||
|
privateKeyFile,
|
||||||
|
privateKeyPassphrase
|
||||||
)
|
)
|
||||||
}.build()
|
.build()
|
||||||
|
|
||||||
|
val repo = RemoteRepository.Builder(repoId, "default", url.toString())
|
||||||
|
.setAuthentication(auth)
|
||||||
|
.build()
|
||||||
|
|
||||||
repos.add(repo)
|
repos.add(repo)
|
||||||
return true.asSuccess()
|
return true.asSuccess()
|
||||||
@@ -124,5 +158,23 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
|
|||||||
* they should be replaced with an allowed character.
|
* they should be replaced with an allowed character.
|
||||||
*/
|
*/
|
||||||
private val FORBIDDEN_CHARS = Regex("[/\\\\:<>\"|?*]")
|
private val FORBIDDEN_CHARS = Regex("[/\\\\:<>\"|?*]")
|
||||||
|
|
||||||
|
private fun makeResolveFailureResult(
|
||||||
|
exception: DependencyResolutionException,
|
||||||
|
location: SourceCode.LocationWithId?
|
||||||
|
): ResultWithDiagnostics.Failure {
|
||||||
|
val allCauses = generateSequence(exception) { e: Throwable -> e.cause }.toList()
|
||||||
|
val primaryCause = allCauses.firstOrNull { it is ArtifactResolutionException } ?: exception
|
||||||
|
|
||||||
|
val message = buildString {
|
||||||
|
append(primaryCause::class.simpleName)
|
||||||
|
if (primaryCause.message != null) {
|
||||||
|
append(": ")
|
||||||
|
append(primaryCause.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeResolveFailureResult(message, location)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+40
@@ -124,6 +124,46 @@ class MavenResolverTest : ResolversTestBase() {
|
|||||||
assertTrue(files.any { it.name.startsWith("space-sdk") })
|
assertTrue(files.any { it.name.startsWith("space-sdk") })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun testAuthFailure() {
|
||||||
|
val resolver = MavenDependenciesResolver()
|
||||||
|
val options = buildOptions(
|
||||||
|
DependenciesResolverOptionsName.USERNAME to "invalid name",
|
||||||
|
DependenciesResolverOptionsName.PASSWORD to "invalid password",
|
||||||
|
)
|
||||||
|
resolver.addRepository("https://packages.jetbrains.team/maven/p/crl/maven/", options)
|
||||||
|
val result = runBlocking {
|
||||||
|
resolver.resolve("com.jetbrains:space-sdk:1.0-dev")
|
||||||
|
} as ResultWithDiagnostics.Failure
|
||||||
|
|
||||||
|
assertEquals(1, result.reports.size)
|
||||||
|
val diagnostic = result.reports.single()
|
||||||
|
assertEquals(
|
||||||
|
"ArtifactResolutionException: Could not transfer artifact com.jetbrains:space-sdk:pom:1.0-dev " +
|
||||||
|
"from/to https___packages.jetbrains.team_maven_p_crl_maven_ (https://packages.jetbrains.team/maven/p/crl/maven/): " +
|
||||||
|
"authentication failed for https://packages.jetbrains.team/maven/p/crl/maven/com/jetbrains/space-sdk/1.0-dev/space-sdk-1.0-dev.pom, " +
|
||||||
|
"status: 401 Unauthorized",
|
||||||
|
diagnostic.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAuthIncorrectEnvUsage() {
|
||||||
|
val resolver = MavenDependenciesResolver()
|
||||||
|
val options = buildOptions(
|
||||||
|
DependenciesResolverOptionsName.USERNAME to "\$MY_USERNAME_XXX",
|
||||||
|
DependenciesResolverOptionsName.KEY_PASSPHRASE to "\$MY_KEY_PASSPHRASE_YYY",
|
||||||
|
)
|
||||||
|
val result = resolver.addRepository("https://packages.jetbrains.team/maven/p/crl/maven/", options) as ResultWithDiagnostics.Failure
|
||||||
|
|
||||||
|
val messages = result.reports.map { it.message }
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
"Environment variable `MY_USERNAME_XXX` for username is not set",
|
||||||
|
"Environment variable `MY_KEY_PASSPHRASE_YYY` for private key passphrase is not set"
|
||||||
|
),
|
||||||
|
messages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Ignored - tests with custom repos often break the CI due to the caching issues
|
// Ignored - tests with custom repos often break the CI due to the caching issues
|
||||||
// TODO: find a way to enable it back
|
// TODO: find a way to enable it back
|
||||||
@Ignore
|
@Ignore
|
||||||
|
|||||||
Reference in New Issue
Block a user