mirror of https://github.com/sbt/sbt.git
Fixes #429 - Add better error messages when credentials are not found.
* Create hackery to inspect registered credentials in the IvyCredentialStore.
* Create a new authenticator which inserts itself *after* the ivy authenticator.
- Will issue an error message detailing host/realm required if credentials are
not found.
- Also lists out configured Realms with a 'is misspelled' message.
- Ignores proxy-related authentication errors, for now.
This commit is contained in:
parent
c5c6978c94
commit
25400a11d3
|
|
@ -99,6 +99,8 @@ final class IvySbt(val configuration: IvyConfiguration)
|
||||||
def withIvy[T](log: MessageLogger)(f: Ivy => T): T =
|
def withIvy[T](log: MessageLogger)(f: Ivy => T): T =
|
||||||
withDefaultLogger(log)
|
withDefaultLogger(log)
|
||||||
{
|
{
|
||||||
|
// See #429 - We always insert a helper authenticator here which lets us get more useful authentication errors.
|
||||||
|
ivyint.ErrorMessageAuthenticator.install()
|
||||||
ivy.pushContext()
|
ivy.pushContext()
|
||||||
ivy.getLoggerEngine.pushLogger(log)
|
ivy.getLoggerEngine.pushLogger(log)
|
||||||
try { f(ivy) }
|
try { f(ivy) }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package sbt
|
||||||
|
package ivyint
|
||||||
|
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.net.Authenticator
|
||||||
|
import java.net.PasswordAuthentication
|
||||||
|
import org.apache.ivy.util.Credentials
|
||||||
|
import org.apache.ivy.util.Message
|
||||||
|
import org.apache.ivy.util.url.IvyAuthenticator
|
||||||
|
import org.apache.ivy.util.url.CredentialsStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to install an Authenticator that works with the IvyAuthenticator to provide better error messages when
|
||||||
|
* credentials don't line up.
|
||||||
|
*/
|
||||||
|
object ErrorMessageAuthenticator {
|
||||||
|
private var securityWarningLogged = false
|
||||||
|
|
||||||
|
private def originalAuthenticator: Option[Authenticator] = {
|
||||||
|
try {
|
||||||
|
val f = classOf[Authenticator].getDeclaredField("theAuthenticator");
|
||||||
|
f.setAccessible(true);
|
||||||
|
Option(f.get(null).asInstanceOf[Authenticator])
|
||||||
|
} catch {
|
||||||
|
// TODO - Catch more specific errors.
|
||||||
|
case t: Throwable =>
|
||||||
|
Message.debug("Error occurred while getting the original authenticator: " + t.getMessage)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy val ivyOriginalField = {
|
||||||
|
val field = classOf[IvyAuthenticator].getDeclaredField("original")
|
||||||
|
field.setAccessible(true)
|
||||||
|
field
|
||||||
|
}
|
||||||
|
// Attempts to get the original authenticator form the ivy class or returns null.
|
||||||
|
private def installIntoIvy(ivy: IvyAuthenticator): Option[Authenticator] = {
|
||||||
|
// Here we install ourselves as the IvyAuthenticator's default so we get called AFTER Ivy has a chance to run.
|
||||||
|
def installIntoIvyImpl(original: Option[Authenticator]): Unit = {
|
||||||
|
val newOriginal = new ErrorMessageAuthenticator(original)
|
||||||
|
ivyOriginalField.set(ivy, newOriginal)
|
||||||
|
}
|
||||||
|
|
||||||
|
try Option(ivyOriginalField.get(ivy).asInstanceOf[Authenticator]) match {
|
||||||
|
case Some(alreadyThere: ErrorMessageAuthenticator) => // We're already installed, no need to do the work again.
|
||||||
|
case originalOpt => installIntoIvyImpl(originalOpt)
|
||||||
|
} catch {
|
||||||
|
case t: Throwable =>
|
||||||
|
Message.debug("Error occurred will trying to install debug messages into Ivy Authentication" + t.getMessage)
|
||||||
|
}
|
||||||
|
Some(ivy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Installs the error message authenticator so we have nicer error messages when using java's URL for downloading. */
|
||||||
|
def install() {
|
||||||
|
// Actually installs the error message authenticator.
|
||||||
|
def doInstall(original: Option[Authenticator]): Unit =
|
||||||
|
try Authenticator.setDefault(new ErrorMessageAuthenticator(original))
|
||||||
|
catch {
|
||||||
|
case e: SecurityException if !securityWarningLogged =>
|
||||||
|
securityWarningLogged = true;
|
||||||
|
Message.warn("Not enough permissions to set the ErorrMessageAuthenticator. "
|
||||||
|
+ "Helpful debug messages disabled!");
|
||||||
|
}
|
||||||
|
// We will try to use the original authenticator as backup authenticator.
|
||||||
|
// Since there is no getter available, so try to use some reflection to
|
||||||
|
// obtain it. If that doesn't work, assume there is no original authenticator
|
||||||
|
def doInstallIfIvy(original: Option[Authenticator]): Unit =
|
||||||
|
original match {
|
||||||
|
case Some(installed: ErrorMessageAuthenticator) => // Ignore, we're already installed
|
||||||
|
case Some(ivy: IvyAuthenticator) => installIntoIvy(ivy)
|
||||||
|
case original => doInstall(original)
|
||||||
|
}
|
||||||
|
doInstallIfIvy(originalAuthenticator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* An authenticator which just delegates to a previous authenticator and issues *nice*
|
||||||
|
* error messages on failure to find credentials.
|
||||||
|
*
|
||||||
|
* Since ivy installs its own credentials handler EVERY TIME it resolves or publishes, we want to
|
||||||
|
* install this one at some point and eventually ivy will capture it and use it.
|
||||||
|
*/
|
||||||
|
private[sbt] final class ErrorMessageAuthenticator(original: Option[Authenticator]) extends Authenticator {
|
||||||
|
|
||||||
|
protected override def getPasswordAuthentication(): PasswordAuthentication = {
|
||||||
|
// We're guaranteed to only get here if Ivy's authentication fails
|
||||||
|
if (!isProxyAuthentication) {
|
||||||
|
val host = getRequestingHost
|
||||||
|
// TODO - levenshtein distance "did you mean" message.
|
||||||
|
Message.error(s"Unable to find credentials for [${getRequestingPrompt} @ ${host}].")
|
||||||
|
val configuredRealms = IvyCredentialsLookup.realmsForHost.getOrElse(host, Set.empty)
|
||||||
|
if(!configuredRealms.isEmpty) {
|
||||||
|
Message.error(s" Is one of these realms mispelled for host [${host}]:")
|
||||||
|
configuredRealms foreach { realm =>
|
||||||
|
Message.error(s" * ${realm}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO - Maybe we should work on a helpful proxy message...
|
||||||
|
|
||||||
|
// TODO - To be more maven friendly, we may want to also try to grab the "first" authentication that shows up for a server and try it.
|
||||||
|
// or maybe allow that behavior to be configured, since maven users aren't used to realms (which they should be).
|
||||||
|
|
||||||
|
// Grabs the authentication that would have been provided had we not been installed...
|
||||||
|
def originalAuthentication: Option[PasswordAuthentication] = {
|
||||||
|
Authenticator.setDefault(original.getOrElse(null))
|
||||||
|
try Option(Authenticator.requestPasswordAuthentication(
|
||||||
|
getRequestingHost,
|
||||||
|
getRequestingSite,
|
||||||
|
getRequestingPort,
|
||||||
|
getRequestingProtocol,
|
||||||
|
getRequestingPrompt,
|
||||||
|
getRequestingScheme))
|
||||||
|
finally Authenticator.setDefault(this)
|
||||||
|
}
|
||||||
|
originalAuthentication.getOrElse(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this authentication if for a proxy and not for an HTTP server.
|
||||||
|
* We want to display different error messages, depending.
|
||||||
|
*/
|
||||||
|
private def isProxyAuthentication: Boolean =
|
||||||
|
getRequestorType == Authenticator.RequestorType.PROXY
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package sbt
|
||||||
|
package ivyint
|
||||||
|
|
||||||
|
import org.apache.ivy.util.url.CredentialsStore
|
||||||
|
import collection.JavaConverters._
|
||||||
|
|
||||||
|
/** A key used to store credentials in the ivy credentials store. */
|
||||||
|
private[sbt] sealed trait CredentialKey
|
||||||
|
/** Represents a key in the ivy credentials store that is only specific to a host. */
|
||||||
|
private[sbt] case class Host(name: String) extends CredentialKey
|
||||||
|
/** Represents a key in the ivy credentials store that is keyed to both a host and a "realm". */
|
||||||
|
private[sbt] case class Realm(host: String, realm: String) extends CredentialKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper mechanism to improve credential related error messages.
|
||||||
|
*
|
||||||
|
* This evil class exposes to us the necessary information to warn on credential failure and offer
|
||||||
|
* spelling/typo suggestions.
|
||||||
|
*/
|
||||||
|
private[sbt] object IvyCredentialsLookup {
|
||||||
|
|
||||||
|
/** Helper extractor for Ivy's key-value store of credentials. */
|
||||||
|
private object KeySplit {
|
||||||
|
def unapply(key: String): Option[(String,String)] = {
|
||||||
|
key.indexOf('@') match {
|
||||||
|
case -1 => None
|
||||||
|
case n => Some(key.take(n) -> key.drop(n+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Here we cheat runtime private so we can look in the credentials store.
|
||||||
|
*
|
||||||
|
* TODO - Don't bomb at class load time...
|
||||||
|
*/
|
||||||
|
private val credKeyringField = {
|
||||||
|
val tmp = classOf[CredentialsStore].getDeclaredField("KEYRING")
|
||||||
|
tmp.setAccessible(true)
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All the keys for credentials in the ivy configuration store. */
|
||||||
|
def keyringKeys: Set[CredentialKey] = {
|
||||||
|
val map = credKeyringField.get(null).asInstanceOf[java.util.HashMap[String, Any]]
|
||||||
|
// make a clone of the set...
|
||||||
|
(map.keySet.asScala.map {
|
||||||
|
case KeySplit(realm, host) => Realm(host, realm)
|
||||||
|
case host => Host(host)
|
||||||
|
})(collection.breakOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping of host -> realms in the ivy credentials store.
|
||||||
|
*/
|
||||||
|
def realmsForHost: Map[String, Set[String]] =
|
||||||
|
keyringKeys collect {
|
||||||
|
case x: Realm => x
|
||||||
|
} groupBy { realm =>
|
||||||
|
realm.host
|
||||||
|
} mapValues { realms =>
|
||||||
|
realms map (_.realm)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue