[PATCH 1 of 1] kerberosauth: support for Kerberos authentication
Henrik Stuart
hg at hstuart.dk
Fri Aug 7 16:15:39 UTC 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