src/hg-ssh
author Paul Crowley <paul@lshift.net>
Mon, 12 Oct 2009 16:52:06 +0100
changeset 108 00b48d7bdfa0
parent 107 84e9e33d866b
child 109 72100d3ed1bd
permissions -rwxr-xr-x
Improve check on whether user supplied a command

#!/usr/bin/env python
#
# Copyright 2008-2009 LShift Ltd
# Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
# Authors:
# Paul Crowley <paul@lshift.net>
# Thomas Arendsen Hein <thomas@intevation.de>
# with ideas from  Mathieu PASQUET <kiorky@cryptelium.net>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

"""
hg-ssh - limit access to hg repositories reached via ssh.  Part of
mercurial-server.

This script is called by hg-ssh-wrapper with no arguments - everything
should be in enviroment variables:

HG_ACCESS_RULES_PATH identifies the paths to the rule files
REMOTE_USER the remote user (which is the key used by ssh)
SSH_ORIGINAL_COMMAND the command the user was trying to run

It uses SSH_ORIGINAL_COMMAND to determine what the user was trying to
do and to what repository, and then checks each rule in the rule file
in turn for a matching rule which decides what to do, defaulting to
disallowing the action.

"""

# enable importing on demand to reduce startup time
from mercurial import demandimport; demandimport.enable()

from mercurial import dispatch

import sys, os, os.path
import base64
from mercurialserver import ruleset, paths

def fail(message):
    sys.stderr.write("mercurial-server: %s\n" % message)
    sys.exit(-1)

def checkpath(path):
    path = os.path.dirname(path)
    if path == "":
        return
    if os.path.exists(path + "/.hg"):
        raise ruleset.AccessException()
    checkpath(path)

def getrepo(op, repo):
    repo = os.path.normcase(os.path.normpath(repo.rstrip("/")))
    if len(repo) == 0:
        fail("path to repository seems to be empty")
    if repo.startswith("/"):
        fail("absolute paths are not supported")
    for component in repo.split("/"):
        if component.startswith("."):
            fail("paths cannot contain dot file components")
    ruleset.rules.set(repo=repo)
    ruleset.rules.check(op, branch=None, file=None)
    checkpath(repo)
    return repo

#logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w")
#logfile.write("Started: %s\n" % sys.argv)

paths.setExePath()

if len(sys.argv) == 3 and sys.argv[1] == "--base64":
    ruleset.rules.set(user = base64.b64decode(sys.argv[2]))
elif len(sys.argv) == 2:
    ruleset.rules.set(user = sys.argv[1])
else:
    fail("hg-ssh wrongly called, is authorized_keys corrupt? (%s)" 
        % sys.argv)

# Use a different hgrc for remote pulls - this way you can set
# up access.py for everything at once without affecting local operations

os.environ['HGRCPATH'] = paths.getEtcPath() + "/remote-hgrc"

os.chdir('repos')

for f in [
    paths.getEtcPath() + "/access.conf", 
    os.getcwd() + "/hgadmin/access.conf"]:
    if os.path.isfile(f):
        ruleset.rules.readfile(f)

cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
try:
    if cmd is None:
        fail("direct logins on the hg account prohibited")
    elif cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'):
        repo = getrepo("read", cmd[6:-14])
        if not os.path.isdir(repo + "/.hg"):
            fail("no such repository %s" % repo)
        dispatch.dispatch(['-R', repo, 'serve', '--stdio'])
    elif cmd.startswith('hg init '):
        repo = getrepo("init", cmd[8:])
        if os.path.exists(repo):
            fail("%s exists" % repo)
        d = os.path.dirname(repo)
        if d != "" and not os.path.isdir(d):
            os.makedirs(d)
        dispatch.dispatch(['init', repo])
    else:
        fail("illegal command %r" % cmd)
except ruleset.AccessException:
    fail("access denied")