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 |