[PATCH 2 of 2 v2] hg-ssh: more flexible permissions for hg-ssh

David Schleimer dschleimer at fb.com
Mon May 21 18:29:25 CDT 2012


# HG changeset patch
# User David Schleimer <dschleimer at fb.com>
# Date 1337642370 25200
# Node ID 6be86d4b3a0c424272600164500b6329b43ab946
# Parent  b52b7fe0dd08b257dfc69c8a5de503cec94f4b76
hg-ssh: more flexible permissions for hg-ssh

This allows more flexible control over the permissions granted to a
ssh key when using hg-ssh as the command in an authorized_keys file.

Specifically, it allows you to restrict a key to read-only access, as
well as allowing you to grant a key access to any repo, instead of
needing to whitelist repos.

diff --git a/contrib/hg-ssh b/contrib/hg-ssh
--- a/contrib/hg-ssh
+++ b/contrib/hg-ssh
@@ -35,9 +35,21 @@
 
 def main():
     cwd = os.getcwd()
+    allrepos = False
+    readonly = False
+    args = sys.argv[1:]
+    while len(args):
+        if args[0] == '--all-repos':
+            allrepos = True
+            args.pop(0)
+        elif args[0] == '--read-only':
+            readonly = True
+            args.pop(0)
+        else:
+            break
     allowed_paths = [os.path.normpath(os.path.join(cwd,
                                                    os.path.expanduser(path)))
-                     for path in sys.argv[1:]]
+                     for path in args]
     orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
     try:
         cmdargv = shlex.split(orig_cmd)
@@ -48,10 +60,12 @@
     if cmdargv[:2] == ['hg', '-R'] and cmdargv[3:] == ['serve', '--stdio']:
         path = cmdargv[2]
         repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
-        if repo in allowed_paths:
-            dispatch.dispatch(dispatch.request(['-R', repo,
-                                                'serve',
-                                                '--stdio']))
+        if repo in allowed_paths or allrepos:
+            cmd = ['-R', repo, 'serve', '--stdio']
+            if readonly:
+                cmd += ['--config',
+                        'hooks.prechangegroup=python:__main__.rejectpush']
+            dispatch.dispatch(dispatch.request(cmd))
         else:
             sys.stderr.write('Illegal repository "%s"\n' % repo)
             sys.exit(255)
@@ -59,5 +73,11 @@
         sys.stderr.write('Illegal command "%s"\n' % orig_cmd)
         sys.exit(255)
 
+def rejectpush(ui, **kwargs):
+    ui.warn("Permission denied\n")
+    # mercurial hooks use unix process conventions for hook return values
+    # so a truthy return means failure
+    return True
+
 if __name__ == '__main__':
     main()
diff --git a/tests/test-ssh.t b/tests/test-ssh.t
--- a/tests/test-ssh.t
+++ b/tests/test-ssh.t
@@ -280,7 +280,7 @@
 
 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
-parameters:
+parameters to only allow a particular repo:
 
   $ cat > ssh.sh << EOF
   > userhost="\$1"
@@ -308,6 +308,62 @@
   Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation
   [255]
 
+Test hg-ssh in all repos mode:
+
+  $ cat > ssh.sh << EOF
+  > userhost="\$1"
+  > SSH_ORIGINAL_COMMAND="\$2"
+  > export SSH_ORIGINAL_COMMAND
+  > PYTHONPATH="$PYTHONPATH"
+  > export PYTHONPATH
+  > python "$TESTDIR/../contrib/hg-ssh" --all-repos
+  > EOF
+
+  $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
+  3fb238f49e8c
+
+  $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
+  remote: abort: There is no Mercurial repository here (.hg not found)!
+  abort: no suitable response from remote hg!
+  [255]
+
+  $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
+  remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
+  abort: no suitable response from remote hg!
+  [255]
+
+Test hg-ssh in read-only mode:
+
+  $ cat > ssh.sh << EOF
+  > userhost="\$1"
+  > SSH_ORIGINAL_COMMAND="\$2"
+  > export SSH_ORIGINAL_COMMAND
+  > PYTHONPATH="$PYTHONPATH"
+  > export PYTHONPATH
+  > python "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
+  > EOF
+
+  $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 5 changes to 4 files (+1 heads)
+  updating to branch default
+  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+  $ cd read-only-local
+  $ echo "baz" > bar
+  $ hg ci -A -m "unpushable commit" bar
+  $ hg push --ssh "sh ../ssh.sh"
+  pushing to ssh://user@dummy/$TESTTMP/remote
+  searching for changes
+  remote: Permission denied
+  remote: abort: prechangegroup hook failed
+  [1]
+
+  $ cd ..
+
   $ cat dummylog
   Got arguments 1:user at dummy 2:hg -R nonexistent serve --stdio
   Got arguments 1:user at dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio


More information about the Mercurial-devel mailing list