push over http: server side authorization support.
authorVadim Gelfer <vadim.gelfer@gmail.com>
Tue, 20 Jun 2006 15:23:01 -0700
changeset 2466 e10665147d26
parent 2465 c91118f425d0
child 2467 4e78dc71d946
push over http: server side authorization support. new hgrc entries allow_push, deny_push, push_ssl control push over http. allow_push list controls push. if empty or not set, no user can push. if "*", any user (incl. unauthenticated user) can push. if list of user names, only authenticated users in list can push. deny_push list examined before allow_push. if "*", no user can push. if list of user names, no unauthenticated user can push, and no users in list can push. push_ssl requires https connection for push. default is true, so password sniffing can not be done.
doc/hgrc.5.txt
mercurial/hgweb/hgweb_mod.py
mercurial/hgweb/request.py
--- a/doc/hgrc.5.txt	Tue Jun 20 15:17:28 2006 -0700
+++ b/doc/hgrc.5.txt	Tue Jun 20 15:23:01 2006 -0700
@@ -381,6 +381,14 @@
     Default is false.
   allowpull;;
     Whether to allow pulling from the repository. Default is true.
+  allow_push;;
+    Whether to allow pushing to the repository.  If empty or not set,
+    push is not allowed.  If the special value "*", any remote user
+    can push, including unauthenticated users.  Otherwise, the remote
+    user must have been authenticated, and the authenticated user name
+    must be present in this list (separated by whitespace or ",").
+    The contents of the allow_push list are examined after the
+    deny_push list.
   allowzip;;
     (DEPRECATED) Whether to allow .zip downloading of repo revisions.
     Default is false. This feature creates temporary files.
@@ -391,6 +399,13 @@
   contact;;
     Name or email address of the person in charge of the repository.
     Default is "unknown".
+  deny_push;;
+    Whether to deny pushing to the repository.  If empty or not set,
+    push is not denied.  If the special value "*", all remote users
+    are denied push.  Otherwise, unauthenticated users are all denied,
+    and any authenticated user name present in this list (separated by
+    whitespace or ",") is also denied.  The contents of the deny_push
+    list are examined before the allow_push list.
   description;;
     Textual description of the repository's purpose or contents.
     Default is "unknown".
@@ -407,6 +422,9 @@
     Maximum number of files to list per changeset. Default is 10.
   port;;
     Port to listen on. Default is 8000.
+  push_ssl;;
+    Whether to require that inbound pushes be transported over SSL to
+    prevent password sniffing.  Default is true.
   style;;
     Which template map style to use.
   templates;;
--- a/mercurial/hgweb/hgweb_mod.py	Tue Jun 20 15:17:28 2006 -0700
+++ b/mercurial/hgweb/hgweb_mod.py	Tue Jun 20 15:23:01 2006 -0700
@@ -839,19 +839,59 @@
         req.httphdr("application/mercurial-0.1", length=len(resp))
         req.write(resp)
 
+    def check_perm(self, req, op, default):
+        '''check permission for operation based on user auth.
+        return true if op allowed, else false.
+        default is policy to use if no config given.'''
+
+        user = req.env.get('REMOTE_USER')
+
+        deny = self.repo.ui.config('web', 'deny_' + op, '')
+        deny = deny.replace(',', ' ').split()
+
+        if deny and (not user or deny == ['*'] or user in deny):
+            return False
+
+        allow = self.repo.ui.config('web', 'allow_' + op, '')
+        allow = allow.replace(',', ' ').split()
+
+        return (allow and (allow == ['*'] or user in allow)) or default
+
     def do_unbundle(self, req):
+        def bail(response, headers={}):
+            length = int(req.env['CONTENT_LENGTH'])
+            for s in util.filechunkiter(req, limit=length):
+                # drain incoming bundle, else client will not see
+                # response when run outside cgi script
+                pass
+            req.httphdr("application/mercurial-0.1", headers=headers)
+            req.write('0\n')
+            req.write(response)
+
+        # require ssl by default, auth info cannot be sniffed and
+        # replayed
+        ssl_req = self.repo.ui.configbool('web', 'push_ssl', True)
+        if ssl_req and not req.env.get('HTTPS'):
+            bail(_('ssl required\n'))
+            return
+
+        # do not allow push unless explicitly allowed
+        if not self.check_perm(req, 'push', False):
+            bail(_('push not authorized\n'),
+                 headers={'status': '401 Unauthorized'})
+            return
+
+        req.httphdr("application/mercurial-0.1")
+
         their_heads = req.form['heads'][0].split(' ')
 
         def check_heads():
             heads = map(hex, self.repo.heads())
             return their_heads == [hex('force')] or their_heads == heads
 
-        req.httphdr("application/mercurial-0.1")
-
         # fail early if possible
         if not check_heads():
-            req.write('0\n')
-            req.write(_('unsynced changes\n'))
+            bail(_('unsynced changes\n'))
             return
 
         # do not lock repo until all changegroup data is
--- a/mercurial/hgweb/request.py	Tue Jun 20 15:17:28 2006 -0700
+++ b/mercurial/hgweb/request.py	Tue Jun 20 15:23:01 2006 -0700
@@ -45,9 +45,9 @@
             self.out.write("%s: %s\r\n" % header)
         self.out.write("\r\n")
 
-    def httphdr(self, type, filename=None, length=0):
-
-        headers = [('Content-type', type)]
+    def httphdr(self, type, filename=None, length=0, headers={}):
+        headers = headers.items()
+        headers.append(('Content-type', type))
         if filename:
             headers.append(('Content-disposition', 'attachment; filename=%s' %
                             filename))