changeset 3552:9b52239dc740

save settings from untrusted config files in a separate configparser This untrusted configparser is a superset of the trusted configparser, so that interpolation still works. Also add an "untrusted" argument to ui.config* to allow querying ui.ucdata. With --debug, we print a warning when we read an untrusted config file, and when we try to access a trusted setting that has one value in the trusted configparser and another in the untrusted configparser.
author Alexis S. L. Carvalho <alexis@cecm.usp.br>
date Thu, 26 Oct 2006 19:25:45 +0200
parents 3b07e223534b
children e1508621e9ef
files doc/hgrc.5.txt mercurial/ui.py tests/test-trusted.py tests/test-trusted.py.out
diffstat 4 files changed, 349 insertions(+), 41 deletions(-) [+]
line wrap: on
line diff
--- a/doc/hgrc.5.txt	Thu Oct 26 19:25:44 2006 +0200
+++ b/doc/hgrc.5.txt	Thu Oct 26 19:25:45 2006 +0200
@@ -50,8 +50,9 @@
     particular repository.  This file is not version-controlled, and
     will not get transferred during a "clone" operation.  Options in
     this file override options in all other configuration files.
-    On Unix, this file is only read if it belongs to a trusted user
-    or to a trusted group.
+    On Unix, most of this file will be ignored if it doesn't belong
+    to a trusted user or to a trusted group.  See the documentation
+    for the trusted section below for more details.
 
 SYNTAX
 ------
@@ -367,11 +368,16 @@
     data transfer overhead.  Default is False.
 
 trusted::
-  Mercurial will only read the .hg/hgrc file from a repository if
-  it belongs to a trusted user or to a trusted group.  This section
-  specifies what users and groups are trusted.  The current user is
-  always trusted.  To trust everybody, list a user or a group with
-  name "*".
+  For security reasons, Mercurial will not use the settings in
+  the .hg/hgrc file from a repository if it doesn't belong to a
+  trusted user or to a trusted group.  The main exception is the
+  web interface, which automatically uses some safe settings, since
+  it's common to serve repositories from different users.
+
+  This section specifies what users and groups are trusted.  The
+  current user is always trusted.  To trust everybody, list a user
+  or a group with name "*".
+
   users;;
     Comma-separated list of trusted users.
   groups;;
--- a/mercurial/ui.py	Thu Oct 26 19:25:44 2006 +0200
+++ b/mercurial/ui.py	Thu Oct 26 19:25:45 2006 +0200
@@ -41,7 +41,9 @@
             self.traceback = traceback
             self.trusted_users = {}
             self.trusted_groups = {}
+            # if ucdata is not None, its keys must be a superset of cdata's
             self.cdata = util.configparser()
+            self.ucdata = None
             self.readconfig(util.rcpath())
             self.updateopts(verbose, debug, quiet, interactive)
         else:
@@ -51,6 +53,8 @@
             self.trusted_users = parentui.trusted_users.copy()
             self.trusted_groups = parentui.trusted_groups.copy()
             self.cdata = dupconfig(self.parentui.cdata)
+            if self.parentui.ucdata:
+                self.ucdata = dupconfig(self.parentui.ucdata)
             if self.parentui.overlay:
                 self.overlay = dupconfig(self.parentui.overlay)
 
@@ -95,7 +99,7 @@
             group = util.groupname(st.st_gid)
             if user not in tusers and group not in tgroups:
                 if warn:
-                    self.warn(_('Not reading file %s from untrusted '
+                    self.warn(_('Not trusting file %s from untrusted '
                                 'user %s, group %s\n') % (f, user, group))
                 return False
         return True
@@ -108,12 +112,30 @@
                 fp = open(f)
             except IOError:
                 continue
-            if not self._is_trusted(fp, f):
-                continue
+            cdata = self.cdata
+            trusted = self._is_trusted(fp, f)
+            if not trusted:
+                if self.ucdata is None:
+                    self.ucdata = dupconfig(self.cdata)
+                cdata = self.ucdata
+            elif self.ucdata is not None:
+                # use a separate configparser, so that we don't accidentally
+                # override ucdata settings later on.
+                cdata = util.configparser()
+
             try:
-                self.cdata.readfp(fp, f)
+                cdata.readfp(fp, f)
             except ConfigParser.ParsingError, inst:
-                raise util.Abort(_("Failed to parse %s\n%s") % (f, inst))
+                msg = _("Failed to parse %s\n%s") % (f, inst)
+                if trusted:
+                    raise util.Abort(msg)
+                self.warn(_("Ignored: %s\n") % msg)
+
+            if trusted:
+                if cdata != self.cdata:
+                    updateconfig(cdata, self.cdata)
+                if self.ucdata is not None:
+                    updateconfig(cdata, self.ucdata)
         # override data from config files with data set with ui.setconfig
         if self.overlay:
             updateconfig(self.overlay, self.cdata)
@@ -127,7 +149,10 @@
         self.readhooks.append(hook)
 
     def readsections(self, filename, *sections):
-        "read filename and add only the specified sections to the config data"
+        """Read filename and add only the specified sections to the config data
+
+        The settings are added to the trusted config data.
+        """
         if not sections:
             return
 
@@ -143,6 +168,8 @@
                 cdata.add_section(section)
 
         updateconfig(cdata, self.cdata, sections)
+        if self.ucdata:
+            updateconfig(cdata, self.ucdata, sections)
 
     def fixconfig(self, section=None, name=None, value=None, root=None):
         # translate paths relative to root (or home) into absolute paths
@@ -150,7 +177,7 @@
             if root is None:
                 root = os.getcwd()
             items = section and [(name, value)] or []
-            for cdata in self.cdata, self.overlay:
+            for cdata in self.cdata, self.ucdata, self.overlay:
                 if not cdata: continue
                 if not items and cdata.has_section('paths'):
                     pathsitems = cdata.items('paths')
@@ -181,59 +208,98 @@
     def setconfig(self, section, name, value):
         if not self.overlay:
             self.overlay = util.configparser()
-        for cdata in (self.overlay, self.cdata):
+        for cdata in (self.overlay, self.cdata, self.ucdata):
+            if not cdata: continue
             if not cdata.has_section(section):
                 cdata.add_section(section)
             cdata.set(section, name, value)
         self.fixconfig(section, name, value)
 
-    def _config(self, section, name, default, funcname):
-        if self.cdata.has_option(section, name):
+    def _get_cdata(self, untrusted):
+        if untrusted and self.ucdata:
+            return self.ucdata
+        return self.cdata
+
+    def _config(self, section, name, default, funcname, untrusted, abort):
+        cdata = self._get_cdata(untrusted)
+        if cdata.has_option(section, name):
             try:
-                func = getattr(self.cdata, funcname)
+                func = getattr(cdata, funcname)
                 return func(section, name)
             except ConfigParser.InterpolationError, inst:
-                raise util.Abort(_("Error in configuration section [%s] "
-                                   "parameter '%s':\n%s")
-                                 % (section, name, inst))
+                msg = _("Error in configuration section [%s] "
+                        "parameter '%s':\n%s") % (section, name, inst)
+                if abort:
+                    raise util.Abort(msg)
+                self.warn(_("Ignored: %s\n") % msg)
         return default
 
-    def config(self, section, name, default=None):
-        return self._config(section, name, default, 'get')
+    def _configcommon(self, section, name, default, funcname, untrusted):
+        value = self._config(section, name, default, funcname,
+                             untrusted, abort=True)
+        if self.debugflag and not untrusted and self.ucdata:
+            uvalue = self._config(section, name, None, funcname,
+                                  untrusted=True, abort=False)
+            if uvalue is not None and uvalue != value:
+                self.warn(_("Ignoring untrusted configuration option "
+                            "%s.%s = %s\n") % (section, name, uvalue))
+        return value
 
-    def configbool(self, section, name, default=False):
-        return self._config(section, name, default, 'getboolean')
+    def config(self, section, name, default=None, untrusted=False):
+        return self._configcommon(section, name, default, 'get', untrusted)
 
-    def configlist(self, section, name, default=None):
+    def configbool(self, section, name, default=False, untrusted=False):
+        return self._configcommon(section, name, default, 'getboolean',
+                                  untrusted)
+
+    def configlist(self, section, name, default=None, untrusted=False):
         """Return a list of comma/space separated strings"""
-        result = self.config(section, name)
+        result = self.config(section, name, untrusted=untrusted)
         if result is None:
             result = default or []
         if isinstance(result, basestring):
             result = result.replace(",", " ").split()
         return result
 
-    def has_config(self, section):
+    def has_config(self, section, untrusted=False):
         '''tell whether section exists in config.'''
-        return self.cdata.has_section(section)
+        cdata = self._get_cdata(untrusted)
+        return cdata.has_section(section)
 
-    def configitems(self, section):
+    def _configitems(self, section, untrusted, abort):
         items = {}
-        if self.cdata.has_section(section):
+        cdata = self._get_cdata(untrusted)
+        if cdata.has_section(section):
             try:
-                items.update(dict(self.cdata.items(section)))
+                items.update(dict(cdata.items(section)))
             except ConfigParser.InterpolationError, inst:
-                raise util.Abort(_("Error in configuration section [%s]:\n%s")
-                                 % (section, inst))
+                msg = _("Error in configuration section [%s]:\n"
+                        "%s") % (section, inst)
+                if abort:
+                    raise util.Abort(msg)
+                self.warn(_("Ignored: %s\n") % msg)
+        return items
+
+    def configitems(self, section, untrusted=False):
+        items = self._configitems(section, untrusted=untrusted, abort=True)
+        if self.debugflag and not untrusted and self.ucdata:
+            uitems = self._configitems(section, untrusted=True, abort=False)
+            keys = uitems.keys()
+            keys.sort()
+            for k in keys:
+                if uitems[k] != items.get(k):
+                    self.warn(_("Ignoring untrusted configuration option "
+                                "%s.%s = %s\n") % (section, k, uitems[k]))
         x = items.items()
         x.sort()
         return x
 
-    def walkconfig(self):
-        sections = self.cdata.sections()
+    def walkconfig(self, untrusted=False):
+        cdata = self._get_cdata(untrusted)
+        sections = cdata.sections()
         sections.sort()
         for section in sections:
-            for name, value in self.configitems(section):
+            for name, value in self.configitems(section, untrusted):
                 yield section, name, value.replace('\n', '\\n')
 
     def extensions(self):
--- a/tests/test-trusted.py	Thu Oct 26 19:25:44 2006 +0200
+++ b/tests/test-trusted.py	Thu Oct 26 19:25:45 2006 +0200
@@ -9,7 +9,7 @@
 hgrc = os.environ['HGRCPATH']
 
 def testui(user='foo', group='bar', tusers=(), tgroups=(),
-           cuser='foo', cgroup='bar', debug=False):
+           cuser='foo', cgroup='bar', debug=False, silent=False):
     # user, group => owners of the file
     # tusers, tgroups => trusted users/groups
     # cuser, cgroup => user/group of the current process
@@ -56,8 +56,18 @@
     parentui.updateopts(debug=debug)
     u = ui.ui(parentui=parentui)
     u.readconfig('.hg/hgrc')
+    if silent:
+        return u
+    print 'trusted'
     for name, path in u.configitems('paths'):
         print '   ', name, '=', path
+    print 'untrusted'
+    for name, path in u.configitems('paths', untrusted=True):
+        print '.',
+        u.config('paths', name) # warning with debug=True
+        print '.',
+        u.config('paths', name, untrusted=True) # no warnings
+        print name, '=', path
     print
 
     return u
@@ -68,6 +78,7 @@
 f = open('.hg/hgrc', 'w')
 f.write('[paths]\n')
 f.write('local = /another/path\n\n')
+f.write('interpolated = %(global)s%(local)s\n\n')
 f.close()
 
 #print '# Everything is run by user foo, group bar\n'
@@ -111,3 +122,83 @@
 
 print "# Can't figure out the name of the user running this process"
 testui(user='abc', group='def', cuser=None)
+
+print "# prints debug warnings"
+u = testui(user='abc', group='def', cuser='foo', debug=True)
+
+print "# ui.readsections"
+filename = 'foobar'
+f = open(filename, 'w')
+f.write('[foobar]\n')
+f.write('baz = quux\n')
+f.close()
+u.readsections(filename, 'foobar')
+print u.config('foobar', 'baz')
+
+print
+print "# read trusted, untrusted, new ui, trusted"
+u = ui.ui()
+u.updateopts(debug=True)
+u.readconfig(filename)
+u2 = ui.ui(parentui=u)
+def username(uid=None):
+    return 'foo'
+util.username = username
+u2.readconfig('.hg/hgrc')
+print 'trusted:'
+print u2.config('foobar', 'baz')
+print u2.config('paths', 'interpolated')
+print 'untrusted:'
+print u2.config('foobar', 'baz', untrusted=True)
+print u2.config('paths', 'interpolated', untrusted=True)
+
+print 
+print "# error handling"
+
+def assertraises(f, exc=util.Abort):
+    try:
+        f()
+    except exc, inst:
+        print 'raised', inst.__class__.__name__
+    else:
+        print 'no exception?!'
+
+print "# file doesn't exist"
+os.unlink('.hg/hgrc')
+assert not os.path.exists('.hg/hgrc')
+testui(debug=True, silent=True)
+testui(user='abc', group='def', debug=True, silent=True)
+
+print
+print "# parse error"
+f = open('.hg/hgrc', 'w')
+f.write('foo = bar')
+f.close()
+testui(user='abc', group='def', silent=True)
+assertraises(lambda: testui(debug=True, silent=True))
+
+print
+print "# interpolation error"
+f = open('.hg/hgrc', 'w')
+f.write('[foo]\n')
+f.write('bar = %(')
+f.close()
+u = testui(debug=True, silent=True)
+print '# regular config:'
+print '  trusted',
+assertraises(lambda: u.config('foo', 'bar'))
+print 'untrusted',
+assertraises(lambda: u.config('foo', 'bar', untrusted=True))
+
+u = testui(user='abc', group='def', debug=True, silent=True)
+print '  trusted ',
+print u.config('foo', 'bar')
+print 'untrusted',
+assertraises(lambda: u.config('foo', 'bar', untrusted=True))
+
+print '# configitems:'
+print '  trusted ',
+print u.configitems('foo')
+print 'untrusted',
+assertraises(lambda: u.configitems('foo', untrusted=True))
+
--- a/tests/test-trusted.py.out	Thu Oct 26 19:25:44 2006 +0200
+++ b/tests/test-trusted.py.out	Thu Oct 26 19:25:45 2006 +0200
@@ -1,67 +1,212 @@
 # same user, same group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # same user, different group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # different user, same group
-Not reading file .hg/hgrc from untrusted user abc, group bar
+Not trusting file .hg/hgrc from untrusted user abc, group bar
+trusted
     global = /some/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # different user, same group, but we trust the group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # different user, different group
-Not reading file .hg/hgrc from untrusted user abc, group def
+Not trusting file .hg/hgrc from untrusted user abc, group def
+trusted
     global = /some/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # different user, different group, but we trust the user
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # different user, different group, but we trust the group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # different user, different group, but we trust the user and the group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # we trust all users
 # different user, different group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # we trust all groups
 # different user, different group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # we trust all users and groups
 # different user, different group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # we don't get confused by users and groups with the same name
 # different user, different group
-Not reading file .hg/hgrc from untrusted user abc, group def
+Not trusting file .hg/hgrc from untrusted user abc, group def
+trusted
     global = /some/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # list of user names
 # different user, different group, but we trust the user
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # list of group names
 # different user, different group, but we trust the group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
 # Can't figure out the name of the user running this process
 # different user, different group
+trusted
     global = /some/path
+    interpolated = /some/path/another/path
     local = /another/path
+untrusted
+. . global = /some/path
+. . interpolated = /some/path/another/path
+. . local = /another/path
 
+# prints debug warnings
+# different user, different group
+Not trusting file .hg/hgrc from untrusted user abc, group def
+trusted
+Ignoring untrusted configuration option paths.interpolated = /some/path/another/path
+Ignoring untrusted configuration option paths.local = /another/path
+    global = /some/path
+untrusted
+. . global = /some/path
+.Ignoring untrusted configuration option paths.interpolated = /some/path/another/path
+ . interpolated = /some/path/another/path
+.Ignoring untrusted configuration option paths.local = /another/path
+ . local = /another/path
+
+# ui.readsections
+quux
+
+# read trusted, untrusted, new ui, trusted
+Not trusting file foobar from untrusted user abc, group def
+trusted:
+Ignoring untrusted configuration option foobar.baz = quux
+None
+/some/path/another/path
+untrusted:
+quux
+/some/path/another/path
+
+# error handling
+# file doesn't exist
+# same user, same group
+# different user, different group
+
+# parse error
+# different user, different group
+Not trusting file .hg/hgrc from untrusted user abc, group def
+Ignored: Failed to parse .hg/hgrc
+File contains no section headers.
+file: .hg/hgrc, line: 1
+'foo = bar'
+# same user, same group
+raised Abort
+
+# interpolation error
+# same user, same group
+# regular config:
+  trusted raised Abort
+untrusted raised Abort
+# different user, different group
+Not trusting file .hg/hgrc from untrusted user abc, group def
+  trusted Ignored: Error in configuration section [foo] parameter 'bar':
+bad interpolation variable reference '%('
+ None
+untrusted raised Abort
+# configitems:
+  trusted Ignored: Error in configuration section [foo]:
+bad interpolation variable reference '%('
+ []
+untrusted raised Abort