allow http authentication information to be specified in the configuration
authorSune Foldager <cryo@cyanite.org>
Mon, 04 May 2009 20:26:27 +0200
changeset 8333 89c80c3dc584
parent 8332 3e544c074459
child 8334 0695288e8c37
allow http authentication information to be specified in the configuration
doc/hgrc.5.txt
mercurial/url.py
tests/test-hgweb-auth.py
tests/test-hgweb-auth.py.out
--- a/doc/hgrc.5.txt	Fri May 08 18:30:44 2009 +0200
+++ b/doc/hgrc.5.txt	Mon May 04 20:26:27 2009 +0200
@@ -100,6 +100,45 @@
 Mercurial "hgrc" file, the purpose of each section, its possible
 keys, and their possible values.
 
+[[auth]]
+auth:
+  Authentication credentials for HTTP authentication.
+  Each line has the following format:
+
+    <name>.<argument> = <value>
+
+  where <name> is used to group arguments into authentication entries.
+  Example:
+
+    foo.prefix = hg.intevation.org/mercurial
+    foo.username = foo
+    foo.password = bar
+    foo.schemes = http https
+
+  Supported arguments:
+
+  prefix;;
+    Either '*' or a URI prefix with or without the scheme part. The
+    authentication entry with the longest matching prefix is used
+    (where '*' matches everything and counts as a match of length 1).
+    If the prefix doesn't include a scheme, the match is performed against
+    the URI with its scheme stripped as well, and the schemes argument,
+    q.v., is then subsequently consulted.
+  username;;
+    Username to authenticate with.
+  password;;
+    Optional. Password to authenticate with. If not given the user will be
+    prompted for it.
+  schemes;;
+    Optional. Space separated list of URI schemes to use this authentication
+    entry with. Only used if the prefix doesn't include a scheme. Supported
+    schemes are http and https. They will match static-http and static-https
+    respectively, as well.
+    Default: https.
+
+  If no suitable authentication entry is found, the user is prompted for
+  credentials as usual if required by the remote.
+
 [[decode]]
 decode/encode::
   Filters for transforming files on checkout/checkin. This would
--- a/mercurial/url.py	Fri May 08 18:30:44 2009 +0200
+++ b/mercurial/url.py	Mon May 04 20:26:27 2009 +0200
@@ -105,24 +105,58 @@
             self, realm, authuri)
         user, passwd = authinfo
         if user and passwd:
+            self._writedebug(user, passwd)
             return (user, passwd)
 
-        if not self.ui.interactive():
-            raise util.Abort(_('http authorization required'))
+        user, passwd = self._readauthtoken(authuri)
+        if not user or not passwd:
+            if not self.ui.interactive():
+                raise util.Abort(_('http authorization required'))
 
-        self.ui.write(_("http authorization required\n"))
-        self.ui.status(_("realm: %s\n") % realm)
-        if user:
-            self.ui.status(_("user: %s\n") % user)
-        else:
-            user = self.ui.prompt(_("user:"), default=None)
+            self.ui.write(_("http authorization required\n"))
+            self.ui.status(_("realm: %s\n") % realm)
+            if user:
+                self.ui.status(_("user: %s\n") % user)
+            else:
+                user = self.ui.prompt(_("user:"), default=None)
 
-        if not passwd:
-            passwd = self.ui.getpass()
+            if not passwd:
+                passwd = self.ui.getpass()
 
         self.add_password(realm, authuri, user, passwd)
+        self._writedebug(user, passwd)
         return (user, passwd)
 
+    def _writedebug(self, user, passwd):
+        msg = _('http auth: user %s, password %s\n')
+        self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
+
+    def _readauthtoken(self, uri):
+        # Read configuration
+        config = dict()
+        for key, val in self.ui.configitems('auth'):
+            group, setting = key.split('.', 1)
+            gdict = config.setdefault(group, dict())
+            gdict[setting] = val
+
+        # Find the best match
+        scheme, hostpath = uri.split('://', 1)
+        bestlen = 0
+        bestauth = None, None
+        for auth in config.itervalues():
+            prefix = auth.get('prefix')
+            if not prefix: continue
+            p = prefix.split('://', 1)
+            if len(p) > 1:
+                schemes, prefix = [p[0]], p[1]
+            else:
+                schemes = (auth.get('schemes') or 'https').split()
+            if (prefix == '*' or hostpath.startswith(prefix)) and \
+                len(prefix) > bestlen and scheme in schemes:
+                bestlen = len(prefix)
+                bestauth = auth.get('username'), auth.get('password')
+        return bestauth
+
 class proxyhandler(urllib2.ProxyHandler):
     def __init__(self, ui):
         proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgweb-auth.py	Mon May 04 20:26:27 2009 +0200
@@ -0,0 +1,61 @@
+from mercurial import demandimport; demandimport.enable()
+from mercurial import ui
+from mercurial import url
+from mercurial.error import Abort
+
+class myui(ui.ui):
+    def interactive(self):
+        return False
+
+origui = myui()
+
+def writeauth(items):
+    ui = origui.copy()
+    for name, value in items.iteritems():
+        ui.setconfig('auth', name, value)
+    return ui
+
+def dumpdict(dict):
+    return '{' + ', '.join(['%s: %s' % (k, dict[k]) for k in sorted(dict.iterkeys())]) + '}'
+
+def test(auth):
+    print 'CFG:', dumpdict(auth)
+    prefixes = set()
+    for k in auth:
+        prefixes.add(k.split('.', 1)[0])
+    for p in prefixes:
+        auth.update({p + '.username': p, p + '.password': p})
+
+    ui = writeauth(auth)
+
+    def _test(uri):
+        print 'URI:', uri
+        try:
+            pm = url.passwordmgr(ui)
+            print '    ', pm.find_user_password('test', uri)
+        except Abort, e:
+            print 'abort'
+
+    _test('http://example.org/foo')
+    _test('http://example.org/foo/bar')
+    _test('http://example.org/bar')
+    _test('https://example.org/foo')
+    _test('https://example.org/foo/bar')
+    _test('https://example.org/bar')
+
+
+print '\n*** Test in-uri schemes\n'
+test({'x.prefix': 'http://example.org'})
+test({'x.prefix': 'https://example.org'})
+test({'x.prefix': 'http://example.org', 'x.schemes': 'https'})
+test({'x.prefix': 'https://example.org', 'x.schemes': 'http'})
+
+print '\n*** Test separately configured schemes\n'
+test({'x.prefix': 'example.org', 'x.schemes': 'http'})
+test({'x.prefix': 'example.org', 'x.schemes': 'https'})
+test({'x.prefix': 'example.org', 'x.schemes': 'http https'})
+
+print '\n*** Test prefix matching\n'
+test({'x.prefix': 'http://example.org/foo', 'y.prefix': 'http://example.org/bar'})
+test({'x.prefix': 'http://example.org/foo', 'y.prefix': 'http://example.org/foo/bar'})
+test({'x.prefix': '*', 'y.prefix': 'https://example.org/bar'})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgweb-auth.py.out	Mon May 04 20:26:27 2009 +0200
@@ -0,0 +1,139 @@
+
+*** Test in-uri schemes
+
+CFG: {x.prefix: http://example.org}
+URI: http://example.org/foo
+     ('x', 'x')
+URI: http://example.org/foo/bar
+     ('x', 'x')
+URI: http://example.org/bar
+     ('x', 'x')
+URI: https://example.org/foo
+     abort
+URI: https://example.org/foo/bar
+     abort
+URI: https://example.org/bar
+     abort
+CFG: {x.prefix: https://example.org}
+URI: http://example.org/foo
+     abort
+URI: http://example.org/foo/bar
+     abort
+URI: http://example.org/bar
+     abort
+URI: https://example.org/foo
+     ('x', 'x')
+URI: https://example.org/foo/bar
+     ('x', 'x')
+URI: https://example.org/bar
+     ('x', 'x')
+CFG: {x.prefix: http://example.org, x.schemes: https}
+URI: http://example.org/foo
+     ('x', 'x')
+URI: http://example.org/foo/bar
+     ('x', 'x')
+URI: http://example.org/bar
+     ('x', 'x')
+URI: https://example.org/foo
+     abort
+URI: https://example.org/foo/bar
+     abort
+URI: https://example.org/bar
+     abort
+CFG: {x.prefix: https://example.org, x.schemes: http}
+URI: http://example.org/foo
+     abort
+URI: http://example.org/foo/bar
+     abort
+URI: http://example.org/bar
+     abort
+URI: https://example.org/foo
+     ('x', 'x')
+URI: https://example.org/foo/bar
+     ('x', 'x')
+URI: https://example.org/bar
+     ('x', 'x')
+
+*** Test separately configured schemes
+
+CFG: {x.prefix: example.org, x.schemes: http}
+URI: http://example.org/foo
+     ('x', 'x')
+URI: http://example.org/foo/bar
+     ('x', 'x')
+URI: http://example.org/bar
+     ('x', 'x')
+URI: https://example.org/foo
+     abort
+URI: https://example.org/foo/bar
+     abort
+URI: https://example.org/bar
+     abort
+CFG: {x.prefix: example.org, x.schemes: https}
+URI: http://example.org/foo
+     abort
+URI: http://example.org/foo/bar
+     abort
+URI: http://example.org/bar
+     abort
+URI: https://example.org/foo
+     ('x', 'x')
+URI: https://example.org/foo/bar
+     ('x', 'x')
+URI: https://example.org/bar
+     ('x', 'x')
+CFG: {x.prefix: example.org, x.schemes: http https}
+URI: http://example.org/foo
+     ('x', 'x')
+URI: http://example.org/foo/bar
+     ('x', 'x')
+URI: http://example.org/bar
+     ('x', 'x')
+URI: https://example.org/foo
+     ('x', 'x')
+URI: https://example.org/foo/bar
+     ('x', 'x')
+URI: https://example.org/bar
+     ('x', 'x')
+
+*** Test prefix matching
+
+CFG: {x.prefix: http://example.org/foo, y.prefix: http://example.org/bar}
+URI: http://example.org/foo
+     ('x', 'x')
+URI: http://example.org/foo/bar
+     ('x', 'x')
+URI: http://example.org/bar
+     ('y', 'y')
+URI: https://example.org/foo
+     abort
+URI: https://example.org/foo/bar
+     abort
+URI: https://example.org/bar
+     abort
+CFG: {x.prefix: http://example.org/foo, y.prefix: http://example.org/foo/bar}
+URI: http://example.org/foo
+     ('x', 'x')
+URI: http://example.org/foo/bar
+     ('y', 'y')
+URI: http://example.org/bar
+     abort
+URI: https://example.org/foo
+     abort
+URI: https://example.org/foo/bar
+     abort
+URI: https://example.org/bar
+     abort
+CFG: {x.prefix: *, y.prefix: https://example.org/bar}
+URI: http://example.org/foo
+     abort
+URI: http://example.org/foo/bar
+     abort
+URI: http://example.org/bar
+     abort
+URI: https://example.org/foo
+     ('x', 'x')
+URI: https://example.org/foo/bar
+     ('x', 'x')
+URI: https://example.org/bar
+     ('y', 'y')