diff --git a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt index 3fcfad11be6..b993deeb678 100644 --- a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt +++ b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt @@ -7,14 +7,13 @@ package kotlin.script.experimental.dependencies.maven import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.resolution.ArtifactResolutionException import org.eclipse.aether.resolution.DependencyResolutionException import org.eclipse.aether.util.artifact.JavaScopes import org.eclipse.aether.util.repository.AuthenticationBuilder import java.io.File import java.util.* -import kotlin.script.experimental.api.ResultWithDiagnostics -import kotlin.script.experimental.api.SourceCode -import kotlin.script.experimental.api.asSuccess +import kotlin.script.experimental.api.* import kotlin.script.experimental.dependencies.ExternalDependenciesResolver import kotlin.script.experimental.dependencies.RepositoryCoordinates import kotlin.script.experimental.dependencies.impl.* @@ -71,13 +70,30 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { ) ResultWithDiagnostics.Success(deps.map { it.file }) } catch (e: DependencyResolutionException) { - makeResolveFailureResult(e.message ?: "unknown error", sourceCodeLocation) + makeResolveFailureResult(e, sourceCodeLocation) } } - private fun tryResolveEnvironmentVariable(str: String) = - if (str.startsWith("$")) System.getenv(str.substring(1)) ?: str - else str + private fun tryResolveEnvironmentVariable( + str: String?, + optionName: String, + location: SourceCode.LocationWithId? + ): ResultWithDiagnostics { + 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( repositoryCoordinates: RepositoryCoordinates, @@ -87,30 +103,48 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { val url = repositoryCoordinates.toRepositoryUrlOrNull() ?: return false.asSuccess() val repoId = repositoryCoordinates.string.replace(FORBIDDEN_CHARS, "_") - val repo = RemoteRepository.Builder(repoId, "default", url.toString()).apply { - /** - * 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 - */ - setAuthentication( - AuthenticationBuilder().apply { - @Suppress("DEPRECATION") - val mavenRepo = repositoryCoordinates as? MavenRepositoryCoordinates - val username = options.username ?: mavenRepo?.username - val password = options.password ?: mavenRepo?.password - addUsername(username?.let(::tryResolveEnvironmentVariable)) - addPassword(password?.let(::tryResolveEnvironmentVariable)) - addPrivateKey( - options.privateKeyFile?.let(::tryResolveEnvironmentVariable), - options.privateKeyPassphrase?.let(::tryResolveEnvironmentVariable) - ) - }.build() + + @Suppress("DEPRECATION") + val mavenRepo = repositoryCoordinates as? MavenRepositoryCoordinates + val usernameRaw = options.username ?: mavenRepo?.username + val passwordRaw = options.password ?: mavenRepo?.password + + val reports = mutableListOf() + fun getFinalValue(optionName: String, rawValue: String?): String? { + return tryResolveEnvironmentVariable(rawValue, optionName, sourceCodeLocation) + .onFailure { reports.addAll(it.reports) } + .valueOrNull() + } + + val username = getFinalValue("username", usernameRaw) + val password = getFinalValue("password", passwordRaw) + val privateKeyFile = getFinalValue("private key file", options.privateKeyFile) + val privateKeyPassphrase = getFinalValue("private key passphrase", options.privateKeyPassphrase) + + if (reports.isNotEmpty()) { + return ResultWithDiagnostics.Failure(reports) + } + + /** + * 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) return true.asSuccess() @@ -124,5 +158,23 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { * they should be replaced with an allowed character. */ 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) + } } } \ No newline at end of file diff --git a/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt b/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt index 8a79e66a8ff..978d87d3e56 100644 --- a/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt +++ b/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt @@ -124,6 +124,46 @@ class MavenResolverTest : ResolversTestBase() { 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 // TODO: find a way to enable it back @Ignore