Patchwork [14,of,14,"] hidden: add support to explicitly access hidden changesets with ssh peers

login
register
mail settings
Submitter Pierre-Yves David
Date April 13, 2019, 11:40 p.m.
Message ID <d044257bc1c60ce46d93.1555198844@nodosa.octopoid.net>
Download mbox | patch
Permalink /patch/39602/
State Accepted
Headers show

Comments

Pierre-Yves David - April 13, 2019, 11:40 p.m.
# HG changeset patch
# User Manuel Jacob <me@manueljacob.de>
# Date 1555119895 -7200
#      Sat Apr 13 03:44:55 2019 +0200
# Node ID d044257bc1c60ce46d93c4576316af6ad9ed9e6c
# Parent  8e2e24e9da81838ededb601dea208d5ed21a35f4
# EXP-Topic hgweb-obsolete
# Available At https://bitbucket.org/octobus/mercurial-devel/
#              hg pull https://bitbucket.org/octobus/mercurial-devel/ -r d044257bc1c6
hidden: add support to explicitly access hidden changesets with ssh peers

This implements support for using --remote-hidden with an ssh server. The
remote `hg serve --stdio` call is passed the `--hidden` flag as a request to
access hidden changesets.

This approach as benefit similar to the one we used for http peers. It:
* work around the lack of global parameters in protocol v1,
* reuse a flag we need for the command line (that do not use the wireproto),
* can be safely ignored by older client (fitting the best effort contract).

Same as for http, the feature is experimental so we have all the room we needs
to update the implementation in the future if deemed necessary.

The ssh version of the `--remote-hidden` config uses the same configuration than
the `http` support to control the access to this feature. The name of the user
running the command is used for the checking.

Test written by Pierre-Yves David.

Patch

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -67,6 +67,7 @@  from . import (
 )
 from .utils import (
     dateutil,
+    procutil,
     stringutil,
 )
 
@@ -5294,7 +5295,18 @@  def serve(ui, repo, **opts):
         if repo is None:
             raise error.RepoError(_("there is no Mercurial repository here"
                                     " (.hg not found)"))
-        s = wireprotoserver.sshserver(ui, repo)
+        accesshidden = False
+        if repo.filtername is None:
+            allow = ui.configlist('experimental', 'server.allow-hidden-access')
+            user = procutil.getuser()
+            if (allow and scmutil.ismember(ui, user, allow)):
+                accesshidden = True
+            else:
+                msg = _('ignoring request to access hidden changeset by '
+                        'unauthorized user: %s\n') % user
+                ui.warn(msg)
+
+        s = wireprotoserver.sshserver(ui, repo, accesshidden=accesshidden)
         s.serve_forever()
 
     service = server.createservice(ui, repo, opts)
diff --git a/mercurial/sshpeer.py b/mercurial/sshpeer.py
--- a/mercurial/sshpeer.py
+++ b/mercurial/sshpeer.py
@@ -154,17 +154,19 @@  def _cleanuppipes(ui, pipei, pipeo, pipe
 
         pipee.close()
 
-def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
+def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None,
+                    remotehidden=False):
     """Create an SSH connection to a server.
 
     Returns a tuple of (process, stdin, stdout, stderr) for the
     spawned process.
     """
-    cmd = '%s %s %s' % (
-        sshcmd,
-        args,
-        procutil.shellquote('%s -R %s serve --stdio' % (
-            _serverquote(remotecmd), _serverquote(path))))
+    remoteserve = '%s -R %s serve --stdio'
+    remoteserve %= (_serverquote(remotecmd), _serverquote(path))
+    if remotehidden:
+        remoteserve += ' --hidden'
+    remoteserve = _serverquote(remoteserve)
+    cmd = '%s %s %s' % (sshcmd, args, remoteserve)
 
     ui.debug('running %s\n' % cmd)
     cmd = procutil.quotecommand(cmd)
@@ -365,7 +367,7 @@  def _performhandshake(ui, stdin, stdout,
 
 class sshv1peer(wireprotov1peer.wirepeer):
     def __init__(self, ui, url, proc, stdin, stdout, stderr, caps,
-                 autoreadstderr=True):
+                 autoreadstderr=True, remotehidden=False):
         """Create a peer from an existing SSH connection.
 
         ``proc`` is a handle on the underlying SSH process.
@@ -392,6 +394,7 @@  class sshv1peer(wireprotov1peer.wirepeer
         self._pipee = stderr
         self._caps = caps
         self._autoreadstderr = autoreadstderr
+        self._remotehidden = remotehidden
 
     # Commands that have a "framed" response where the first line of the
     # response contains the length of that response.
@@ -408,11 +411,7 @@  class sshv1peer(wireprotov1peer.wirepeer
         return None
 
     def peer(self, remotehidden=False):
-        if remotehidden:
-            msg = _("ignoring `--remote-hidden` request\n"
-                    "(access to hidden changeset for ssh peers not supported "
-                    "yet)\n")
-            self.ui.warn(msg)
+        assert remotehidden == self._remotehidden
         return self
 
     def canpush(self):
@@ -586,11 +585,6 @@  def makepeer(ui, path, proc, stdin, stdo
     servers and clients via non-standard means, which can be useful for
     testing.
     """
-    if remotehidden:
-        msg = _("ignoring `--remote-hidden` request\n"
-                "(access to hidden changeset for ssh peers not supported "
-                "yet)\n")
-        ui.warn(msg)
     try:
         protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
     except Exception:
@@ -599,10 +593,12 @@  def makepeer(ui, path, proc, stdin, stdo
 
     if protoname == wireprototypes.SSHV1:
         return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps,
-                         autoreadstderr=autoreadstderr)
+                         autoreadstderr=autoreadstderr,
+                         remotehidden=remotehidden)
     elif protoname == wireprototypes.SSHV2:
         return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps,
-                         autoreadstderr=autoreadstderr)
+                         autoreadstderr=autoreadstderr,
+                         remotehidden=remotehidden)
     else:
         _cleanuppipes(ui, stdout, stdin, stderr)
         raise error.RepoError(_('unknown version of SSH protocol: %s') %
@@ -649,7 +645,8 @@  def instance(ui, path, create, intents=N
             raise error.RepoError(_('could not create remote repo'))
 
     proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
-                                                  remotepath, sshenv)
+                                                  remotepath, sshenv,
+                                                  remotehidden=remotehidden)
 
     peer = makepeer(ui, path, proc, stdin, stdout, stderr,
                     remotehidden=remotehidden)
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -566,7 +566,7 @@  class sshv2protocolhandler(sshv1protocol
     def addcapabilities(self, repo, caps):
         return caps
 
-def _runsshserver(ui, repo, fin, fout, ev):
+def _runsshserver(ui, repo, fin, fout, ev, accesshidden=False):
     # This function operates like a state machine of sorts. The following
     # states are defined:
     #
@@ -655,7 +655,8 @@  def _runsshserver(ui, repo, fin, fout, e
                 _sshv1respondbytes(fout, b'')
                 continue
 
-            rsp = wireprotov1server.dispatch(repo, proto, request)
+            rsp = wireprotov1server.dispatch(repo, proto, request,
+                                             accesshidden=accesshidden)
 
             if isinstance(rsp, bytes):
                 _sshv1respondbytes(fout, rsp)
@@ -781,10 +782,11 @@  def _runsshserver(ui, repo, fin, fout, e
                                          state)
 
 class sshserver(object):
-    def __init__(self, ui, repo, logfh=None):
+    def __init__(self, ui, repo, logfh=None, accesshidden=False):
         self._ui = ui
         self._repo = repo
         self._fin, self._fout = ui.protectfinout()
+        self._accesshidden = accesshidden
 
         # Log write I/O to stdout and stderr if configured.
         if logfh:
@@ -800,4 +802,5 @@  class sshserver(object):
 
     def serveuntil(self, ev):
         """Serve until a threading.Event is set."""
-        _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
+        _runsshserver(self._ui, self._repo, self._fin, self._fout, ev,
+                      self._accesshidden)
diff --git a/tests/test-remote-hidden.t b/tests/test-remote-hidden.t
--- a/tests/test-remote-hidden.t
+++ b/tests/test-remote-hidden.t
@@ -6,6 +6,8 @@  Test the ability to access a hidden revi
 
   $ . $TESTDIR/testlib/obsmarker-common.sh
   $ cat >> $HGRCPATH << EOF
+  > [ui]
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [phases]
   > # public changeset are not obsolete
   > publish=false
@@ -304,6 +306,98 @@  pulling an hidden changeset with --remot
   abort: filtered revision 'be215fbb8c50' (not in 'served' subset)!
   [255]
 
+Test --remote-hidden for ssh peer
+----------------------------------
+
+  $ hg clone --pull ssh://user@dummy/repo-with-hidden client-ssh
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 1 files
+  2 new obsolescence markers
+  new changesets 5f354f46e585:c33affeb3f6b (1 drafts)
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg -R client-ssh log -G --hidden -v
+  @  1:c33affeb3f6b c_Amend_New [draft]
+  |
+  o  0:5f354f46e585 c_Public [public]
+  
+
+Check on a server that do not allow hidden access:
+``````````````````````````````````````````````````
+
+pulling an hidden changeset should fail:
+
+  $ hg -R client-ssh pull -r be215fbb8c50
+  pulling from ssh://user@dummy/repo-with-hidden
+  abort: filtered revision 'be215fbb8c50' (not in 'served' subset)!
+  [255]
+
+pulling an hidden changeset with --remote-hidden should succeed:
+
+  $ hg -R client-ssh pull --remote-hidden -r be215fbb8c50
+  pulling from ssh://user@dummy/repo-with-hidden
+  remote: ignoring request to access hidden changeset by unauthorized user: * (glob)
+  abort: filtered revision 'be215fbb8c50' (not in 'served' subset)!
+  [255]
+  $ hg -R client-ssh log -G --hidden -v
+  @  1:c33affeb3f6b c_Amend_New [draft]
+  |
+  o  0:5f354f46e585 c_Public [public]
+  
+
+Check on a server that do allow hidden access:
+``````````````````````````````````````````````
+
+  $ cat << EOF >> repo-with-hidden/.hg/hgrc
+  > [experimental]
+  > server.allow-hidden-access=*
+  > EOF
+
+pulling an hidden changeset should fail:
+
+  $ hg -R client-ssh pull -r be215fbb8c50
+  pulling from ssh://user@dummy/repo-with-hidden
+  abort: filtered revision 'be215fbb8c50' (not in 'served' subset)!
+  [255]
+
+pulling an hidden changeset with --remote-hidden should succeed:
+
+  $ hg -R client-ssh pull --remote-hidden -r be215fbb8c50
+  pulling from ssh://user@dummy/repo-with-hidden
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (1 other changesets obsolete on arrival)
+  (run 'hg heads' to see heads)
+  $ hg -R client-ssh log -G --hidden -v
+  x  2:be215fbb8c50 c_Amend_Old [draft]
+  |
+  | @  1:c33affeb3f6b c_Amend_New [draft]
+  |/
+  o  0:5f354f46e585 c_Public [public]
+  
+
+Pulling a secret changeset is still forbidden:
+
+secret visible:
+
+  $ hg -R client-ssh pull --remote-hidden -r 8d28cbe335f3
+  pulling from ssh://user@dummy/repo-with-hidden
+  abort: filtered revision '8d28cbe335f3' (not in 'served.hidden' subset)!
+  [255]
+
+secret hidden:
+
+  $ hg -R client-ssh pull --remote-hidden -r 1c6afd79eb66
+  pulling from ssh://user@dummy/repo-with-hidden
+  abort: filtered revision '1c6afd79eb66' (not in 'served.hidden' subset)!
+  [255]
+
 =============
 Final cleanup
 =============