Merge lp:~parthm/bzr/376388-dot-bazaar-ownership into lp:bzr

Proposed by Parth Malwankar
Status: Merged
Merged at revision: not available
Proposed branch: lp:~parthm/bzr/376388-dot-bazaar-ownership
Merge into: lp:bzr
Diff against target: 221 lines (+112/-3)
7 files modified
NEWS (+5/-0)
bzrlib/config.py (+3/-2)
bzrlib/osutils.py (+47/-0)
bzrlib/tests/features.py (+10/-0)
bzrlib/tests/test_osutils.py (+41/-0)
bzrlib/trace.py (+4/-1)
doc/developers/testing.txt (+2/-0)
To merge this branch: bzr merge lp:~parthm/bzr/376388-dot-bazaar-ownership
Reviewer Review Type Date Requested Status
Martin Pool Approve
Vincent Ladeuil Needs Fixing
Martin Packman (community) Needs Fixing
Review via email: mp+19691@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Parth Malwankar (parthm) wrote :

This patch fixes bug #376388 by ensuring the .bazaar,
.bazaar/bazaar.conf and .bzr.log inherit usr and grp ownership
from the containing folder.

Following functions are added to osutils:
* copy_ownership: copies usr/grp ownership from src file/dir to dst file/dir
* mkdir: wraps os.mkdir and adds an optional arg ownership_src. When provided,
  the newly created dir is given ownership based on ownership_src.
* open: wraps __builtin__.open and adds an optional arg ownership_src. When provided,
  the newly created file is given ownership based on ownership_src.
* parent_dir: wraps os.path.dirname handling the special case of dirname returning ''
  by returning '.' instead.

config.py and trace.py are updated to use osutils.{mkdir,open} rather than the
base functions during creation of above mentioned files.

Whitebox tests are added to test_osutils.py to verify mkdir, open and parent_dir.
Manual testing for done using "sudo bzr whoami a@b".

ChmodFeature was added to tests/__init__.py to ensure os.chmod support for test
cases.

This proposal was originally based of 2.0:
https://code.launchpad.net/~parthm/bzr/2.0_376388_dot_bazaar_ownership/+merge/19593/
but is resubmitted now as it is meant to go into trunk and testing requires
overrideAttr support that is not available in 2.0 branch.

Revision history for this message
Parth Malwankar (parthm) wrote :

Similar issue filed as bug #524306

Revision history for this message
Martin Packman (gz) wrote :

As mentioned in a different (and now deleted?) merge request, this clashes with that change as they both try and create a shadow of a builtin function in osutils, which I think is bad style.

In this case, I don't see why:

+ ownership_src = osutils.parent_dir(_bzr_log_filename)
+ bzr_log_file = osutils.open(_bzr_log_filename, 'at', buffering, ownership_src)

Is preferable to the more obvious:

+ bzr_log_file = open(_bzr_log_filename, 'at', 0) # unbuffered
+ osutils.copy_ownership(_bzr_log_filename,
+ osutils.parent_dir(_bzr_log_filename))

Would also be nice to avoid this junk entirely on windows, but that doesn't matter much.

review: Needs Fixing
Revision history for this message
Parth Malwankar (parthm) wrote :

> As mentioned in a different (and now deleted?) merge request, this clashes
> with that change as they both try and create a shadow of a builtin function in
> osutils, which I think is bad style.
>
> In this case, I don't see why:
>
> + ownership_src = osutils.parent_dir(_bzr_log_filename)
> + bzr_log_file = osutils.open(_bzr_log_filename, 'at', buffering,
> ownership_src)
>

Good point. Maybe we should have open_with_ownership (or something similar)
for which ownership_src is mandatory. This will keep the usage clean and
also not be confused with the builtin.

Revision history for this message
Vincent Ladeuil (vila) wrote :

You have some PEP8 issues (see doc/developpers/HACKING.txt section Code layout),
namely:
- no spaces around '='
27 + f = osutils.open_with_ownership(path, 'wb', ownership_src = parent)

- lines should be no more than 79 characters (watch for your tests docstrings
  in particular)

58 +def parent_dir(path):

This looks weird, why not just embed that code in copy_ownership like your docstring
seem to imply ?

87 +def open_with_ownership(filename, mode='r', bufsize=-1, ownership_src=None):
88 + """This function wraps the python builtin open. filename, mode and bufsize

From "Code Layout" cited above:
One often-missed requirement is that the first line of docstrings
should be a self-contained one-sentence summary.

135 + # we can't overrideAttr os.chown on OS that doesn't support chown
136 + if os.name == 'posix' and hasattr(os, 'chown'):
137 + self.overrideAttr(os, 'chown', self._dummy_chown)

You don't need to protect the call since your tests already depends on tests.ChownFeature.

145 + def test_mkdir_with_ownership_no_chown(self):
146 + """ensure that osutils.mkdir_with_ownership does not chown without ownership_src arg"""
147 + self.requireFeature(tests.ChownFeature)

You can avoid avoid duplicating that check for each test by doing:

class TestCreationOps(tests.TestCaseInTempDir):

    _test_needs_features = [tests.ChownFeature]

142 + def _dummy_chown(self, path, uid, gid):
143 + self.path, self.uid, self.gid = path, uid, gid

Hmm, using a dummy here is questionable since you don't really test calling chown(),
I'd prefer a recording variant like:

def _instrumented_chown(self, path, uid, gid):
    self.calls.append((path, uid, gid))
    return os.chown(path, uid, gid)

so you can both check that it was called *and* verify what was done.

180 + ownsrc = '/'

Ouch ! You're playing with fire here, I realize you may want to test with a different
uid/gid than the current user but... you'd better create such a reference file explicitly.

On the overall, I'm still a bit doubtful on the whole approach, it seems fine for root
but what happens if we use 'sudo -Ujoe' ?

review: Needs Fixing
Revision history for this message
Parth Malwankar (parthm) wrote :

Thanks for your comments Vincent.
I have updated the code based on your review comments.

The testing approach ( http://pastebin.com/CKhiAgL4 ) didn't work ( http://pastebin.com/ZPk2XGvv ), so it seems like we are stuck with this approach for now unless more ideas come through.

Based on the discussion vila and I had on IRC it seems that user foo cannot change ownership to user bar and chown would work only under sudo. Can such a situation arise? Not having a thorough test case or approach that works under all situations seems somewhat risky.

As discussed I am setting the status to Needs Review so that we can thrash out the solution some more as needed. I would be happy to try out any other ideas. Based on the outcome we could either update the patch or reject it.

Revision history for this message
Vincent Ladeuil (vila) wrote :
Revision history for this message
Parth Malwankar (parthm) wrote :

> See https://bugs.edge.launchpad.net/bzr/+bug/376388/comments/16,
> only root can use chown.

One approach could be to catch OSError during chown. That way
the ownership would be correct under sudo and in the normal
case the user won't be bothered.

Revision history for this message
Martin Pool (mbp) wrote :

56
57 +def copy_ownership(dst, src=None):
58 + """copy usr/grp ownership from src file/dir to dst file/dir.
59 + If src is None, the containing directory is used as source."""
60 + if os.name != 'posix':
61 + return False
62 +
63 + if src == None:
64 + src = os.path.dirname(dst)
65 + if src == '':
66 + src = '.'
67 +
68 + try:
69 + s = os.stat(src)
70 + os.chown(dst, s.st_uid, s.st_gid)
71 + except OSError, e:
72 + trace.warning("IOError: %s. Unable to copy ownership from '%s' to '%s'" % (e, src, dst))
73 + return True
74 +

Your approach of catching OSError is fne.

Normally we would write the message as:

 Unable to copy ownership from "%s" to "%s": %s

with the message at the end.

I think rather than checking os.name it would be better to check if os.chown exists, ie

chown = getattr(os, 'chown')
if chown is None: return

Why does this return a boolean? If it needs to, you should document and maybe test the value. Otherwise, don't return anything.

For consistency our docstrings have:

 * Sentence capitalization and punctuation
 * If possible a single sentence on the first line
 * A blank line between paragraphs
 * The closing quotes on a separate line

See http://www.python.org/dev/peps/pep-0008/ and the developer docs

114 +
115 +class _ChownFeature(tests.Feature):
116 + """os.chown is supported"""
117 +
118 + def _probe(self):
119 + return os.name == 'posix' and hasattr(os, 'chown')
120 +
121 +ChownFeature = _ChownFeature()
122 +

Normally the instance of the feature object should be named like an instance, ie lowercase chown_feature. Sorry this is confusing, we changed it a while ago.

Thanks!

Martin

review: Needs Fixing
Revision history for this message
Parth Malwankar (parthm) wrote :

Thanks for the review Martin.
I have made the relevant changes.

Revision history for this message
Martin Pool (mbp) :
review: Approve
Revision history for this message
Martin Pool (mbp) wrote :

I'll resolve the conflicts, tweak the feature name to be lowercase, and submit this.

Revision history for this message
Parth Malwankar (parthm) wrote :

Thanks Martin.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-03-11 05:38:54 +0000
+++ NEWS 2010-03-11 06:39:25 +0000
@@ -120,6 +120,11 @@
120* Tolerate patches with leading noise in ``bzr-handle-patch``.120* Tolerate patches with leading noise in ``bzr-handle-patch``.
121 (Toshio Kuratomi, Martin Pool, #502076)121 (Toshio Kuratomi, Martin Pool, #502076)
122122
123* ``.bazaar``, ``.bazaar/bazaar.conf`` and ``.bzr.log`` inherit user and
124 group ownership from the containing directory. This allow bzr to work
125 better with sudo.
126 (Parth Malwankar, #376388)
127
123API Changes128API Changes
124***********129***********
125130
126131
=== modified file 'bzrlib/config.py'
--- bzrlib/config.py 2010-02-23 07:43:11 +0000
+++ bzrlib/config.py 2010-03-11 06:39:25 +0000
@@ -510,7 +510,8 @@
510 self._write_config_file()510 self._write_config_file()
511511
512 def _write_config_file(self):512 def _write_config_file(self):
513 f = open(self._get_filename(), 'wb')513 path = self._get_filename()
514 f = osutils.open_with_ownership(path, 'wb')
514 self._get_parser().write(f)515 self._get_parser().write(f)
515 f.close()516 f.close()
516517
@@ -809,7 +810,7 @@
809 trace.mutter('creating config parent directory: %r', parent_dir)810 trace.mutter('creating config parent directory: %r', parent_dir)
810 os.mkdir(parent_dir)811 os.mkdir(parent_dir)
811 trace.mutter('creating config directory: %r', path)812 trace.mutter('creating config directory: %r', path)
812 os.mkdir(path)813 osutils.mkdir_with_ownership(path)
813814
814815
815def config_dir():816def config_dir():
816817
=== modified file 'bzrlib/osutils.py'
--- bzrlib/osutils.py 2010-03-11 04:33:41 +0000
+++ bzrlib/osutils.py 2010-03-11 06:39:25 +0000
@@ -51,7 +51,9 @@
51 cache_utf8,51 cache_utf8,
52 errors,52 errors,
53 win32utils,53 win32utils,
54 trace,
54 )55 )
56
55""")57""")
5658
57# sha and md5 modules are deprecated in python2.6 but hashlib is available as59# sha and md5 modules are deprecated in python2.6 but hashlib is available as
@@ -1804,6 +1806,51 @@
1804 real_handlers[kind](abspath, relpath)1806 real_handlers[kind](abspath, relpath)
18051807
18061808
1809def copy_ownership(dst, src=None):
1810 """Copy usr/grp ownership from src file/dir to dst file/dir.
1811
1812 If src is None, the containing directory is used as source. If chown
1813 fails, the error is ignored and a warning is printed.
1814 """
1815 has_chown = getattr(os, 'chown')
1816 if has_chown is None: return
1817
1818 if src == None:
1819 src = os.path.dirname(dst)
1820 if src == '':
1821 src = '.'
1822
1823 try:
1824 s = os.stat(src)
1825 os.chown(dst, s.st_uid, s.st_gid)
1826 except OSError, e:
1827 trace.warning("Unable to copy ownership from '%s' to '%s': IOError: %s." % (src, dst, e))
1828
1829
1830def mkdir_with_ownership(path, ownership_src=None):
1831 """Create the directory 'path' with specified ownership.
1832
1833 If ownership_src is given, copies (chown) usr/grp ownership
1834 from 'ownership_src' to 'path'. If ownership_src is None, use the
1835 containing dir ownership.
1836 """
1837 os.mkdir(path)
1838 copy_ownership(path, ownership_src)
1839
1840
1841def open_with_ownership(filename, mode='r', bufsize=-1, ownership_src=None):
1842 """Open the file 'filename' with the specified ownership.
1843
1844 If ownership_src is specified, copy usr/grp ownership from ownership_src
1845 to filename. If ownership_src is None, copy ownership from containing
1846 directory.
1847 Returns the opened file object.
1848 """
1849 f = open(filename, mode, bufsize)
1850 copy_ownership(filename, ownership_src)
1851 return f
1852
1853
1807def path_prefix_key(path):1854def path_prefix_key(path):
1808 """Generate a prefix-order path key for path.1855 """Generate a prefix-order path key for path.
18091856
18101857
=== modified file 'bzrlib/tests/features.py'
--- bzrlib/tests/features.py 2010-02-23 07:43:11 +0000
+++ bzrlib/tests/features.py 2010-03-11 06:39:25 +0000
@@ -14,6 +14,7 @@
14# along with this program; if not, write to the Free Software14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1616
17import os
1718
18from bzrlib import tests19from bzrlib import tests
19from bzrlib.symbol_versioning import deprecated_in20from bzrlib.symbol_versioning import deprecated_in
@@ -23,3 +24,12 @@
23paramiko = tests.ModuleAvailableFeature('paramiko')24paramiko = tests.ModuleAvailableFeature('paramiko')
24pycurl = tests.ModuleAvailableFeature('pycurl')25pycurl = tests.ModuleAvailableFeature('pycurl')
25subunit = tests.ModuleAvailableFeature('subunit')26subunit = tests.ModuleAvailableFeature('subunit')
27
28class _ChownFeature(tests.Feature):
29 """os.chown is supported"""
30
31 def _probe(self):
32 return os.name == 'posix' and hasattr(os, 'chown')
33
34chown_feature = _ChownFeature()
35
2636
=== modified file 'bzrlib/tests/test_osutils.py'
--- bzrlib/tests/test_osutils.py 2010-02-23 07:43:11 +0000
+++ bzrlib/tests/test_osutils.py 2010-03-11 06:39:25 +0000
@@ -33,6 +33,7 @@
33 win32utils,33 win32utils,
34 )34 )
35from bzrlib.tests import (35from bzrlib.tests import (
36 features,
36 file_utils,37 file_utils,
37 test__walkdirs_win32,38 test__walkdirs_win32,
38 )39 )
@@ -1975,3 +1976,43 @@
1975 del os.environ['COLUMNS']1976 del os.environ['COLUMNS']
1976 # Whatever the result is, if we don't raise an exception, it's ok.1977 # Whatever the result is, if we don't raise an exception, it's ok.
1977 osutils.terminal_width()1978 osutils.terminal_width()
1979
1980class TestCreationOps(tests.TestCaseInTempDir):
1981 _test_needs_features = [features.chown_feature]
1982
1983 def setUp(self):
1984 tests.TestCaseInTempDir.setUp(self)
1985 self.overrideAttr(os, 'chown', self._dummy_chown)
1986
1987 # params set by call to _dummy_chown
1988 self.path = self.uid = self.gid = None
1989
1990 def _dummy_chown(self, path, uid, gid):
1991 self.path, self.uid, self.gid = path, uid, gid
1992
1993 def test_mkdir_with_ownership_chown(self):
1994 """Ensure that osutils.mkdir_with_ownership chowns correctly with ownership_src.
1995 """
1996 ownsrc = '/'
1997 osutils.mkdir_with_ownership('foo', ownsrc)
1998
1999 s = os.stat(ownsrc)
2000 self.assertEquals(self.path, 'foo')
2001 self.assertEquals(self.uid, s.st_uid)
2002 self.assertEquals(self.gid, s.st_gid)
2003
2004 def test_open_with_ownership_chown(self):
2005 """Ensure that osutils.open_with_ownership chowns correctly with ownership_src.
2006 """
2007 ownsrc = '/'
2008 f = osutils.open_with_ownership('foo', 'w', ownership_src=ownsrc)
2009
2010 # do a test write and close
2011 f.write('hello')
2012 f.close()
2013
2014 s = os.stat(ownsrc)
2015 self.assertEquals(self.path, 'foo')
2016 self.assertEquals(self.uid, s.st_uid)
2017 self.assertEquals(self.gid, s.st_gid)
2018
19782019
=== modified file 'bzrlib/trace.py'
--- bzrlib/trace.py 2010-02-24 01:20:51 +0000
+++ bzrlib/trace.py 2010-03-11 06:39:25 +0000
@@ -241,14 +241,17 @@
241 _bzr_log_filename = _get_bzr_log_filename()241 _bzr_log_filename = _get_bzr_log_filename()
242 _rollover_trace_maybe(_bzr_log_filename)242 _rollover_trace_maybe(_bzr_log_filename)
243 try:243 try:
244 bzr_log_file = open(_bzr_log_filename, 'at', buffering=0) # unbuffered244 buffering = 0 # unbuffered
245 bzr_log_file = osutils.open_with_ownership(_bzr_log_filename, 'at', buffering)
245 # bzr_log_file.tell() on windows always return 0 until some writing done246 # bzr_log_file.tell() on windows always return 0 until some writing done
246 bzr_log_file.write('\n')247 bzr_log_file.write('\n')
247 if bzr_log_file.tell() <= 2:248 if bzr_log_file.tell() <= 2:
248 bzr_log_file.write("this is a debug log for diagnosing/reporting problems in bzr\n")249 bzr_log_file.write("this is a debug log for diagnosing/reporting problems in bzr\n")
249 bzr_log_file.write("you can delete or truncate this file, or include sections in\n")250 bzr_log_file.write("you can delete or truncate this file, or include sections in\n")
250 bzr_log_file.write("bug reports to https://bugs.launchpad.net/bzr/+filebug\n\n")251 bzr_log_file.write("bug reports to https://bugs.launchpad.net/bzr/+filebug\n\n")
252
251 return bzr_log_file253 return bzr_log_file
254
252 except IOError, e:255 except IOError, e:
253 # If we are failing to open the log, then most likely logging has not256 # If we are failing to open the log, then most likely logging has not
254 # been set up yet. So we just write to stderr rather than using257 # been set up yet. So we just write to stderr rather than using
255258
=== modified file 'doc/developers/testing.txt'
--- doc/developers/testing.txt 2010-02-28 10:08:29 +0000
+++ doc/developers/testing.txt 2010-03-11 06:39:25 +0000
@@ -576,6 +576,8 @@
576 - UnicodeFilenameFeature576 - UnicodeFilenameFeature
577 - FTPServerFeature577 - FTPServerFeature
578 - CaseInsensitiveFilesystemFeature.578 - CaseInsensitiveFilesystemFeature.
579 - ChownFeature: The test can rely on OS being POSIX and python
580 supporting os.chown.
579581
580582
581Defining a new feature that tests can require583Defining a new feature that tests can require