diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- a/doc/hgrc.5.txt +++ b/doc/hgrc.5.txt @@ -50,6 +50,8 @@ installed. 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 ------ @@ -349,6 +351,16 @@ server:: 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. 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;; diff --git a/mercurial/ui.py b/mercurial/ui.py --- a/mercurial/ui.py +++ b/mercurial/ui.py @@ -19,6 +19,8 @@ class ui(object): # this is the parent of all ui children self.parentui = None self.readhooks = list(readhooks) + self.trusted_users = {} + self.trusted_groups = {} self.cdata = ConfigParser.SafeConfigParser() self.readconfig(util.rcpath()) @@ -37,6 +39,8 @@ class ui(object): # parentui may point to an ui object which is already a child self.parentui = parentui.parentui or parentui self.readhooks = list(parentui.readhooks or readhooks) + self.trusted_users = parentui.trusted_users.copy() + self.trusted_groups = parentui.trusted_groups.copy() parent_cdata = self.parentui.cdata self.cdata = ConfigParser.SafeConfigParser(parent_cdata.defaults()) # make interpolation work @@ -72,7 +76,22 @@ class ui(object): fn = [fn] for f in fn: try: - self.cdata.read(f) + fp = open(f) + except IOError: + continue + if ((self.trusted_users or self.trusted_groups) and + '*' not in self.trusted_users and + '*' not in self.trusted_groups): + st = util.fstat(fp) + user = util.username(st.st_uid) + group = util.groupname(st.st_gid) + if (user not in self.trusted_users and + group not in self.trusted_groups): + self.warn(_('not reading file %s from untrusted ' + 'user %s, group %s\n') % (f, user, group)) + continue + try: + self.cdata.readfp(fp, f) except ConfigParser.ParsingError, inst: raise util.Abort(_("Failed to parse %s\n%s") % (f, inst)) # translate paths relative to root (or home) into absolute paths @@ -81,6 +100,13 @@ class ui(object): for name, path in self.configitems("paths"): if path and "://" not in path and not os.path.isabs(path): self.cdata.set("paths", name, os.path.join(root, path)) + 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 for hook in self.readhooks: hook(self) diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -15,7 +15,7 @@ platform-specific details from the core. from i18n import gettext as _ from demandload import * demandload(globals(), "cStringIO errno getpass popen2 re shutil sys tempfile") -demandload(globals(), "os threading time") +demandload(globals(), "os threading time pwd grp") # used by parsedate defaultdateformats = ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', @@ -509,6 +509,38 @@ def getuser(): raise Abort(_('user name not available - set USERNAME ' 'environment variable')) +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: + # force an ImportError if there's no module pwd + getpwuid = pwd.getpwuid + if uid is None: + uid = os.getuid() + try: + return 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: + # force an ImportError if there's no module grp + getgrgid = grp.getgrgid + if gid is None: + gid = os.getgid() + try: + return getgrgid(gid)[0] + except KeyError: + return str(gid) + except ImportError: + return None + # Platform specific variants if os.name == 'nt': demandload(globals(), "msvcrt") diff --git a/tests/test-trusted.py b/tests/test-trusted.py new file mode 100755 --- /dev/null +++ b/tests/test-trusted.py @@ -0,0 +1,112 @@ +#!/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'): + # 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() + 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) diff --git a/tests/test-trusted.py.out b/tests/test-trusted.py.out new file mode 100644 --- /dev/null +++ b/tests/test-trusted.py.out @@ -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 +