1 #!/usr/bin/env python |
1 #!/usr/bin/env python |
2 # |
2 # |
3 # Copyright 2008 LShift Ltd |
3 # Copyright 2008 LShift Ltd |
4 # Modified by Paul Crowley <paul@lshift.net> |
|
5 # with ideas from Mathieu PASQUET <kiorky@cryptelium.net> |
|
6 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de> |
4 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de> |
7 # Author(s): |
5 # Author(s): |
|
6 # Paul Crowley <paul@lshift.net> |
8 # Thomas Arendsen Hein <thomas@intevation.de> |
7 # Thomas Arendsen Hein <thomas@intevation.de> |
|
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 |
11 # of the GNU General Public License, incorporated herein by reference. |
11 # of the GNU General Public License, incorporated herein by reference. |
12 |
12 |
13 """ |
13 """ |
14 hg-ssh - a wrapper for ssh access to a limited set of mercurial repos |
14 hg-ssh - limit access to hg repositories reached via ssh. Part of hg-admin-tools. |
15 |
15 |
16 DO NOT USE WITHOUT READING THE SOURCE! In particular note that we HARDWIRE |
16 This script is called by hg-ssh-wrapper with two arguments: |
17 the path to hgadmin/hg-ssh-access.conf. |
|
18 |
17 |
19 To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8): |
18 hg-ssh <rulefile> <keyname> |
20 command="hg-ssh keyid" ssh-dss ... |
|
21 (probably together with these other useful options: |
|
22 no-port-forwarding,no-X11-forwarding,no-agent-forwarding) |
|
23 |
19 |
24 This allows pull/push over ssh to to the repositories given as arguments. |
20 It expects to find the command the SSH user was trying to run in the environment variable |
25 |
21 SSH_ORIGINAL_COMMAND, and uses it 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. |
26 If all your repositories are subdirectories of a common directory, you can |
|
27 allow shorter paths with: |
|
28 command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2" |
|
29 |
|
30 You can use pattern matching of your normal shell, e.g.: |
|
31 command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}" |
|
32 """ |
22 """ |
33 |
23 |
34 # enable importing on demand to reduce startup time |
24 # enable importing on demand to reduce startup time |
35 from mercurial import demandimport; demandimport.enable() |
25 from mercurial import demandimport; demandimport.enable() |
36 |
26 |
47 goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars)) |
37 goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars)) |
48 def goodpath(path): |
38 def goodpath(path): |
49 if goodpathre.match(path) is None: |
39 if goodpathre.match(path) is None: |
50 fail("Disallowing path: %s" % path) |
40 fail("Disallowing path: %s" % path) |
51 |
41 |
52 |
|
53 # Don't put anything except *A-Za-z0-9_- in rule globs or |
42 # Don't put anything except *A-Za-z0-9_- in rule globs or |
54 # you'll probably break security. No regexp metachars, not even . |
43 # you'll probably break security. No regexp metachars, not even . |
55 # We may fix this later. |
44 # We may fix this later. |
56 goodglobre = re.compile("[*/%s]+$" % allowedchars) |
45 goodglobre = re.compile("[*/%s]+$" % allowedchars) |
57 def globmatch(pattern, match): |
46 def globmatch(pattern, match): |
59 fail("Bad glob pattern in auth config: %s" % pattern) |
48 fail("Bad glob pattern in auth config: %s" % pattern) |
60 pattern = pattern.replace(".", r'\.') |
49 pattern = pattern.replace(".", r'\.') |
61 pattern = pattern.replace("*", "[%s]*" % allowedchars) |
50 pattern = pattern.replace("*", "[%s]*" % allowedchars) |
62 return re.compile(pattern + "$").match(match) is not None |
51 return re.compile(pattern + "$").match(match) is not None |
63 |
52 |
64 def testrule(keyname, path, applicable): |
53 def testrule(rulefile keyname, path, applicable): |
65 goodpath(keyname) |
54 goodpath(keyname) |
66 goodpath(path) |
55 goodpath(path) |
67 f = open("hgadmin/hg-ssh-access.conf") |
56 f = open(rulefile) |
68 try: |
57 try: |
69 for l in f: |
58 for l in f: |
70 l = l.strip() |
59 l = l.strip() |
71 if l == "" or l.startswith("#"): |
60 if l == "" or l.startswith("#"): |
72 continue |
61 continue |
76 return rule in applicable |
65 return rule in applicable |
77 return False |
66 return False |
78 finally: |
67 finally: |
79 f.close() |
68 f.close() |
80 |
69 |
81 def get_cmd(keyname, cmd): |
70 def get_cmd(rulefile, keyname, cmd): |
82 if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): |
71 if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): |
83 path = cmd[6:-14] |
72 path = cmd[6:-14] |
84 if testrule(keyname, path, set(["allow", "init"])): |
73 if testrule(rulefile, keyname, path, set(["allow", "init"])): |
85 return ['-R', path, 'serve', '--stdio'] |
74 return ['-R', path, 'serve', '--stdio'] |
86 elif cmd.startswith('hg init '): |
75 elif cmd.startswith('hg init '): |
87 path = cmd[8:] |
76 path = cmd[8:] |
88 if testrule(keyname, path, set(["init"])): |
77 if testrule(rulefile, keyname, path, set(["init"])): |
89 return ['init', path] |
78 return ['init', path] |
90 fail("Illegal command %r" % cmd) |
79 fail("Illegal command %r" % cmd) |
91 |
80 |
92 #logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w") |
81 #logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w") |
93 #logfile.write("Started: %s\n" % sys.argv) |
82 #logfile.write("Started: %s\n" % sys.argv) |
94 |
83 |
95 if len(sys.argv) != 2: |
84 if len(sys.argv) != 3: |
96 fail("Error in hg-ssh configuration in .ssh/authorized_keys: too many arguments (%s)" |
85 fail("hg-ssh must have exactly two arguments (%s)" |
97 % sys.argv) |
86 % sys.argv) |
98 |
87 |
99 keyname = sys.argv[1] |
88 rulefile = sys.argv[1] |
100 todispatch = get_cmd(keyname, os.environ.get('SSH_ORIGINAL_COMMAND', '?')) |
89 keyname = sys.argv[2] |
101 os.environ["HG_ACL_USER"] = keyname |
90 todispatch = get_cmd(rulefile, keyname, os.environ.get('SSH_ORIGINAL_COMMAND', '?')) |
102 dispatch.dispatch(todispatch) |
91 dispatch.dispatch(todispatch) |
103 |
92 |