src/hg-ssh
author Cédric Krier <ced@b2ck.com>
Sun, 14 Dec 2014 20:30:25 +0100
changeset 374 7a1d6b228af6
parent 306 f832a8aeef44
permissions -rwxr-xr-x
Set user as login name using a configuration index of the key path.

#!/usr/bin/env python

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

It is called by ssh due to an entry in the authorized_keys file,
with the name for the key passed on the command line.

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

try:
    request = dispatch.request
except AttributeError:
    request = list

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

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

config.initExe()

for k,v in config.getEnv():
    os.environ[k.upper()] = v

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

paths = []
path = user
while path:
    path, tail = os.path.split(path)
    paths.append(tail)
paths.reverse()
i = config.getUserPathIndex()
if i >= 0 and i < len(paths):
    user = paths[i]
os.environ['LOGNAME'] = user

os.chdir(config.getReposPath())

for f in config.getAccessPaths():
    if os.path.isfile(f):
        ruleset.rules.readfile(f)

alloweddots = config.getAllowedDots()

def dotException(pathtail):
    for ex in alloweddots:
        splex = ex.split("/")
        if len(pathtail) >= len(splex) and pathtail[:len(splex)] == splex:
            return True
    return False

def checkDots(path, pathtail = []):
    head, tail = os.path.split(path)
    pathtail = [tail] + pathtail
    if tail.startswith(".") and not dotException(pathtail):
            fail("paths cannot contain dot file components")
    if head:
        checkDots(head, pathtail)

def getrepo(op, repo):
    # First canonicalise, then check the string, then the rules
    repo = repo.strip().rstrip("/")
    if len(repo) == 0:
        fail("path to repository seems to be empty")
    if repo.startswith("/"):
        fail("absolute paths are not supported")
    checkDots(repo)
    ruleset.rules.set(repo=repo)
    if not ruleset.rules.allow(op, branch=None, file=None):
        fail("access denied")
    return repo

cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
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(request(['-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(request(['init', repo]))
else:
    fail("illegal command %r" % cmd)