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
1=== modified file 'NEWS'
2--- NEWS 2010-03-11 05:38:54 +0000
3+++ NEWS 2010-03-11 06:39:25 +0000
4@@ -120,6 +120,11 @@
5 * Tolerate patches with leading noise in ``bzr-handle-patch``.
6 (Toshio Kuratomi, Martin Pool, #502076)
7
8+* ``.bazaar``, ``.bazaar/bazaar.conf`` and ``.bzr.log`` inherit user and
9+ group ownership from the containing directory. This allow bzr to work
10+ better with sudo.
11+ (Parth Malwankar, #376388)
12+
13 API Changes
14 ***********
15
16
17=== modified file 'bzrlib/config.py'
18--- bzrlib/config.py 2010-02-23 07:43:11 +0000
19+++ bzrlib/config.py 2010-03-11 06:39:25 +0000
20@@ -510,7 +510,8 @@
21 self._write_config_file()
22
23 def _write_config_file(self):
24- f = open(self._get_filename(), 'wb')
25+ path = self._get_filename()
26+ f = osutils.open_with_ownership(path, 'wb')
27 self._get_parser().write(f)
28 f.close()
29
30@@ -809,7 +810,7 @@
31 trace.mutter('creating config parent directory: %r', parent_dir)
32 os.mkdir(parent_dir)
33 trace.mutter('creating config directory: %r', path)
34- os.mkdir(path)
35+ osutils.mkdir_with_ownership(path)
36
37
38 def config_dir():
39
40=== modified file 'bzrlib/osutils.py'
41--- bzrlib/osutils.py 2010-03-11 04:33:41 +0000
42+++ bzrlib/osutils.py 2010-03-11 06:39:25 +0000
43@@ -51,7 +51,9 @@
44 cache_utf8,
45 errors,
46 win32utils,
47+ trace,
48 )
49+
50 """)
51
52 # sha and md5 modules are deprecated in python2.6 but hashlib is available as
53@@ -1804,6 +1806,51 @@
54 real_handlers[kind](abspath, relpath)
55
56
57+def copy_ownership(dst, src=None):
58+ """Copy usr/grp ownership from src file/dir to dst file/dir.
59+
60+ If src is None, the containing directory is used as source. If chown
61+ fails, the error is ignored and a warning is printed.
62+ """
63+ has_chown = getattr(os, 'chown')
64+ if has_chown is None: return
65+
66+ if src == None:
67+ src = os.path.dirname(dst)
68+ if src == '':
69+ src = '.'
70+
71+ try:
72+ s = os.stat(src)
73+ os.chown(dst, s.st_uid, s.st_gid)
74+ except OSError, e:
75+ trace.warning("Unable to copy ownership from '%s' to '%s': IOError: %s." % (src, dst, e))
76+
77+
78+def mkdir_with_ownership(path, ownership_src=None):
79+ """Create the directory 'path' with specified ownership.
80+
81+ If ownership_src is given, copies (chown) usr/grp ownership
82+ from 'ownership_src' to 'path'. If ownership_src is None, use the
83+ containing dir ownership.
84+ """
85+ os.mkdir(path)
86+ copy_ownership(path, ownership_src)
87+
88+
89+def open_with_ownership(filename, mode='r', bufsize=-1, ownership_src=None):
90+ """Open the file 'filename' with the specified ownership.
91+
92+ If ownership_src is specified, copy usr/grp ownership from ownership_src
93+ to filename. If ownership_src is None, copy ownership from containing
94+ directory.
95+ Returns the opened file object.
96+ """
97+ f = open(filename, mode, bufsize)
98+ copy_ownership(filename, ownership_src)
99+ return f
100+
101+
102 def path_prefix_key(path):
103 """Generate a prefix-order path key for path.
104
105
106=== modified file 'bzrlib/tests/features.py'
107--- bzrlib/tests/features.py 2010-02-23 07:43:11 +0000
108+++ bzrlib/tests/features.py 2010-03-11 06:39:25 +0000
109@@ -14,6 +14,7 @@
110 # along with this program; if not, write to the Free Software
111 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
112
113+import os
114
115 from bzrlib import tests
116 from bzrlib.symbol_versioning import deprecated_in
117@@ -23,3 +24,12 @@
118 paramiko = tests.ModuleAvailableFeature('paramiko')
119 pycurl = tests.ModuleAvailableFeature('pycurl')
120 subunit = tests.ModuleAvailableFeature('subunit')
121+
122+class _ChownFeature(tests.Feature):
123+ """os.chown is supported"""
124+
125+ def _probe(self):
126+ return os.name == 'posix' and hasattr(os, 'chown')
127+
128+chown_feature = _ChownFeature()
129+
130
131=== modified file 'bzrlib/tests/test_osutils.py'
132--- bzrlib/tests/test_osutils.py 2010-02-23 07:43:11 +0000
133+++ bzrlib/tests/test_osutils.py 2010-03-11 06:39:25 +0000
134@@ -33,6 +33,7 @@
135 win32utils,
136 )
137 from bzrlib.tests import (
138+ features,
139 file_utils,
140 test__walkdirs_win32,
141 )
142@@ -1975,3 +1976,43 @@
143 del os.environ['COLUMNS']
144 # Whatever the result is, if we don't raise an exception, it's ok.
145 osutils.terminal_width()
146+
147+class TestCreationOps(tests.TestCaseInTempDir):
148+ _test_needs_features = [features.chown_feature]
149+
150+ def setUp(self):
151+ tests.TestCaseInTempDir.setUp(self)
152+ self.overrideAttr(os, 'chown', self._dummy_chown)
153+
154+ # params set by call to _dummy_chown
155+ self.path = self.uid = self.gid = None
156+
157+ def _dummy_chown(self, path, uid, gid):
158+ self.path, self.uid, self.gid = path, uid, gid
159+
160+ def test_mkdir_with_ownership_chown(self):
161+ """Ensure that osutils.mkdir_with_ownership chowns correctly with ownership_src.
162+ """
163+ ownsrc = '/'
164+ osutils.mkdir_with_ownership('foo', ownsrc)
165+
166+ s = os.stat(ownsrc)
167+ self.assertEquals(self.path, 'foo')
168+ self.assertEquals(self.uid, s.st_uid)
169+ self.assertEquals(self.gid, s.st_gid)
170+
171+ def test_open_with_ownership_chown(self):
172+ """Ensure that osutils.open_with_ownership chowns correctly with ownership_src.
173+ """
174+ ownsrc = '/'
175+ f = osutils.open_with_ownership('foo', 'w', ownership_src=ownsrc)
176+
177+ # do a test write and close
178+ f.write('hello')
179+ f.close()
180+
181+ s = os.stat(ownsrc)
182+ self.assertEquals(self.path, 'foo')
183+ self.assertEquals(self.uid, s.st_uid)
184+ self.assertEquals(self.gid, s.st_gid)
185+
186
187=== modified file 'bzrlib/trace.py'
188--- bzrlib/trace.py 2010-02-24 01:20:51 +0000
189+++ bzrlib/trace.py 2010-03-11 06:39:25 +0000
190@@ -241,14 +241,17 @@
191 _bzr_log_filename = _get_bzr_log_filename()
192 _rollover_trace_maybe(_bzr_log_filename)
193 try:
194- bzr_log_file = open(_bzr_log_filename, 'at', buffering=0) # unbuffered
195+ buffering = 0 # unbuffered
196+ bzr_log_file = osutils.open_with_ownership(_bzr_log_filename, 'at', buffering)
197 # bzr_log_file.tell() on windows always return 0 until some writing done
198 bzr_log_file.write('\n')
199 if bzr_log_file.tell() <= 2:
200 bzr_log_file.write("this is a debug log for diagnosing/reporting problems in bzr\n")
201 bzr_log_file.write("you can delete or truncate this file, or include sections in\n")
202 bzr_log_file.write("bug reports to https://bugs.launchpad.net/bzr/+filebug\n\n")
203+
204 return bzr_log_file
205+
206 except IOError, e:
207 # If we are failing to open the log, then most likely logging has not
208 # been set up yet. So we just write to stderr rather than using
209
210=== modified file 'doc/developers/testing.txt'
211--- doc/developers/testing.txt 2010-02-28 10:08:29 +0000
212+++ doc/developers/testing.txt 2010-03-11 06:39:25 +0000
213@@ -576,6 +576,8 @@
214 - UnicodeFilenameFeature
215 - FTPServerFeature
216 - CaseInsensitiveFilesystemFeature.
217+ - ChownFeature: The test can rely on OS being POSIX and python
218+ supporting os.chown.
219
220
221 Defining a new feature that tests can require