hgweb: centralize permission checks for protocol commands
authorDirkjan Ochtman <dirkjan@ochtman.nl>
Sun, 29 Jun 2008 11:35:06 +0200
changeset 6779 d3147b4e3e8a
parent 6778 959efdac4a9c
child 6780 4c1d67e0fa8c
hgweb: centralize permission checks for protocol commands Consistently enforces authorization checks set up in hgrc up front, so that the actual commands don't have to worry about them and implementers of hgweb alternatives can easily implement their own permission checks.
mercurial/hgweb/hgweb_mod.py
mercurial/hgweb/protocol.py
tests/test-hgweb-commands.out
--- a/mercurial/hgweb/hgweb_mod.py	Sun Jun 29 11:02:19 2008 +0200
+++ b/mercurial/hgweb/hgweb_mod.py	Sun Jun 29 11:35:06 2008 +0200
@@ -16,6 +16,13 @@
 from request import wsgirequest
 import webcommands, protocol, webutil
 
+perms = {
+    'changegroup': 'pull',
+    'changegroupsubset': 'pull',
+    'unbundle': 'push',
+    'stream_out': 'pull',
+}
+
 class hgweb(object):
     def __init__(self, repo, name=None):
         if isinstance(repo, str):
@@ -95,6 +102,8 @@
 
         cmd = req.form.get('cmd', [''])[0]
         if cmd and cmd in protocol.__all__:
+            if cmd in perms and not self.check_perm(req, perms[cmd]):
+                return
             method = getattr(protocol, cmd)
             method(self, req)
             return
@@ -343,16 +352,39 @@
         'zip': ('application/zip', 'zip', '.zip', None),
         }
 
-    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.'''
+    def check_perm(self, req, op):
+        '''Check permission for operation based on request data (including
+        authentication info. Return true if op allowed, else false.'''
+
+        def error(status, message):
+            req.respond(status, protocol.HGTYPE)
+            req.write('0\n%s\n' % message)
+
+        if op == 'pull':
+            return self.allowpull
+
+        # enforce that you can only push using POST requests
+        if req.env['REQUEST_METHOD'] != 'POST':
+            error('405 Method Not Allowed', 'push requires POST request')
+            return False
+
+        # require ssl by default for pushing, auth info cannot be sniffed
+        # and replayed
+        scheme = req.env.get('wsgi.url_scheme')
+        if self.configbool('web', 'push_ssl', True) and scheme != 'https':
+            error(HTTP_OK, 'ssl required')
+            return False
 
         user = req.env.get('REMOTE_USER')
 
-        deny = self.configlist('web', 'deny_' + op)
+        deny = self.configlist('web', 'deny_push')
         if deny and (not user or deny == ['*'] or user in deny):
+            error('401 Unauthorized', 'push not authorized')
             return False
 
-        allow = self.configlist('web', 'allow_' + op)
-        return (allow and (allow == ['*'] or user in allow)) or default
+        allow = self.configlist('web', 'allow_push')
+        result = allow and (allow == ['*'] or user in allow)
+        if not result:
+            error('401 Unauthorized', 'push not authorized')
+
+        return result
--- a/mercurial/hgweb/protocol.py	Sun Jun 29 11:02:19 2008 +0200
+++ b/mercurial/hgweb/protocol.py	Sun Jun 29 11:35:06 2008 +0200
@@ -62,8 +62,6 @@
 def changegroup(web, req):
     req.respond(HTTP_OK, HGTYPE)
     nodes = []
-    if not web.allowpull:
-        return
 
     if 'roots' in req.form:
         nodes = map(bin, req.form['roots'][0].split(" "))
@@ -82,8 +80,6 @@
     req.respond(HTTP_OK, HGTYPE)
     bases = []
     heads = []
-    if not web.allowpull:
-        return
 
     if 'bases' in req.form:
         bases = [bin(x) for x in req.form['bases'][0].split(' ')]
@@ -120,28 +116,7 @@
         req.write('0\n')
         req.write(response)
 
-    # enforce that you can only unbundle with POST requests
-    if req.env['REQUEST_METHOD'] != 'POST':
-        headers = {'status': '405 Method Not Allowed'}
-        bail('unbundle requires POST request\n', headers)
-        return
-
-    # require ssl by default, auth info cannot be sniffed and
-    # replayed
-    ssl_req = web.configbool('web', 'push_ssl', True)
-    if ssl_req:
-        if req.env.get('wsgi.url_scheme') != 'https':
-            bail('ssl required\n')
-            return
-        proto = 'https'
-    else:
-        proto = 'http'
-
-    # do not allow push unless explicitly allowed
-    if not web.check_perm(req, 'push', False):
-        bail('push not authorized\n', headers={'status': '401 Unauthorized'})
-        return
-
+    proto = req.env.get('wsgi.url_scheme') or 'http'
     their_heads = req.form['heads'][0].split(' ')
 
     def check_heads():
@@ -224,7 +199,5 @@
         os.unlink(tempname)
 
 def stream_out(web, req):
-    if not web.allowpull:
-        return
     req.respond(HTTP_OK, HGTYPE)
     streamclone.stream_out(web.repo, req, untrusted=True)
Binary file tests/test-hgweb-commands.out has changed