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

Dirkjan Ochtman dirkjan at ochtman.nl
Fri Aug 7 11:32:57 CDT 2009


On Fri, Aug 7, 2009 at 18:15, Henrik Stuart<hg at hstuart.dk> wrote:
> # HG changeset patch
> # User Henrik Stuart <hg at hstuart.dk>
> # Date 1249661668 -7200
> # Node ID 99a4bd3e9da9587d68d44fedfa0d2f83390d9bf2
> # Parent  19d07553d1b29f554b8478437a30b85a20312c5f
> kerberosauth: support for Kerberos authentication

I don't think we want this for hgext at this time, but I'd be happy to
push something that makes this easier in url.py!

Seems we'd want something nicer than what you've currently got,
though. How about adding a handlerfuncs = [] to url.py, your extension
does handlerfuncs.append(KerberosHandler), then the calling code can
do handlers.extend([h(ui, passmgr) for h in handlerfuncs])?

Cheers,

Dirkjan

>
> 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
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
>



More information about the Mercurial-devel mailing list