#!/usr/bin/env python## Copyright 2008 LShift Ltd# Modified by Paul Crowley <paul@lshift.net># with ideas from Mathieu PASQUET <kiorky@cryptelium.net># Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de># Author(s):# Thomas Arendsen Hein <thomas@intevation.de>## This software may be used and distributed according to the terms# of the GNU General Public License, incorporated herein by reference."""hg-ssh - a wrapper for ssh access to a limited set of mercurial reposDO NOT USE WITHOUT READING THE SOURCE! In particular note that we HARDWIREthe path to hgadmin/hg-ssh-access.conf.To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):command="hg-ssh keyid" ssh-dss ...(probably together with these other useful options: no-port-forwarding,no-X11-forwarding,no-agent-forwarding)This allows pull/push over ssh to to the repositories given as arguments.If all your repositories are subdirectories of a common directory, you canallow shorter paths with:command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"You can use pattern matching of your normal shell, e.g.:command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}""""# enable importing on demand to reduce startup timefrom mercurial import demandimport; demandimport.enable()from mercurial import dispatchimport sys, os, redef fail(message): #logfile.write("Fail: %s\n" % message) sys.stderr.write(message + "\n") sys.exit(-1)allowedchars = "A-Za-z0-9_.-"goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars))def goodpath(path): if goodpathre.match(path) is None: fail("Disallowing path: %s" % path)# Don't put anything except *A-Za-z0-9_- in rule globs or# you'll probably break security. No regexp metachars, not even .# We may fix this later.goodglobre = re.compile("[*/%s]+$" % allowedchars)def globmatch(pattern, match): if goodglobre.match(pattern) is None: fail("Bad glob pattern in auth config: %s" % pattern) pattern = pattern.replace(".", r'\.') pattern = pattern.replace("*", "[%s]*" % allowedchars) return re.compile(pattern + "$").match(match) is not Nonedef testrule(keyname, path, applicable): goodpath(keyname) goodpath(path) f = open("hgadmin/hg-ssh-access.conf") try: for l in f: l = l.strip() if l == "" or l.startswith("#"): continue rule, rk, rp = l.split() if globmatch(rk, keyname) and globmatch(rp, path): #logfile.write("Used rule: %s\n" % l) return rule in applicable return False finally: f.close()def get_cmd(keyname, cmd): if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): path = cmd[6:-14] if testrule(keyname, path, set(["allow", "init"])): return ['-R', path, 'serve', '--stdio'] elif cmd.startswith('hg init '): path = cmd[8:] if testrule(keyname, path, set(["init"])): return ['init', path] fail("Illegal command %r" % cmd)#logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w")#logfile.write("Started: %s\n" % sys.argv)if len(sys.argv) != 2: fail("Error in hg-ssh configuration in .ssh/authorized_keys: too many arguments (%s)" % sys.argv)keyname = sys.argv[1]todispatch = get_cmd(keyname, os.environ.get('SSH_ORIGINAL_COMMAND', '?'))os.environ["HG_ACL_USER"] = keynamedispatch.dispatch(todispatch)