Only read .hg/hgrc files from trusted users/groups
authorAlexis S. L. Carvalho <alexis@cecm.usp.br>
Thu, 26 Oct 2006 19:25:44 +0200
changeset 3551 3b07e223534b
parent 3550 ef80b13df85a
child 3552 9b52239dc740
Only read .hg/hgrc files from trusted users/groups The list of trusted users and groups is specified in the [trusted] section of a hgrc; the current user is always trusted; "*" can be used to trust all users/groups. Global hgrc files are always read. On Windows (and other systems that don't have the pwd and grp modules), all .hg/hgrc files are read. This is essentially the same patch that was previously applied as revision 494521a3f142.
doc/hgrc.5.txt
mercurial/ui.py
mercurial/util.py
tests/test-trusted.py
tests/test-trusted.py.out
--- a/doc/hgrc.5.txt	Thu Oct 26 10:06:12 2006 -0700
+++ b/doc/hgrc.5.txt	Thu Oct 26 19:25:44 2006 +0200
@@ -50,6 +50,8 @@
     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.
 
 SYNTAX
 ------
@@ -364,6 +366,17 @@
     6Mbps), uncompressed streaming is slower, because of the extra
     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 "*".
+  users;;
+    Comma-separated list of trusted users.
+  groups;;
+    Comma-separated list of trusted groups.
+
 ui::
   User interface controls.
   debug;;
--- a/mercurial/ui.py	Thu Oct 26 10:06:12 2006 -0700
+++ b/mercurial/ui.py	Thu Oct 26 19:25:44 2006 +0200
@@ -39,6 +39,8 @@
             self.debugflag = debug
             self.interactive = interactive
             self.traceback = traceback
+            self.trusted_users = {}
+            self.trusted_groups = {}
             self.cdata = util.configparser()
             self.readconfig(util.rcpath())
             self.updateopts(verbose, debug, quiet, interactive)
@@ -46,6 +48,8 @@
             # parentui may point to an ui object which is already a child
             self.parentui = parentui.parentui or parentui
             self.readhooks = self.parentui.readhooks[:]
+            self.trusted_users = parentui.trusted_users.copy()
+            self.trusted_groups = parentui.trusted_groups.copy()
             self.cdata = dupconfig(self.parentui.cdata)
             if self.parentui.overlay:
                 self.overlay = dupconfig(self.parentui.overlay)
@@ -82,12 +86,32 @@
         elif self.verbose and self.quiet:
             self.quiet = self.verbose = False
 
+    def _is_trusted(self, fp, f, warn=True):
+        tusers = self.trusted_users
+        tgroups = self.trusted_groups
+        if (tusers or tgroups) and '*' not in tusers and '*' not in tgroups:
+            st = util.fstat(fp)
+            user = util.username(st.st_uid)
+            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 '
+                                'user %s, group %s\n') % (f, user, group))
+                return False
+        return True
+
     def readconfig(self, fn, root=None):
         if isinstance(fn, basestring):
             fn = [fn]
         for f in fn:
             try:
-                self.cdata.read(f)
+                fp = open(f)
+            except IOError:
+                continue
+            if not self._is_trusted(fp, f):
+                continue
+            try:
+                self.cdata.readfp(fp, f)
             except ConfigParser.ParsingError, inst:
                 raise util.Abort(_("Failed to parse %s\n%s") % (f, inst))
         # override data from config files with data set with ui.setconfig
@@ -144,6 +168,16 @@
             if name is None or name == 'interactive':
                 self.interactive = self.configbool("ui", "interactive", True)
 
+        # update trust information
+        if section is None or section == 'trusted':
+            user = util.username()
+            if user is not None:
+                self.trusted_users[user] = 1
+                for user in self.configlist('trusted', 'users'):
+                    self.trusted_users[user] = 1
+                for group in self.configlist('trusted', 'groups'):
+                    self.trusted_groups[group] = 1
+
     def setconfig(self, section, name, value):
         if not self.overlay:
             self.overlay = util.configparser()
--- a/mercurial/util.py	Thu Oct 26 10:06:12 2006 -0700
+++ b/mercurial/util.py	Thu Oct 26 19:25:44 2006 +0200
@@ -519,6 +519,36 @@
     except AttributeError:
         return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
 
+def username(uid=None):
+    """Return the name of the user with the given uid.
+
+    If uid is None, return the name of the current user."""
+    try:
+        import pwd
+        if uid is None:
+            uid = os.getuid()
+        try:
+            return pwd.getpwuid(uid)[0]
+        except KeyError:
+            return str(uid)
+    except ImportError:
+        return None
+
+def groupname(gid=None):
+    """Return the name of the group with the given gid.
+
+    If gid is None, return the name of the current group."""
+    try:
+        import grp
+        if gid is None:
+            gid = os.getgid()
+        try:
+            return grp.getgrgid(gid)[0]
+        except KeyError:
+            return str(gid)
+    except ImportError:
+        return None
+
 # Platform specific variants
 if os.name == 'nt':
     demandload(globals(), "msvcrt")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-trusted.py	Thu Oct 26 19:25:44 2006 +0200
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# Since it's not easy to write a test that portably deals
+# with files from different users/groups, we cheat a bit by
+# monkey-patching some functions in the util module
+
+import os
+from mercurial import ui, util
+
+hgrc = os.environ['HGRCPATH']
+
+def testui(user='foo', group='bar', tusers=(), tgroups=(),
+           cuser='foo', cgroup='bar', debug=False):
+    # user, group => owners of the file
+    # tusers, tgroups => trusted users/groups
+    # cuser, cgroup => user/group of the current process
+
+    # write a global hgrc with the list of trusted users/groups and
+    # some setting so that we can be sure it was read
+    f = open(hgrc, 'w')
+    f.write('[paths]\n')
+    f.write('global = /some/path\n\n')
+
+    if tusers or tgroups:
+        f.write('[trusted]\n')
+        if tusers:
+            f.write('users = %s\n' % ', '.join(tusers))
+        if tgroups:
+            f.write('groups = %s\n' % ', '.join(tgroups))
+    f.close()
+
+    # override the functions that give names to uids and gids
+    def username(uid=None):
+        if uid is None:
+            return cuser
+        return user
+    util.username = username
+
+    def groupname(gid=None):
+        if gid is None:
+            return 'bar'
+        return group
+    util.groupname = groupname
+
+    # try to read everything
+    #print '# File belongs to user %s, group %s' % (user, group)
+    #print '# trusted users = %s; trusted groups = %s' % (tusers, tgroups)
+    kind = ('different', 'same')
+    who = ('', 'user', 'group', 'user and the group')
+    trusted = who[(user in tusers) + 2*(group in tgroups)]
+    if trusted:
+        trusted = ', but we trust the ' + trusted
+    print '# %s user, %s group%s' % (kind[user == cuser], kind[group == cgroup],
+                                     trusted)
+
+    parentui = ui.ui()
+    parentui.updateopts(debug=debug)
+    u = ui.ui(parentui=parentui)
+    u.readconfig('.hg/hgrc')
+    for name, path in u.configitems('paths'):
+        print '   ', name, '=', path
+    print
+
+    return u
+
+os.mkdir('repo')
+os.chdir('repo')
+os.mkdir('.hg')
+f = open('.hg/hgrc', 'w')
+f.write('[paths]\n')
+f.write('local = /another/path\n\n')
+f.close()
+
+#print '# Everything is run by user foo, group bar\n'
+
+# same user, same group
+testui()
+# same user, different group
+testui(group='def')
+# different user, same group
+testui(user='abc')
+# ... but we trust the group
+testui(user='abc', tgroups=['bar'])
+# different user, different group
+testui(user='abc', group='def')
+# ... but we trust the user
+testui(user='abc', group='def', tusers=['abc'])
+# ... but we trust the group
+testui(user='abc', group='def', tgroups=['def'])
+# ... but we trust the user and the group
+testui(user='abc', group='def', tusers=['abc'], tgroups=['def'])
+# ... but we trust all users
+print '# we trust all users'
+testui(user='abc', group='def', tusers=['*'])
+# ... but we trust all groups
+print '# we trust all groups'
+testui(user='abc', group='def', tgroups=['*'])
+# ... but we trust the whole universe
+print '# we trust all users and groups'
+testui(user='abc', group='def', tusers=['*'], tgroups=['*'])
+# ... check that users and groups are in different namespaces
+print "# we don't get confused by users and groups with the same name"
+testui(user='abc', group='def', tusers=['def'], tgroups=['abc'])
+# ... lists of user names work
+print "# list of user names"
+testui(user='abc', group='def', tusers=['foo', 'xyz', 'abc', 'bleh'],
+       tgroups=['bar', 'baz', 'qux'])
+# ... lists of group names work
+print "# list of group names"
+testui(user='abc', group='def', tusers=['foo', 'xyz', 'bleh'],
+       tgroups=['bar', 'def', 'baz', 'qux'])
+
+print "# Can't figure out the name of the user running this process"
+testui(user='abc', group='def', cuser=None)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-trusted.py.out	Thu Oct 26 19:25:44 2006 +0200
@@ -0,0 +1,67 @@
+# same user, same group
+    global = /some/path
+    local = /another/path
+
+# same user, different group
+    global = /some/path
+    local = /another/path
+
+# different user, same group
+Not reading file .hg/hgrc from untrusted user abc, group bar
+    global = /some/path
+
+# different user, same group, but we trust the group
+    global = /some/path
+    local = /another/path
+
+# different user, different group
+Not reading file .hg/hgrc from untrusted user abc, group def
+    global = /some/path
+
+# different user, different group, but we trust the user
+    global = /some/path
+    local = /another/path
+
+# different user, different group, but we trust the group
+    global = /some/path
+    local = /another/path
+
+# different user, different group, but we trust the user and the group
+    global = /some/path
+    local = /another/path
+
+# we trust all users
+# different user, different group
+    global = /some/path
+    local = /another/path
+
+# we trust all groups
+# different user, different group
+    global = /some/path
+    local = /another/path
+
+# we trust all users and groups
+# different user, different group
+    global = /some/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
+    global = /some/path
+
+# list of user names
+# different user, different group, but we trust the user
+    global = /some/path
+    local = /another/path
+
+# list of group names
+# different user, different group, but we trust the group
+    global = /some/path
+    local = /another/path
+
+# Can't figure out the name of the user running this process
+# different user, different group
+    global = /some/path
+    local = /another/path
+