hg-ssh
changeset 18 538d6b198f4a
parent 15 f3654416d178
child 19 62ee928ac9b3
child 20 f4daa224dc7e
equal deleted inserted replaced
17:4c98440de851 18:538d6b198f4a
     1 #!/usr/bin/env python
     1 #!/usr/bin/env python
     2 #
     2 #
     3 # Copyright 2008 LShift Ltd
     3 # Copyright 2008 LShift Ltd
     4 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
     4 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
     5 # Author(s):
     5 # Authors:
     6 # Paul Crowley <paul@lshift.net>
     6 # Paul Crowley <paul@lshift.net>
     7 # Thomas Arendsen Hein <thomas@intevation.de>
     7 # Thomas Arendsen Hein <thomas@intevation.de>
     8 # with ideas from  Mathieu PASQUET <kiorky@cryptelium.net>
     8 # with ideas from  Mathieu PASQUET <kiorky@cryptelium.net>
     9 #
     9 #
    10 # This software may be used and distributed according to the terms
    10 # This software may be used and distributed according to the terms
    12 
    12 
    13 """
    13 """
    14 hg-ssh - limit access to hg repositories reached via ssh.  Part of
    14 hg-ssh - limit access to hg repositories reached via ssh.  Part of
    15 hg-admin-tools.
    15 hg-admin-tools.
    16 
    16 
    17 This script is called by hg-ssh-wrapper with two arguments:
    17 This script is called by hg-ssh-wrapper with no arguments - everything
       
    18 should be in enviroment variables:
    18 
    19 
    19 hg-ssh <rulefile> <keyname>
    20 HG_ACCESS_RULES_FILE identifies the path to the rules file
       
    21 REMOTE_USER the remote user (which is the key used by ssh)
       
    22 SSH_ORIGINAL_COMMAND the command the user was trying to run
    20 
    23 
    21 It expects to find the command the SSH user was trying to run in the
    24 It uses SSH_ORIGINAL_COMMAND to determine what the user was trying to
    22 environment variable SSH_ORIGINAL_COMMAND, and uses it to determine
    25 do and to what repository, and then checks each rule in the rule file
    23 what the user was trying to do and to what repository, and then checks
    26 in turn for a matching rule which decides what to do, defaulting to
    24 each rule in the rule file in turn for a matching rule which decides
    27 disallowing the action.
    25 what to do, defaulting to disallowing the action.
    28 
    26 """
    29 """
    27 
    30 
    28 # enable importing on demand to reduce startup time
    31 # enable importing on demand to reduce startup time
    29 from mercurial import demandimport; demandimport.enable()
    32 from mercurial import demandimport; demandimport.enable()
    30 
    33 
    31 from mercurial import dispatch
    34 from mercurial import dispatch
    32 
    35 
    33 import sys, os, re
    36 import sys, os
       
    37 import rules
    34 
    38 
    35 def fail(message):
    39 def fail(message):
    36     #logfile.write("Fail: %s\n" % message)
    40     #logfile.write("Fail: %s\n" % message)
    37     sys.stderr.write(message + "\n")
    41     sys.stderr.write(message + "\n")
    38     sys.exit(-1)
    42     sys.exit(-1)
    39 
    43 
    40 # Note that this currently disallows dots in path components - if you change it
    44 def getpath(path):
    41 # to allow them ensure that "." and ".." are disallowed in path components.
    45     if path.endswith("/"):
    42 allowedchars = "A-Za-z0-9_-"
    46         path = path[:-1]
    43 goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars))
    47     if not rules.goodpath(path):
    44 def goodpath(path):
       
    45     if goodpathre.match(path) is None:
       
    46         fail("Disallowing path: %s" % path)
    48         fail("Disallowing path: %s" % path)
       
    49     return path
    47 
    50 
    48 # Don't put anything except *A-Za-z0-9_- in rule globs or
    51 def get_cmd(rules, remoteuser, cmd):
    49 # you'll probably break security.  No regexp metachars, not even .
       
    50 # We may fix this later.
       
    51 goodglobre = re.compile("[*/%s]+$" % allowedchars)
       
    52 def globmatch(pattern, match):
       
    53     if goodglobre.match(pattern) is None:
       
    54         fail("Bad glob pattern in auth config: %s" % pattern)
       
    55     pattern = pattern.replace(".", r'\.')
       
    56     pattern = pattern.replace("*", "[%s]*" % allowedchars)
       
    57     return re.compile(pattern + "$").match(match) is not None
       
    58 
       
    59 def testrule(rulefile, keyname, path, applicable):
       
    60     goodpath(keyname)
       
    61     goodpath(path)
       
    62     f = open(rulefile)
       
    63     try:
       
    64         for l in f:
       
    65             l = l.strip()
       
    66             if l == "" or l.startswith("#"):
       
    67                 continue
       
    68             rule, rk, rp = l.split()
       
    69             if globmatch(rk, keyname) and globmatch(rp, path):
       
    70                 #logfile.write("Used rule: %s\n" % l)
       
    71                 return rule in applicable
       
    72     finally:
       
    73         f.close()
       
    74     return False
       
    75 
       
    76 def get_cmd(rulefile, keyname, cmd):
       
    77     if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'):
    52     if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'):
    78         path = cmd[6:-14]
    53         repo = getpath(cmd[6:-14])
    79         if testrule(rulefile, keyname, path, set(["allow", "init"])):
    54         if rules.allow("read", user=remoteuser, repo=repo, file=None):
    80             return ['-R', path, 'serve', '--stdio']
    55             os.environ["HG_REPO_PATH"] = repo
       
    56             return ['-R', repo, 'serve', '--stdio']
    81     elif cmd.startswith('hg init '):
    57     elif cmd.startswith('hg init '):
    82         path = cmd[8:]
    58         repo = getpath(cmd[8:])
    83         if testrule(rulefile, keyname, path, set(["init"])):
    59         if rules.allow("init", user=remoteuser, repo=repo, file=None):
    84             return ['init', path]
    60             os.environ["HG_REPO_PATH"] = repo
       
    61             return ['init', repo]
    85     fail("Illegal command %r" % cmd)
    62     fail("Illegal command %r" % cmd)
    86 
    63 
    87 #logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w")
    64 #logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w")
    88 #logfile.write("Started: %s\n" % sys.argv)
    65 #logfile.write("Started: %s\n" % sys.argv)
    89 
    66 
    90 if len(sys.argv) != 3:
    67 if len(sys.argv) != 1:
    91     fail("hg-ssh must have exactly two arguments (%s)" 
    68     fail("hg-ssh must have no arguments (%s)" 
    92         % sys.argv)
    69         % sys.argv)
    93 
    70 
    94 rulefile = sys.argv[1]
    71 rules = rules.Ruleset.readfile(os.environ['HG_ACCESS_RULES_FILE'])
    95 keyname = sys.argv[2]
    72 remoteuser = getpath(os.environ['REMOTE_USER'])
    96 todispatch = get_cmd(rulefile, keyname, 
    73 todispatch = get_cmd(rules, remoteuser, 
    97     os.environ.get('SSH_ORIGINAL_COMMAND', '?'))
    74     os.environ.get('SSH_ORIGINAL_COMMAND', '?'))
    98 dispatch.dispatch(todispatch)
    75 dispatch.dispatch(todispatch)
    99 
    76