hgweb: support for deny_read/allow_read options
authorMark Edgington <edgimar@gmail.com>
Fri, 31 Oct 2008 15:28:06 +0100
changeset 7336 2dc868712dcc
parent 7335 866d2715aff5
child 7337 feb0b76b6717
child 7345 55651328dfcc
hgweb: support for deny_read/allow_read options reimplementation of a patch provided by Nilton Volpato. Folded into a single patch by Thomas Arendsen Hein.
doc/hgrc.5.txt
mercurial/hgweb/hgweb_mod.py
mercurial/hgweb/hgwebdir_mod.py
--- a/doc/hgrc.5.txt	Fri Nov 07 18:42:43 2008 +0100
+++ b/doc/hgrc.5.txt	Fri Oct 31 15:28:06 2008 +0100
@@ -676,6 +676,16 @@
     must be present in this list (separated by whitespace or ",").
     The contents of the allow_push list are examined after the
     deny_push list.
+  allow_read;;
+    If the user has not already been denied repository access due to the
+    contents of deny_read, this list determines whether to grant repository
+    access to the user. If this list is not empty, and the user is
+    unauthenticated or not present in the list (separated by whitespace or ","),
+    then access is denied for the user.  If the list is empty or not set, then
+    access is permitted to all users by default.  Setting allow_read to the
+    special value "*" is equivalent to it not being set (i.e. access is
+    permitted to all users).  The contents of the allow_read list are examined
+    after the deny_read list.
   allowzip;;
     (DEPRECATED) Whether to allow .zip downloading of repo revisions.
     Default is false. This feature creates temporary files.
@@ -693,6 +703,18 @@
     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.
+  deny_read;;
+    Whether to deny reading/viewing of the repository.  If this list is not
+    empty, unauthenticated users are all denied, and any authenticated user name
+    present in this list (separated by whitespace or ",") is also denied access
+    to the repository.  If set to the special value "*", all remote users are
+    denied access (rarely needed ;).  If deny_read is empty or not set, the
+    determination of repository access depends on the presence and content of
+    the allow_read list (see description).  If both deny_read and allow_read are
+    empty or not set, then access is permitted to all users by default.  If the
+    repository is being served via hgwebdir, denied users will not be able to
+    see it in the list of repositories.  The contents of the deny_read list have
+    priority over (are examined before) the contents of the allow_read list.
   description;;
     Textual description of the repository's purpose or contents.
     Default is "unknown".
--- a/mercurial/hgweb/hgweb_mod.py	Fri Nov 07 18:42:43 2008 +0100
+++ b/mercurial/hgweb/hgweb_mod.py	Fri Oct 31 15:28:06 2008 +0100
@@ -161,11 +161,13 @@
         # process the web interface request
 
         try:
-
             tmpl = self.templater(req)
             ctype = tmpl('mimetype', encoding=self.encoding)
             ctype = templater.stringify(ctype)
 
+            # check allow_read / deny_read config options
+            self.check_perm(req, None)
+
             if cmd == '':
                 req.form['cmd'] = [tmpl.cache['default']]
                 cmd = req.form['cmd'][0]
@@ -278,11 +280,24 @@
 
     def check_perm(self, req, op):
         '''Check permission for operation based on request data (including
-        authentication info. Return true if op allowed, else false.'''
+        authentication info). Return if op allowed, else raise an ErrorResponse
+        exception.'''
+
+        user = req.env.get('REMOTE_USER')
+
+        deny_read = self.configlist('web', 'deny_read')
+        if deny_read and (not user or deny_read == ['*'] or user in deny_read):
+            raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
+
+        allow_read = self.configlist('web', 'allow_read')
+        result = (not allow_read) or (allow_read == ['*']) or (user in allow_read)
+        if not result:
+            raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
 
         if op == 'pull' and not self.allowpull:
             raise ErrorResponse(HTTP_OK, '')
-        elif op == 'pull':
+        # op is None when checking allow/deny_read permissions for a web-browser request
+        elif op == 'pull' or op is None:
             return
 
         # enforce that you can only push using POST requests
@@ -296,8 +311,6 @@
         if self.configbool('web', 'push_ssl', True) and scheme != 'https':
             raise ErrorResponse(HTTP_OK, 'ssl required')
 
-        user = req.env.get('REMOTE_USER')
-
         deny = self.configlist('web', 'deny_push')
         if deny and (not user or deny == ['*'] or user in deny):
             raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
--- a/mercurial/hgweb/hgwebdir_mod.py	Fri Nov 07 18:42:43 2008 +0100
+++ b/mercurial/hgweb/hgwebdir_mod.py	Fri Oct 31 15:28:06 2008 +0100
@@ -72,6 +72,28 @@
         req = wsgirequest(env, respond)
         return self.run_wsgi(req)
 
+    def read_allowed(self, ui, req):
+        """Check allow_read and deny_read config options of a repo's ui object
+        to determine user permissions.  By default, with neither option set (or
+        both empty), allow all users to read the repo.  There are two ways a
+        user can be denied read access:  (1) deny_read is not empty, and the
+        user is unauthenticated or deny_read contains user (or *), and (2)
+        allow_read is not empty and the user is not in allow_read.  Return True
+        if user is allowed to read the repo, else return False."""
+
+        user = req.env.get('REMOTE_USER')
+
+        deny_read = ui.configlist('web', 'deny_read', default=None, untrusted=True)
+        if deny_read and (not user or deny_read == ['*'] or user in deny_read):
+            return False
+
+        allow_read = ui.configlist('web', 'allow_read', default=None, untrusted=True)
+        # by default, allow reading if no allow_read option has been set
+        if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
+            return True
+
+        return False
+
     def run_wsgi(self, req):
 
         try:
@@ -175,6 +197,9 @@
                 if u.configbool("web", "hidden", untrusted=True):
                     continue
 
+                if not self.read_allowed(u, req):
+                    continue
+
                 parts = [name]
                 if 'PATH_INFO' in req.env:
                     parts.insert(0, req.env['PATH_INFO'].rstrip('/'))