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:
Josh Suereth 2014-03-04 11:36:34 -05:00
parent c5c6978c94
commit 25400a11d3
3 changed files with 193 additions and 0 deletions

View File

@ -99,6 +99,8 @@ final class IvySbt(val configuration: IvyConfiguration)
def withIvy[T](log: MessageLogger)(f: Ivy => T): T =
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.getLoggerEngine.pushLogger(log)
try { f(ivy) }

View File

@ -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
}

View File

@ -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)
}
}