[PATCH 1 of 1] kerberosauth: support for Kerberos authentication

Henrik Stuart hg at hstuart.dk
Fri Aug 7 11:15:39 CDT 2009


# HG changeset patch
# User Henrik Stuart <hg at hstuart.dk>
# Date 1249661668 -7200
# Node ID 99a4bd3e9da9587d68d44fedfa0d2f83390d9bf2
# Parent  19d07553d1b29f554b8478437a30b85a20312c5f
kerberosauth: support for Kerberos authentication

diff -r 19d07553d1b2 -r 99a4bd3e9da9 hgext/kerberosauth.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/kerberosauth.py	Fri Aug 07 18:14:28 2009 +0200
@@ -0,0 +1,270 @@
+# kerberos.py - Kerberos authentication for Mercurial
+#
+# Copyright 2009 by Henrik Stuart <hg at hstuart.dk>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+
+'''Kerberos authentication
+
+This extension adds Kerberos authentication to the default Mercurial
+handler capabilities. Only single request Kerberos authentication is
+supported due to urllib2's deficiencies in maintaining connections
+across multiple calls. This might impede cross-forest authentication
+in some cases.
+
+Kerberos authentication is controlled using the [auth] section in the
+hgrc file as follows::
+
+    [auth]
+    foo.prefix = hg.example.org
+    foo.username = foo
+    foo.domain = bar
+    foo.password = mypassword
+    foo.schemes = http https
+    foo.enable_kerberos = True
+    foo.spn = http/hg.example.org at QUUX
+    foo.realm = QUUX
+
+username
+    Optional. If specified, a logon process will take place using the
+    values specified in username, domain, and password. If an
+    interactive prompt is being used, the user will be queried for the
+    values. If no username is specified, the default user credentials
+    will be used.
+
+domain
+    Optional. Defines the realm to log on to if username is specified.
+    If no username is specified, this value is ignored.
+
+password
+    Optional. The password corresponding to the username at the
+    specified domain. If no password is specified, and a username is
+    specified, the password will be queried for, if an interactive
+    prompt is used.
+
+enable_kerberos
+    Optional. If this value is anything but True, no Kerberos
+    authentication will be attempted.
+
+spn
+    Optional. The service principal name of the target server. If no
+    spn is specified, the spn will be constructed using a forward and
+    reverse DNS query as follows:
+
+    - hostname -> ip
+    - ip -> reverse-hostname
+    - spn: http/reverse-hostname
+
+    If a realm is given, the spn will instead be:
+
+    - spn: http/reverse-hostname at realm
+
+    SPNs may be different from Kerberos library to Kerberos library.
+    The Windows API uses the form http/fqdn at REALM, while MIT Kerberos
+    uses HTTP at fqdn when performing authentication. Specifying the spn
+    manually will override the library default and potentially cause
+    errors.
+
+realm
+    Optional. The domain/realm the target server is a member of.
+
+Requirements:
+    PyKerberos or pywin32 (the latter only works on Windows).
+
+Pywin32:
+    Everything should work as intended.
+
+PyKerberos:
+    The wrapping of the GSSAPI provided by this module does not allow
+    one to specify any alternative user to use for a single
+    authentication handshake, thus it is up to the user to perform an
+    appropriate kinit (or corresponding) call before using Mercurial.
+    This also means that the auth variables: username, password, and
+    domain, are not supported under PyKerberos.
+
+Note:
+    The use of both pywin32 and PyKerberos rely on DNS to identify the
+    KDC. If both pywin32 and PyKerberos are installed, pywin32 will be
+    used.
+'''
+
+import base64
+import socket
+import urlparse
+
+from mercurial import url, util
+from mercurial.i18n import gettext as _
+from urllib2 import BaseHandler
+
+try:
+    from sspi import ClientAuth
+    import pywintypes
+
+    def generate_client_auth(ui, spn, authinfo):
+        try:
+            ca = ClientAuth('Kerberos', targetspn=spn, auth_info=authinfo)
+            c, cred = ca.authorize(None)
+            return base64.b64encode(cred[0].Buffer)
+        except pywintypes.error, inst:
+            raise util.Abort(inst[2])
+
+    def get_spn(ui, host, realm):
+        return 'http/%s%s' % (host, realm)
+
+    using_pywin32 = True
+except ImportError:
+    def generate_client_auth(ui, spn, authinfo):
+        ui.note(_('Kerberos authentication is not available. Skipping.\n'))
+
+    def get_spn(ui, host, realm):
+        ui.note(_('Kerberos authentication is not available. Generation of SPN will be skipped.\n'))
+
+    using_pywin32 = False
+
+if not using_pywin32:
+    try:
+        from kerberos import authGSSClientInit, authGSSClientStep
+        from kerberos import authGSSClientResponse, authGSSClientClean
+
+        def generate_client_auth(ui, spn, authinfo):
+            if authinfo:
+                ui.warn(_('PyKerberos does not support acquiring different credentials. Please use kinit to acquire alternative credentials.\n'))
+                return None
+
+            result, context = authGSSClientInit(spn)
+            try:
+                if result < 1:
+                    raise util.Abort(_('unable to perform Kerberos initialization'))
+
+                result = authGSSClientStep(context, '')
+                if result < 0:
+                    raise util.Abort(_('unable to perform Kerberos negotiation'))
+
+                return authGSSClientResponse(context)
+            finally:
+                authGSSClientClean(context)
+
+        def get_spn(ui, host, realm):
+            return 'HTTP@%s' % (host,)
+    except ImportError:
+        pass
+
+class AbstractKerberosHandler(object):
+    def __init__(self, ui, passmgr):
+        self.ui = ui
+        self.passmgr = passmgr
+        self.auth_info = None
+        self.retried = 0
+        
+    def http_error_auth_reqed(self, authreq, host, req, headers):
+        auth = headers.getheaders('WWW-Authenticate')
+        if not auth:
+            return
+        found = None
+        for a in auth:
+            type = a.split(' ', 1)[0]
+            if type in ['Negotiate', 'Kerberos']:
+                found = type
+                break
+
+        if found is None:
+            return
+
+        if self.auth_info:
+            auth_info = self.auth_info
+        else:
+            auth_info = self.passmgr.readauthtoken(host)
+
+        if not auth_info:
+            return
+        if not 'enable_kerberos' in auth_info:
+            return
+        if auth_info['enable_kerberos'].strip() != 'True':
+            return
+
+        if self.retried > 5:
+            raise HTTPError(req.get_full_url(), 401, 'Kerberos auth failed', headers, None)
+        else:
+            self.retried += 1
+
+        return self.retry_http_kerberos_auth(found, auth_info, host, req)
+
+    def reset_retry_count(self):
+        self.retried = 0
+
+    def get_spn(self, auth_info, host):
+        if auth_info and 'spn' in auth_info:
+            return auth_info['spn']
+
+        host = url.netlocsplit(urlparse.urlsplit(host)[1])[0]
+
+        candidates = socket.getaddrinfo(host, None)
+        candidate = candidates[0][4][0]
+
+        host = socket.gethostbyaddr(candidate)[0]
+
+        if auth_info and 'realm' in auth_info:
+            realm = '@%s' % auth_info['realm']
+        else:
+            realm = ''
+
+        return get_spn(self.ui, host, realm)
+
+    def get_user(self, auth_info):
+        if 'username' not in auth_info:
+            return None # default credentials
+
+        username = auth_info['username']
+        password = auth_info.get('password')
+        domain = auth_info.get('domain')
+
+        if not password or not domain:
+            if not self.ui.interactive():
+                raise util.Abort(_('http authorization required'))
+
+            if not domain:
+                domain = self.ui.prompt(_("domain for user %s:") %
+                        username, default=None)
+
+            if not password:
+                password = self.ui.getpass(_('password for user %s@%s: ') % 
+                        (username, domain))
+
+        auth_info['password'] = password
+        auth_info['domain'] = domain
+        self.auth_info = auth_info
+
+        return (username, domain, password)
+
+    def retry_http_kerberos_auth(self, type, auth_info, host, req):
+        ca = generate_client_auth(self.ui, self.get_spn(auth_info, host),
+                self.get_user(auth_info))
+        if not ca:
+            return None
+
+        auth = '%s %s' % (type, ca)
+        req.add_header(self.auth_header, auth)
+        return self.parent.open(req)
+
+class KerberosHandler(AbstractKerberosHandler, BaseHandler):
+    auth_header = 'Authorization'
+
+    def __init__(self, ui, passmgr):
+        AbstractKerberosHandler.__init__(self, ui, passmgr)
+
+    def http_error_401(self, req, fp, code, msg, headers):
+        url = req.get_full_url()
+        rv = self.http_error_auth_reqed('www-authenticate',
+                url, req, headers)
+        self.reset_retry_count()
+        return rv
+
+# monkey patching of url.py
+_add_handlers = url._add_handlers
+
+def add_handlers(ui, passmgr, handlers):
+    handlers.append(KerberosHandler(ui, passmgr))
+    _add_handlers(ui, passmgr, handlers)
+
+url._add_handlers = add_handlers
diff -r 19d07553d1b2 -r 99a4bd3e9da9 mercurial/url.py
--- a/mercurial/url.py	Wed Aug 05 22:52:35 2009 -0700
+++ b/mercurial/url.py	Fri Aug 07 18:14:28 2009 +0200
@@ -487,6 +487,9 @@
         authinfo = None
     return url, authinfo
 
+def _add_handlers(ui, pwmgr, handlers):
+    pass
+
 def opener(ui, authinfo=None):
     '''
     construct an opener suitable for urllib2
@@ -507,6 +510,7 @@
 
     handlers.extend((urllib2.HTTPBasicAuthHandler(passmgr),
                      httpdigestauthhandler(passmgr)))
+    _add_handlers(ui, passmgr, handlers)
     opener = urllib2.build_opener(*handlers)
 
     # 1.0 here is the _protocol_ version


More information about the Mercurial-devel mailing list