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