Henrik Stuart avatar Henrik Stuart committed 697daa3

Moved management routines to hg and added Python 2.4 support

Comments (0)

Files changed (2)

 Mercurial behind a web server that handles authentication, e.g. when running
 `hg serve`.
 
-The first step will be to run `python textauth.py` and get usage information
-and see how it is used to create the authentication file.
+The first step is to modify your repository's hgrc file (or possibly
+hgweb.config file if serving with hgwebdir) and add the ``textauth.file``.
 
-The second step is to add it to your repository's hgrc file (or if you're
-serving with hgwebdir to the hgweb.config file, unless you want different
-authentication files for different repositories).
+The second step is to run `hg authedit` and get usage information to see
+how it is used to create/modify the authentication file.
 
 Finally, you can run `hg serve` as normal, and credentials will automatically
 be requested when browsing the repository using the web browser, as well as
     user7:$sha512$e9670bc9fb8d3a578736c31390205b1d9fef50adf941b3b349076d27e483e42b00c38998cac4370a362ffe5966d45012b95f8eb279b2cecfe1760cdf25d5ecca
 
 This extension also doubles as a management interface to this file that can be
-launched by executing the file through python::
+used through Mercurial::
 
-    python textauth.py
+    hg authedit
 
-This will display usage details of working with it. Its interface greatly
-mirrors that of htpasswd, but its storage format is markedly different.
+This will display usage details of working with it. Its interface mirrors that
+of htpasswd somewhat, but its storage format is markedly different.
 
 '''
 
-import hashlib
-
-algos = {
-        'plain': lambda x: '$plain$%s' % (x,),
-        'md5': lambda x: '$md5$%s' % (hashlib.md5(x).hexdigest(),),
-        'sha1': lambda x: '$sha1$%s' % (hashlib.sha1(x).hexdigest(),),
-        'sha224': lambda x: '$sha224$%s' % (hashlib.sha224(x).hexdigest(),),
-        'sha256': lambda x: '$sha256$%s' % (hashlib.sha256(x).hexdigest(),),
-        'sha384': lambda x: '$sha384$%s' % (hashlib.sha384(x).hexdigest(),),
-        'sha512': lambda x: '$sha512$%s' % (hashlib.sha512(x).hexdigest(),),
-        }
-
-if __name__ == '__main__':
-    import sys
-    import os
-    import getpass
-    from optparse import OptionParser
-
-    parser = OptionParser(usage='usage: %prog [options] PASSWORDFILE USERNAME')
-    parser.add_option('-p', '--plain', help='write plaintext password',
-            default=False, action='store_true')
-    parser.add_option('-m', '--md5', help='write password in md5 digest format',
-            default=False, action='store_true')
-    parser.add_option('', '--sha1', help='write password in sha1 format',
-            default=False, action='store_true')
-    parser.add_option('', '--sha224', help='write password in sha224 format',
-            default=False, action='store_true')
-    parser.add_option('', '--sha256', help='write password in sha256 format',
-            default=False, action='store_true')
-    parser.add_option('', '--sha384', help='write password in sha384 format',
-            default=False, action='store_true')
-    parser.add_option('', '--sha512', action='store_true',
-            help='write password in sha512 format (default)', default=False)
-    parser.add_option('-c', '--create', help='create password if it does not '
-            'exist', default=False, action='store_true')
-    parser.add_option('-d', '--delete', help='delete specified user from file',
-            default=False, action='store_true')
-
-    (options, args) = parser.parse_args()
-
-    algo = [None]
-
-    def _exit_if_set(new_algo):
-        if algo[0]:
-            sys.stderr.write('Cannot specify both %s and md5. exiting.\n' %
-                    (algo[0],))
-            sys.exit(1)
-        algo[0] = new_algo
-
-    if options.plain: _exit_if_set('plain')
-    if options.md5: _exit_if_set('md5')
-    if options.sha1: _exit_if_set('sha1')
-    if options.sha224: _exit_if_set('sha224')
-    if options.sha256: _exit_if_set('sha256')
-    if options.sha384: _exit_if_set('sha384')
-    if options.sha512: _exit_if_set('sha512')
-
-    algo = algo[0]
-    if not algo:
-        algo = 'sha512'
-
-    if len(args) != 2:
-        parser.print_help()
-        sys.exit(1)
-
-    fname = args[0]
-    uname = args[1]
-
-    if ':' in uname:
-        sys.stderr.write('username may not contain :\n')
-        sys.exit(1)
-
-    if not options.create and not os.path.exists(fname):
-        sys.stderr.write('use -c to create password file\n')
-        sys.exit(1)
-
-    users = set()
-    entries = []
-    if os.path.exists(fname):
-        with open(fname, 'r') as fh:
-            for l in fh:
-                l = l.strip()
-                if not l:
-                    continue
-                dbuname, dbphash = l.split(':', 1)
-                entries.append((dbuname, dbphash))
-                users.add(dbuname)
-
-    if uname in users and not options.delete:
-        sys.stderr.write('user already in file. aborting.\n')
-        sys.exit(1)
-    elif uname not in users and options.delete:
-        sys.stderr.write('user not in file. aborting.\n')
-        sys.exit(1)
-
-    if options.delete:
-        with open(fname, 'w') as fh:
-            for dbuser, dbphash in entries:
-                if dbuser == uname:
-                    continue
-                fh.write('%s:%s\n' % (dbuser, dbphash))
-    else:
-        with open(fname, 'a') as fh:
-            passwd = getpass.getpass('Enter password:')
-            verify = getpass.getpass('Verify password:')
-            if passwd != verify:
-                sys.stderr.write('password does not verify.\n')
-                sys.exit(1)
-
-            fh.write('%s:%s\n' % (uname, algos[algo](passwd)))
-
-    sys.exit(0)
-
+import getpass
 import base64
 import re
 import os.path
 
+from mercurial.i18n import _
 from mercurial.hgweb import common
 
 def perform_authentication(hgweb, req, op):
+    '''entry-point for hgweb to do HTTP basic authentication'''
+
     def fail(msg):
         raise common.ErrorResponse(common.HTTP_UNAUTHORIZED, msg,
                 [('WWW-Authenticate', 'Basic Realm="mercurial"')])
                 if algo is None:
                     raise common.ErrorResponse(common.HTTP_SERVER_ERROR,
                             'invalid authentication file on server')
-                if algos[algo](passwd) == dbpasswd:
+                algos = _gethashes()
+                if algos[algo](passwd) == rdbpasswd:
                     req.env['REMOTE_USER'] = user
                     return
                 else:
     finally:
         f.close()
 
+def _gethashes():
+    hashes = {'plain': lambda x: x}
+
+    try:
+        import hashlib
+        hashes['md5'] = lambda x: hashlib.md5(x).hexdigest()
+        hashes['sha1'] = lambda x: hashlib.sha1(x).hexdigest()
+        hashes['sha224'] = lambda x: hashlib.sha224(x).hexdigest()
+        hashes['sha256'] = lambda x: hashlib.sha256(x).hexdigest()
+        hashes['sha384'] = lambda x: hashlib.sha384(x).hexdigest()
+        hashes['sha512'] = lambda x: hashlib.sha512(x).hexdigest()
+    except ImportError, AttributeError:
+        import md5
+        import sha
+        hashes['md5'] = lambda x: md5.new(x).hexdigest()
+        hashes['sha1'] = lambda x: sha.new(x).hexdigest()
+
+    return hashes
+
+def _map_read_db(authfile, func):
+    fh = open(authfile, 'r')
+    try:
+        for l in fh:
+            l = l.strip()
+            if not l:
+                continue
+            func(l)
+        pass
+    finally:
+        fh.close()
+
+def _read_db(authfile):
+    entries = {}
+    _map_read_db(authfile, lambda l: entries.update((l.split(':', 1),)))
+    return entries
+
+def _read_db_ordered(authfile):
+    entries = []
+    _map_read_db(authfile, lambda l: entries.append(l.split(':', 1)))
+    return entries
+
+def authedit(ui, repo, username=None, **opts):
+    '''Management routines for working with the authentication file
+
+    Earlier versions of Python only support plain, md5 and sha1 hashes. Using
+    a newer hash will result in an error being printed. Stored hashes using
+    newer hashes than available will cause a 403 forbidden at the time of
+    authentication.
+    '''
+
+    authfile = ui.config('textauth', 'file')
+    if not authfile:
+        ui.warn(_('no authentication file has been specified\n'))
+        return 1
+
+    authfile = os.path.expanduser(authfile)
+
+    if opts.get('list'):
+        users = []
+        if os.path.exists(authfile):
+            fh = open(authfile, 'r')
+            try:
+                for l in fh:
+                    l = l.strip()
+                    if not l:
+                        continue
+                    if ui.debugflag:
+                        users.append(l)
+                    else:
+                        user, hash = l.split(':', 1)
+                        users.append(user)
+            finally:
+                fh.close()
+
+        if not users:
+            ui.status(_('no users defined\n'))
+        else:
+            for user in users:
+                ui.write('%s\n' % (user,))
+
+        return 0
+
+    if not os.path.exists(authfile):
+        if opts.get('delete'):
+            ui.warn(_('authentication file does not exist, cannot delete '
+                'user\n'))
+        elif not opts.get('create'):
+            ui.warn(_('authentication file does not exist, use --create to '
+                'create it\n'))
+
+    if username is None:
+        ui.warn(_('no username specified. aborting.\n'))
+        return 1
+
+    if opts.get('delete'):
+        entries = _read_db_ordered(authfile)
+
+        found = False
+        for user, pw in entries:
+            if user == username:
+                found = True
+                break
+
+        if not found:
+            ui.warn(_('no such user in file\n'))
+            return 1
+
+        fh = open(authfile, 'w')
+        try:
+            for user, pw in entries:
+                if user != username:
+                    fh.write('%s:%s\n' % (user, pw))
+        finally:
+            fh.close()
+
+        return 0
+
+    db = _read_db(authfile)
+    if username in db:
+        ui.warn(_('user already exists in authentication file. please remove '
+            'before adding a new password.\n'))
+        return 1
+
+    hashes = _gethashes()
+
+    hash = opts.get('hash')
+    if not hash:
+        if 'sha512' in hashes:
+            hash = 'sha512'
+        else:
+            hash = 'sha1'
+    else:
+        hash = hash.lower()
+
+    if hash not in hashes:
+        ui.warn(_('unknown or unsupported hash specified (select one of: %s)'
+            '.\n' % (', '.join(sorted(hashes)))))
+        return 1
+
+    passwd = getpass.getpass('Enter password: ')
+    verify = getpass.getpass('Verify password: ')
+
+    if passwd != verify:
+        ui.warn(_('passwords do not match\n'))
+        return 1
+
+    storepw = hashes[hash](passwd)
+
+    fh = open(authfile, 'a')
+    try:
+        fh.write('%s:$%s$%s\n' % (username, hash, storepw))
+    finally:
+        fh.close()
+
 def extsetup():
     common.permhooks.insert(0, perform_authentication)
 
-cmdtable = {}
+cmdtable = {
+        'authedit': (authedit, [
+            ('h', 'hash', '', 
+                _('the hash to use (plain, md5, sha1, sha224, sha256, '
+                'sha384, sha512)\n')),
+            ('c', 'create', False,
+                _('create authentication file if it does not exist')),
+            ('d', 'delete', False,
+                _('delete the user entry rather than add it')),
+            ('l', 'list', False,
+                _('list the users approved for authentication')),
+            ], _('[-h HASH] [-cdl] USERNAME')),
+        }
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.