Much less strict about most things
authorPaul Crowley <paul@lshift.net>
Mon, 12 Oct 2009 16:04:07 +0100
changeset 106 0519745e7a57
parent 105 cd3da73cdf63
child 107 84e9e33d866b
Much less strict about most things
src/hg-ssh
src/mercurialserver/refreshauth.py
src/mercurialserver/ruleset.py
--- a/src/hg-ssh	Mon Oct 12 12:08:49 2009 +0100
+++ b/src/hg-ssh	Mon Oct 12 16:04:07 2009 +0100
@@ -33,40 +33,40 @@
 
 from mercurial import dispatch
 
-import sys, os
+import sys, os, os.path
 from mercurialserver import ruleset, paths
 
 def fail(message):
-    #logfile.write("Fail: %s\n" % message)
-    sys.stderr.write(message + "\n")
+    sys.stderr.write("mercurial-server: %s\n" % message)
     sys.exit(-1)
 
-def getpath(path):
-    path = path.rstrip("/")
-    if not ruleset.goodpath(path):
-        fail("Disallowing path: %s" % path)
-    return path
+def checkpath(path)
+    path = os.path.dirname(path)
+    if path == "":
+        return
+    if os.path.exists(path + "/.hg"):
+        raise ruleset.AccessException()
+    checkpath(path)
 
-def try_cmd(cmd):
-    if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'):
-        repo = getpath(cmd[6:-14])
-        ruleset.rules.set(repo=repo)
-        if ruleset.rules.allow("read", branch=None, file=None):
-            dispatch.dispatch(['-R', repo, 'serve', '--stdio'])
-            return
-    elif cmd.startswith('hg init '):
-        repo = getpath(cmd[8:])
-        ruleset.rules.set(repo=repo)
-        if ruleset.rules.allow("init", branch=None, file=None):
-            dispatch.dispatch(['init', repo])
-            return
-    fail("Illegal command %r" % cmd)
+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)
 
 if len(sys.argv) != 2:
-    fail("hg-ssh must have exactly one argument (%s)" 
+    fail("hg-ssh wrongly called, is authorized_keys corrupt? (%s)" 
         % sys.argv)
 
 paths.setExePath()
@@ -85,4 +85,25 @@
     if os.path.isfile(f):
         ruleset.rules.readfile(f)
 
-try_cmd(os.environ.get('SSH_ORIGINAL_COMMAND', '?'))
+cmd = os.environ.get('SSH_ORIGINAL_COMMAND', '')
+try:
+    if 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])
+    elif cmd == "":
+        fail("direct logins on the hg account prohibited ")
+    else:
+        fail("illegal command %r" % cmd)
+except ruleset.AccessException:
+    fail("access denied")
+
--- a/src/mercurialserver/refreshauth.py	Mon Oct 12 12:08:49 2009 +0100
+++ b/src/mercurialserver/refreshauth.py	Mon Oct 12 16:04:07 2009 +0100
@@ -5,12 +5,14 @@
 # file every time it is run
 # WARNING
 
-import sys
+import re
 import os
 import os.path
 import pwd
 import subprocess
-from mercurialserver import ruleset, paths
+from mercurialserver import paths
+
+goodkey = re.compile("[A-Za-z0-9._-]+$")
 
 def refreshAuth(pw_dir):
     akeyfile = pw_dir + "/.ssh/authorized_keys"
@@ -38,9 +40,10 @@
                     raise Exception("Inconsistent behaviour in os.walk, bailing")
                 #print "Processing file", ffn
                 keyname = ffn[len(kr):]
-                if not ruleset.goodpath(keyname):
+                # FIXME: still too strict
+                if not goodkey.match(keyname)
                     # ignore any path that contains dodgy characters
-                    #print "Ignoring file", ffn
+                    print "Ignoring key that contains banned character:", ffn
                     continue
                 p = subprocess.Popen(("ssh-keygen", "-i", "-f", ffn), 
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
--- a/src/mercurialserver/ruleset.py	Mon Oct 12 12:08:49 2009 +0100
+++ b/src/mercurialserver/ruleset.py	Mon Oct 12 16:04:07 2009 +0100
@@ -12,28 +12,10 @@
 
 allowedchars = "A-Za-z0-9_-"
 
-goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars))
-def goodpath(path):
-    return goodpathre.match(path) is not None
-
-goodglobre = re.compile("[*/%s]+$" % allowedchars)
-
-def goodglob(pattern):
-    return goodglobre.match(pattern) is not None
-
-# Don't put anything except *A-Za-z0-9_- in rule globs or   
-# it will match nothing.  No regexp metachars, not even .
-# We may fix this later.
 def globmatcher(pattern):
-    if not goodglob(pattern):
-        #fail("Bad glob pattern in auth config: %s" % pattern)
-        # FIXME: report it somehow
-        return lambda x: False
-    # Substitution cunning so ** can be different from *
-    pattern = pattern.replace("*", "[]")
-    pattern = pattern.replace("[][]", "[/%s]*" % allowedchars)
-    pattern = pattern.replace("[]", "[%s]*" % allowedchars)
-    rex = re.compile(pattern + "$")
+    p = "[^/]*".join(re.escape(c) for c in pattern.split("*"))
+    # ** means "match recursively" ie "ignore directories"
+    rex = re.compile(p.replace("[^/]*[^/]*", ".*") + "$")
     # None matches everything
     return lambda x: x is None or rex.match(x) is not None
 
@@ -46,6 +28,9 @@
         return True
     return c
 
+class AccessException(Exception):
+    pass
+
 class Ruleset(object):
     '''Class representing the rules in a rule file'''
     
@@ -76,6 +61,10 @@
         a = self.matchrule(kw)
         return a in self.levels and self.levels.index(a) <= self.levels.index(level)
     
+    def check(self, level, **kw):
+        if not self.allow(level, **kw):
+            raise AccessException()
+    
     def readfile(self, fn):
         try:
             f = open(fn)