Status: | Superseded |
---|---|
Proposed branch: | lp:~jelmer/bzr/safe-open |
Merge into: | lp:bzr |
Diff against target: |
819 lines (+688/-18) 8 files modified
bzrlib/branch.py (+3/-3) bzrlib/controldir.py (+16/-12) bzrlib/remote.py (+1/-1) bzrlib/safe_open.py (+307/-0) bzrlib/tests/__init__.py (+1/-0) bzrlib/tests/test_safe_open.py (+356/-0) bzrlib/workingtree.py (+2/-2) doc/en/release-notes/bzr-2.5.txt (+2/-0) |
To merge this branch: | bzr merge lp:~jelmer/bzr/safe-open |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
bzr-core | Pending | ||
Review via email: mp+86643@code.launchpad.net |
This proposal has been superseded by a proposal from 2011-12-22.
Commit message
Description of the change
Import the safe_open code from launchpad.
This code allows the user to open branches while checking all URLs that are
opened with a policy object. This code is generic, and could be useful for
other users than Launchpad too.
To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'bzrlib/branch.py' |
2 | --- bzrlib/branch.py 2011-12-21 20:43:29 +0000 |
3 | +++ bzrlib/branch.py 2011-12-22 01:48:24 +0000 |
4 | @@ -180,8 +180,8 @@ |
5 | For instance, if the branch is at URL/.bzr/branch, |
6 | Branch.open(URL) -> a Branch instance. |
7 | """ |
8 | - control = controldir.ControlDir.open(base, _unsupported, |
9 | - possible_transports=possible_transports) |
10 | + control = controldir.ControlDir.open(base, |
11 | + possible_transports=possible_transports, _unsupported=_unsupported) |
12 | return control.open_branch(unsupported=_unsupported, |
13 | possible_transports=possible_transports) |
14 | |
15 | @@ -3008,7 +3008,7 @@ |
16 | config=conf) |
17 | if stacked_url is None: |
18 | raise errors.NotStacked(self) |
19 | - return stacked_url |
20 | + return stacked_url.encode('utf-8') |
21 | |
22 | @needs_read_lock |
23 | def get_rev_id(self, revno, history=None): |
24 | |
25 | === modified file 'bzrlib/controldir.py' |
26 | --- bzrlib/controldir.py 2011-12-19 13:23:58 +0000 |
27 | +++ bzrlib/controldir.py 2011-12-22 01:48:24 +0000 |
28 | @@ -664,17 +664,19 @@ |
29 | return klass.open(base, _unsupported=True) |
30 | |
31 | @classmethod |
32 | - def open(klass, base, _unsupported=False, possible_transports=None): |
33 | + def open(klass, base, possible_transports=None, probers=None, |
34 | + _unsupported=False): |
35 | """Open an existing controldir, rooted at 'base' (url). |
36 | |
37 | :param _unsupported: a private parameter to the ControlDir class. |
38 | """ |
39 | t = _mod_transport.get_transport(base, possible_transports) |
40 | - return klass.open_from_transport(t, _unsupported=_unsupported) |
41 | + return klass.open_from_transport(t, probers=probers, |
42 | + _unsupported=_unsupported) |
43 | |
44 | @classmethod |
45 | def open_from_transport(klass, transport, _unsupported=False, |
46 | - _server_formats=True): |
47 | + probers=None): |
48 | """Open a controldir within a particular directory. |
49 | |
50 | :param transport: Transport containing the controldir. |
51 | @@ -686,8 +688,8 @@ |
52 | # the redirections. |
53 | base = transport.base |
54 | def find_format(transport): |
55 | - return transport, ControlDirFormat.find_format( |
56 | - transport, _server_formats=_server_formats) |
57 | + return transport, ControlDirFormat.find_format(transport, |
58 | + probers=probers) |
59 | |
60 | def redirected(transport, e, redirection_notice): |
61 | redirected_transport = transport._redirected_to(e.source, e.target) |
62 | @@ -1110,22 +1112,24 @@ |
63 | return self.get_format_description().rstrip() |
64 | |
65 | @classmethod |
66 | + def all_probers(klass): |
67 | + return klass._probers + klass._server_probers |
68 | + |
69 | + @classmethod |
70 | def known_formats(klass): |
71 | """Return all the known formats. |
72 | """ |
73 | result = set() |
74 | - for prober_kls in klass._probers + klass._server_probers: |
75 | + for prober_kls in klass.all_probers(): |
76 | result.update(prober_kls.known_formats()) |
77 | return result |
78 | |
79 | @classmethod |
80 | - def find_format(klass, transport, _server_formats=True): |
81 | + def find_format(klass, transport, probers=None): |
82 | """Return the format present at transport.""" |
83 | - if _server_formats: |
84 | - _probers = klass._server_probers + klass._probers |
85 | - else: |
86 | - _probers = klass._probers |
87 | - for prober_kls in _probers: |
88 | + if probers is None: |
89 | + probers = klass.all_probers() |
90 | + for prober_kls in probers: |
91 | prober = prober_kls() |
92 | try: |
93 | return prober.probe_transport(transport) |
94 | |
95 | === modified file 'bzrlib/remote.py' |
96 | --- bzrlib/remote.py 2011-12-18 12:46:49 +0000 |
97 | +++ bzrlib/remote.py 2011-12-22 01:48:24 +0000 |
98 | @@ -480,7 +480,7 @@ |
99 | warning('VFS BzrDir access triggered\n%s', |
100 | ''.join(traceback.format_stack())) |
101 | self._real_bzrdir = _mod_bzrdir.BzrDir.open_from_transport( |
102 | - self.root_transport, _server_formats=False) |
103 | + self.root_transport, probers=[_mod_bzrdir.BzrProber]) |
104 | self._format._network_name = \ |
105 | self._real_bzrdir._format.network_name() |
106 | |
107 | |
108 | === added file 'bzrlib/safe_open.py' |
109 | --- bzrlib/safe_open.py 1970-01-01 00:00:00 +0000 |
110 | +++ bzrlib/safe_open.py 2011-12-22 01:48:24 +0000 |
111 | @@ -0,0 +1,307 @@ |
112 | +# Copyright (C) 2011 Canonical Ltd |
113 | +# |
114 | +# This program is free software; you can redistribute it and/or modify |
115 | +# it under the terms of the GNU General Public License as published by |
116 | +# the Free Software Foundation; either version 2 of the License, or |
117 | +# (at your option) any later version. |
118 | +# |
119 | +# This program is distributed in the hope that it will be useful, |
120 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
121 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
122 | +# GNU General Public License for more details. |
123 | +# |
124 | +# You should have received a copy of the GNU General Public License |
125 | +# along with this program; if not, write to the Free Software |
126 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
127 | + |
128 | +"""Safe branch opening.""" |
129 | + |
130 | +import threading |
131 | + |
132 | +from bzrlib import ( |
133 | + errors, |
134 | + urlutils, |
135 | + ) |
136 | +from bzrlib.branch import Branch |
137 | +from bzrlib.controldir import ( |
138 | + ControlDir, |
139 | + ) |
140 | + |
141 | + |
142 | +class BadUrl(errors.BzrError): |
143 | + |
144 | + _fmt = "Tried to access a branch from bad URL %(url)s." |
145 | + |
146 | + |
147 | +class BranchReferenceForbidden(errors.BzrError): |
148 | + |
149 | + _fmt = ("Trying to mirror a branch reference and the branch type " |
150 | + "does not allow references.") |
151 | + |
152 | + |
153 | +class BranchLoopError(errors.BzrError): |
154 | + """Encountered a branch cycle. |
155 | + |
156 | + A URL may point to a branch reference or it may point to a stacked branch. |
157 | + In either case, it's possible for there to be a cycle in these references, |
158 | + and this exception is raised when we detect such a cycle. |
159 | + """ |
160 | + |
161 | + _fmt = "Encountered a branch cycle""" |
162 | + |
163 | + |
164 | +class BranchOpenPolicy(object): |
165 | + """Policy on how to open branches. |
166 | + |
167 | + In particular, a policy determines which branches are safe to open by |
168 | + checking their URLs and deciding whether or not to follow branch |
169 | + references. |
170 | + """ |
171 | + |
172 | + def should_follow_references(self): |
173 | + """Whether we traverse references when mirroring. |
174 | + |
175 | + Subclasses must override this method. |
176 | + |
177 | + If we encounter a branch reference and this returns false, an error is |
178 | + raised. |
179 | + |
180 | + :returns: A boolean to indicate whether to follow a branch reference. |
181 | + """ |
182 | + raise NotImplementedError(self.should_follow_references) |
183 | + |
184 | + def transform_fallback_location(self, branch, url): |
185 | + """Validate, maybe modify, 'url' to be used as a stacked-on location. |
186 | + |
187 | + :param branch: The branch that is being opened. |
188 | + :param url: The URL that the branch provides for its stacked-on |
189 | + location. |
190 | + :return: (new_url, check) where 'new_url' is the URL of the branch to |
191 | + actually open and 'check' is true if 'new_url' needs to be |
192 | + validated by check_and_follow_branch_reference. |
193 | + """ |
194 | + raise NotImplementedError(self.transform_fallback_location) |
195 | + |
196 | + def check_one_url(self, url): |
197 | + """Check the safety of the source URL. |
198 | + |
199 | + Subclasses must override this method. |
200 | + |
201 | + :param url: The source URL to check. |
202 | + :raise BadUrl: subclasses are expected to raise this or a subclass |
203 | + when it finds a URL it deems to be unsafe. |
204 | + """ |
205 | + raise NotImplementedError(self.check_one_url) |
206 | + |
207 | + |
208 | +class BlacklistPolicy(BranchOpenPolicy): |
209 | + """Branch policy that forbids certain URLs.""" |
210 | + |
211 | + def __init__(self, should_follow_references, unsafe_urls=None): |
212 | + if unsafe_urls is None: |
213 | + unsafe_urls = set() |
214 | + self._unsafe_urls = unsafe_urls |
215 | + self._should_follow_references = should_follow_references |
216 | + |
217 | + def should_follow_references(self): |
218 | + return self._should_follow_references |
219 | + |
220 | + def check_one_url(self, url): |
221 | + if url in self._unsafe_urls: |
222 | + raise BadUrl(url) |
223 | + |
224 | + def transform_fallback_location(self, branch, url): |
225 | + """See `BranchOpenPolicy.transform_fallback_location`. |
226 | + |
227 | + This class is not used for testing our smarter stacking features so we |
228 | + just do the simplest thing: return the URL that would be used anyway |
229 | + and don't check it. |
230 | + """ |
231 | + return urlutils.join(branch.base, url), False |
232 | + |
233 | + |
234 | +class AcceptAnythingPolicy(BlacklistPolicy): |
235 | + """Accept anything, to make testing easier.""" |
236 | + |
237 | + def __init__(self): |
238 | + super(AcceptAnythingPolicy, self).__init__(True, set()) |
239 | + |
240 | + |
241 | +class WhitelistPolicy(BranchOpenPolicy): |
242 | + """Branch policy that only allows certain URLs.""" |
243 | + |
244 | + def __init__(self, should_follow_references, allowed_urls=None, |
245 | + check=False): |
246 | + if allowed_urls is None: |
247 | + allowed_urls = [] |
248 | + self.allowed_urls = set(url.rstrip('/') for url in allowed_urls) |
249 | + self.check = check |
250 | + |
251 | + def should_follow_references(self): |
252 | + return self._should_follow_references |
253 | + |
254 | + def check_one_url(self, url): |
255 | + if url.rstrip('/') not in self.allowed_urls: |
256 | + raise BadUrl(url) |
257 | + |
258 | + def transform_fallback_location(self, branch, url): |
259 | + """See `BranchOpenPolicy.transform_fallback_location`. |
260 | + |
261 | + Here we return the URL that would be used anyway and optionally check |
262 | + it. |
263 | + """ |
264 | + return urlutils.join(branch.base, url), self.check |
265 | + |
266 | + |
267 | +class SingleSchemePolicy(BranchOpenPolicy): |
268 | + """Branch open policy that rejects URLs not on the given scheme.""" |
269 | + |
270 | + def __init__(self, allowed_scheme): |
271 | + self.allowed_scheme = allowed_scheme |
272 | + |
273 | + def should_follow_references(self): |
274 | + return True |
275 | + |
276 | + def transform_fallback_location(self, branch, url): |
277 | + return urlutils.join(branch.base, url), True |
278 | + |
279 | + def check_one_url(self, url): |
280 | + """Check that `url` is safe to open.""" |
281 | + if urlutils.URL.from_string(str(url)).scheme != self.allowed_scheme: |
282 | + raise BadUrl(url) |
283 | + |
284 | + |
285 | +class SafeBranchOpener(object): |
286 | + """Safe branch opener. |
287 | + |
288 | + All locations that are opened (stacked-on branches, references) are |
289 | + checked against a policy object. |
290 | + |
291 | + The policy object is expected to have the following methods: |
292 | + * check_one_url |
293 | + * should_follow_references |
294 | + * transform_fallback_location |
295 | + """ |
296 | + |
297 | + _threading_data = threading.local() |
298 | + |
299 | + def __init__(self, policy, probers=None): |
300 | + """Create a new SafeBranchOpener. |
301 | + |
302 | + :param policy: The opener policy to use. |
303 | + :param probers: Optional list of probers to allow. |
304 | + Defaults to local and remote bzr probers. |
305 | + """ |
306 | + self.policy = policy |
307 | + self._seen_urls = set() |
308 | + self.probers = probers |
309 | + |
310 | + @classmethod |
311 | + def install_hook(cls): |
312 | + """Install the ``transform_fallback_location`` hook. |
313 | + |
314 | + This is done at module import time, but transform_fallback_locationHook |
315 | + doesn't do anything unless the `_active_openers` threading.Local |
316 | + object has a 'opener' attribute in this thread. |
317 | + |
318 | + This is in a module-level function rather than performed at module |
319 | + level so that it can be called in setUp for testing `SafeBranchOpener` |
320 | + as bzrlib.tests.TestCase.setUp clears hooks. |
321 | + """ |
322 | + Branch.hooks.install_named_hook( |
323 | + 'transform_fallback_location', |
324 | + cls.transform_fallback_locationHook, |
325 | + 'SafeBranchOpener.transform_fallback_locationHook') |
326 | + |
327 | + def check_and_follow_branch_reference(self, url): |
328 | + """Check URL (and possibly the referenced URL) for safety. |
329 | + |
330 | + This method checks that `url` passes the policy's `check_one_url` |
331 | + method, and if `url` refers to a branch reference, it checks whether |
332 | + references are allowed and whether the reference's URL passes muster |
333 | + also -- recursively, until a real branch is found. |
334 | + |
335 | + :param url: URL to check |
336 | + :raise BranchLoopError: If the branch references form a loop. |
337 | + :raise BranchReferenceForbidden: If this opener forbids branch |
338 | + references. |
339 | + """ |
340 | + while True: |
341 | + if url in self._seen_urls: |
342 | + raise BranchLoopError() |
343 | + self._seen_urls.add(url) |
344 | + self.policy.check_one_url(url) |
345 | + next_url = self.follow_reference(url) |
346 | + if next_url is None: |
347 | + return url |
348 | + url = next_url |
349 | + if not self.policy.should_follow_references(): |
350 | + raise BranchReferenceForbidden(url) |
351 | + |
352 | + @classmethod |
353 | + def transform_fallback_locationHook(cls, branch, url): |
354 | + """Installed as the 'transform_fallback_location' Branch hook. |
355 | + |
356 | + This method calls `transform_fallback_location` on the policy object and |
357 | + either returns the url it provides or passes it back to |
358 | + check_and_follow_branch_reference. |
359 | + """ |
360 | + try: |
361 | + opener = getattr(cls._threading_data, "opener") |
362 | + except AttributeError: |
363 | + return url |
364 | + new_url, check = opener.policy.transform_fallback_location(branch, url) |
365 | + if check: |
366 | + return opener.check_and_follow_branch_reference(new_url) |
367 | + else: |
368 | + return new_url |
369 | + |
370 | + def run_with_transform_fallback_location_hook_installed( |
371 | + self, callable, *args, **kw): |
372 | + assert (self.transform_fallback_locationHook in |
373 | + Branch.hooks['transform_fallback_location']) |
374 | + self._threading_data.opener = self |
375 | + try: |
376 | + return callable(*args, **kw) |
377 | + finally: |
378 | + del self._threading_data.opener |
379 | + # We reset _seen_urls here to avoid multiple calls to open giving |
380 | + # spurious loop exceptions. |
381 | + self._seen_urls = set() |
382 | + |
383 | + def follow_reference(self, url): |
384 | + """Get the branch-reference value at the specified url. |
385 | + |
386 | + This exists as a separate method only to be overriden in unit tests. |
387 | + """ |
388 | + bzrdir = ControlDir.open(url, probers=self.probers) |
389 | + return bzrdir.get_branch_reference() |
390 | + |
391 | + def open(self, url): |
392 | + """Open the Bazaar branch at url, first checking for safety. |
393 | + |
394 | + What safety means is defined by a subclasses `follow_reference` and |
395 | + `check_one_url` methods. |
396 | + """ |
397 | + if type(url) != str: |
398 | + raise TypeError |
399 | + |
400 | + url = self.check_and_follow_branch_reference(url) |
401 | + |
402 | + def open_branch(url): |
403 | + dir = ControlDir.open(url, probers=self.probers) |
404 | + return dir.open_branch() |
405 | + return self.run_with_transform_fallback_location_hook_installed( |
406 | + open_branch, url) |
407 | + |
408 | + |
409 | +def safe_open(allowed_scheme, url): |
410 | + """Open the branch at `url`, only accessing URLs on `allowed_scheme`. |
411 | + |
412 | + :raises BadUrl: An attempt was made to open a URL that was not on |
413 | + `allowed_scheme`. |
414 | + """ |
415 | + return SafeBranchOpener(SingleSchemePolicy(allowed_scheme)).open(url) |
416 | + |
417 | + |
418 | +SafeBranchOpener.install_hook() |
419 | |
420 | === modified file 'bzrlib/tests/__init__.py' |
421 | --- bzrlib/tests/__init__.py 2011-12-18 12:46:49 +0000 |
422 | +++ bzrlib/tests/__init__.py 2011-12-22 01:48:24 +0000 |
423 | @@ -4046,6 +4046,7 @@ |
424 | 'bzrlib.tests.test_revisiontree', |
425 | 'bzrlib.tests.test_rio', |
426 | 'bzrlib.tests.test_rules', |
427 | + 'bzrlib.tests.test_safe_open', |
428 | 'bzrlib.tests.test_sampler', |
429 | 'bzrlib.tests.test_scenarios', |
430 | 'bzrlib.tests.test_script', |
431 | |
432 | === added file 'bzrlib/tests/test_safe_open.py' |
433 | --- bzrlib/tests/test_safe_open.py 1970-01-01 00:00:00 +0000 |
434 | +++ bzrlib/tests/test_safe_open.py 2011-12-22 01:48:24 +0000 |
435 | @@ -0,0 +1,356 @@ |
436 | +# Copyright (C) 2011 Canonical Ltd |
437 | +# |
438 | +# This program is free software; you can redistribute it and/or modify |
439 | +# it under the terms of the GNU General Public License as published by |
440 | +# the Free Software Foundation; either version 2 of the License, or |
441 | +# (at your option) any later version. |
442 | +# |
443 | +# This program is distributed in the hope that it will be useful, |
444 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
445 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
446 | +# GNU General Public License for more details. |
447 | +# |
448 | +# You should have received a copy of the GNU General Public License |
449 | +# along with this program; if not, write to the Free Software |
450 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
451 | + |
452 | +"""Tests for the safe branch open code.""" |
453 | + |
454 | +from bzrlib import urlutils |
455 | +from bzrlib.branch import ( |
456 | + Branch, |
457 | + BranchReferenceFormat, |
458 | + ) |
459 | +from bzrlib.bzrdir import ( |
460 | + BzrDir, |
461 | + BzrProber, |
462 | + ) |
463 | +from bzrlib.controldir import ControlDirFormat |
464 | +from bzrlib.errors import NotBranchError |
465 | +from bzrlib.safe_open import ( |
466 | + BadUrl, |
467 | + BlacklistPolicy, |
468 | + BranchLoopError, |
469 | + BranchReferenceForbidden, |
470 | + safe_open, |
471 | + SafeBranchOpener, |
472 | + WhitelistPolicy, |
473 | + ) |
474 | +from bzrlib.tests import ( |
475 | + TestCase, |
476 | + TestCaseWithTransport, |
477 | + ) |
478 | +from bzrlib.transport import chroot |
479 | + |
480 | + |
481 | +class TestSafeBranchOpenerCheckAndFollowBranchReference(TestCase): |
482 | + """Unit tests for `SafeBranchOpener.check_and_follow_branch_reference`.""" |
483 | + |
484 | + def setUp(self): |
485 | + super(TestSafeBranchOpenerCheckAndFollowBranchReference, self).setUp() |
486 | + SafeBranchOpener.install_hook() |
487 | + |
488 | + class StubbedSafeBranchOpener(SafeBranchOpener): |
489 | + """SafeBranchOpener that provides canned answers. |
490 | + |
491 | + We implement the methods we need to to be able to control all the |
492 | + inputs to the `follow_reference` method, which is what is |
493 | + being tested in this class. |
494 | + """ |
495 | + |
496 | + def __init__(self, references, policy): |
497 | + parent_cls = TestSafeBranchOpenerCheckAndFollowBranchReference |
498 | + super(parent_cls.StubbedSafeBranchOpener, self).__init__(policy) |
499 | + self._reference_values = {} |
500 | + for i in range(len(references) - 1): |
501 | + self._reference_values[references[i]] = references[i + 1] |
502 | + self.follow_reference_calls = [] |
503 | + |
504 | + def follow_reference(self, url): |
505 | + self.follow_reference_calls.append(url) |
506 | + return self._reference_values[url] |
507 | + |
508 | + def make_branch_opener(self, should_follow_references, references, |
509 | + unsafe_urls=None): |
510 | + policy = BlacklistPolicy(should_follow_references, unsafe_urls) |
511 | + opener = self.StubbedSafeBranchOpener(references, policy) |
512 | + return opener |
513 | + |
514 | + def test_check_initial_url(self): |
515 | + # check_and_follow_branch_reference rejects all URLs that are not allowed. |
516 | + opener = self.make_branch_opener(None, [], set(['a'])) |
517 | + self.assertRaises( |
518 | + BadUrl, opener.check_and_follow_branch_reference, 'a') |
519 | + |
520 | + def test_not_reference(self): |
521 | + # When branch references are forbidden, check_and_follow_branch_reference |
522 | + # does not raise on non-references. |
523 | + opener = self.make_branch_opener(False, ['a', None]) |
524 | + self.assertEquals( |
525 | + 'a', opener.check_and_follow_branch_reference('a')) |
526 | + self.assertEquals(['a'], opener.follow_reference_calls) |
527 | + |
528 | + def test_branch_reference_forbidden(self): |
529 | + # check_and_follow_branch_reference raises BranchReferenceForbidden if |
530 | + # branch references are forbidden and the source URL points to a |
531 | + # branch reference. |
532 | + opener = self.make_branch_opener(False, ['a', 'b']) |
533 | + self.assertRaises( |
534 | + BranchReferenceForbidden, |
535 | + opener.check_and_follow_branch_reference, 'a') |
536 | + self.assertEquals(['a'], opener.follow_reference_calls) |
537 | + |
538 | + def test_allowed_reference(self): |
539 | + # check_and_follow_branch_reference does not raise if following references |
540 | + # is allowed and the source URL points to a branch reference to a |
541 | + # permitted location. |
542 | + opener = self.make_branch_opener(True, ['a', 'b', None]) |
543 | + self.assertEquals( |
544 | + 'b', opener.check_and_follow_branch_reference('a')) |
545 | + self.assertEquals(['a', 'b'], opener.follow_reference_calls) |
546 | + |
547 | + def test_check_referenced_urls(self): |
548 | + # check_and_follow_branch_reference checks if the URL a reference points |
549 | + # to is safe. |
550 | + opener = self.make_branch_opener( |
551 | + True, ['a', 'b', None], unsafe_urls=set('b')) |
552 | + self.assertRaises( |
553 | + BadUrl, opener.check_and_follow_branch_reference, 'a') |
554 | + self.assertEquals(['a'], opener.follow_reference_calls) |
555 | + |
556 | + def test_self_referencing_branch(self): |
557 | + # check_and_follow_branch_reference raises BranchReferenceLoopError if |
558 | + # following references is allowed and the source url points to a |
559 | + # self-referencing branch reference. |
560 | + opener = self.make_branch_opener(True, ['a', 'a']) |
561 | + self.assertRaises( |
562 | + BranchLoopError, opener.check_and_follow_branch_reference, 'a') |
563 | + self.assertEquals(['a'], opener.follow_reference_calls) |
564 | + |
565 | + def test_branch_reference_loop(self): |
566 | + # check_and_follow_branch_reference raises BranchReferenceLoopError if |
567 | + # following references is allowed and the source url points to a loop |
568 | + # of branch references. |
569 | + references = ['a', 'b', 'a'] |
570 | + opener = self.make_branch_opener(True, references) |
571 | + self.assertRaises( |
572 | + BranchLoopError, opener.check_and_follow_branch_reference, 'a') |
573 | + self.assertEquals(['a', 'b'], opener.follow_reference_calls) |
574 | + |
575 | + |
576 | +class TrackingProber(BzrProber): |
577 | + """Subclass of BzrProber which tracks URLs it has been asked to open.""" |
578 | + |
579 | + seen_urls = [] |
580 | + |
581 | + @classmethod |
582 | + def probe_transport(klass, transport): |
583 | + klass.seen_urls.append(transport.base) |
584 | + return BzrProber.probe_transport(transport) |
585 | + |
586 | + |
587 | +class TestSafeBranchOpenerStacking(TestCaseWithTransport): |
588 | + |
589 | + def setUp(self): |
590 | + super(TestSafeBranchOpenerStacking, self).setUp() |
591 | + SafeBranchOpener.install_hook() |
592 | + |
593 | + def make_branch_opener(self, allowed_urls, probers=None): |
594 | + policy = WhitelistPolicy(True, allowed_urls, True) |
595 | + return SafeBranchOpener(policy, probers) |
596 | + |
597 | + def test_probers(self): |
598 | + # Only the specified probers should be used |
599 | + b = self.make_branch('branch') |
600 | + opener = self.make_branch_opener([b.base], probers=[]) |
601 | + self.assertRaises(NotBranchError, opener.open, b.base) |
602 | + opener = self.make_branch_opener([b.base], probers=[BzrProber]) |
603 | + self.assertEquals(b.base, opener.open(b.base).base) |
604 | + |
605 | + def test_default_probers(self): |
606 | + # If no probers are specified to the constructor |
607 | + # of SafeBranchOpener, then a safe set will be used, |
608 | + # rather than all probers registered in bzr. |
609 | + self.addCleanup(ControlDirFormat.unregister_prober, TrackingProber) |
610 | + ControlDirFormat.register_prober(TrackingProber) |
611 | + # Open a location without any branches, so that all probers are |
612 | + # tried. |
613 | + # First, check that the TrackingProber tracks correctly. |
614 | + TrackingProber.seen_urls = [] |
615 | + opener = self.make_branch_opener(["."], probers=[TrackingProber]) |
616 | + self.assertRaises(NotBranchError, opener.open, ".") |
617 | + self.assertEquals(1, len(TrackingProber.seen_urls)) |
618 | + TrackingProber.seen_urls = [] |
619 | + # And make sure it's registered in such a way that BzrDir.open would |
620 | + # use it. |
621 | + self.assertRaises(NotBranchError, BzrDir.open, ".") |
622 | + self.assertEquals(1, len(TrackingProber.seen_urls)) |
623 | + |
624 | + def test_allowed_url(self): |
625 | + # the opener does not raise an exception for branches stacked on |
626 | + # branches with allowed URLs. |
627 | + stacked_on_branch = self.make_branch('base-branch', format='1.6') |
628 | + stacked_branch = self.make_branch('stacked-branch', format='1.6') |
629 | + stacked_branch.set_stacked_on_url(stacked_on_branch.base) |
630 | + opener = self.make_branch_opener( |
631 | + [stacked_branch.base, stacked_on_branch.base]) |
632 | + # This doesn't raise an exception. |
633 | + opener.open(stacked_branch.base) |
634 | + |
635 | + def test_nstackable_repository(self): |
636 | + # treats branches with UnstackableRepositoryFormats as |
637 | + # being not stacked. |
638 | + branch = self.make_branch('unstacked', format='knit') |
639 | + opener = self.make_branch_opener([branch.base]) |
640 | + # This doesn't raise an exception. |
641 | + opener.open(branch.base) |
642 | + |
643 | + def test_allowed_relative_url(self): |
644 | + # passes on absolute urls to check_one_url, even if the |
645 | + # value of stacked_on_location in the config is set to a relative URL. |
646 | + stacked_on_branch = self.make_branch('base-branch', format='1.6') |
647 | + stacked_branch = self.make_branch('stacked-branch', format='1.6') |
648 | + stacked_branch.set_stacked_on_url('../base-branch') |
649 | + opener = self.make_branch_opener( |
650 | + [stacked_branch.base, stacked_on_branch.base]) |
651 | + # Note that stacked_on_branch.base is not '../base-branch', it's an |
652 | + # absolute URL. |
653 | + self.assertNotEqual('../base-branch', stacked_on_branch.base) |
654 | + # This doesn't raise an exception. |
655 | + opener.open(stacked_branch.base) |
656 | + |
657 | + def test_allowed_relative_nested(self): |
658 | + # Relative URLs are resolved relative to the stacked branch. |
659 | + self.get_transport().mkdir('subdir') |
660 | + a = self.make_branch('subdir/a', format='1.6') |
661 | + b = self.make_branch('b', format='1.6') |
662 | + b.set_stacked_on_url('../subdir/a') |
663 | + c = self.make_branch('subdir/c', format='1.6') |
664 | + c.set_stacked_on_url('../../b') |
665 | + opener = self.make_branch_opener([c.base, b.base, a.base]) |
666 | + # This doesn't raise an exception. |
667 | + opener.open(c.base) |
668 | + |
669 | + def test_forbidden_url(self): |
670 | + # raises a BadUrl exception if a branch is stacked on a |
671 | + # branch with a forbidden URL. |
672 | + stacked_on_branch = self.make_branch('base-branch', format='1.6') |
673 | + stacked_branch = self.make_branch('stacked-branch', format='1.6') |
674 | + stacked_branch.set_stacked_on_url(stacked_on_branch.base) |
675 | + opener = self.make_branch_opener([stacked_branch.base]) |
676 | + self.assertRaises(BadUrl, opener.open, stacked_branch.base) |
677 | + |
678 | + def test_forbidden_url_nested(self): |
679 | + # raises a BadUrl exception if a branch is stacked on a |
680 | + # branch that is in turn stacked on a branch with a forbidden URL. |
681 | + a = self.make_branch('a', format='1.6') |
682 | + b = self.make_branch('b', format='1.6') |
683 | + b.set_stacked_on_url(a.base) |
684 | + c = self.make_branch('c', format='1.6') |
685 | + c.set_stacked_on_url(b.base) |
686 | + opener = self.make_branch_opener([c.base, b.base]) |
687 | + self.assertRaises(BadUrl, opener.open, c.base) |
688 | + |
689 | + def test_self_stacked_branch(self): |
690 | + # raises StackingLoopError if a branch is stacked on |
691 | + # itself. This avoids infinite recursion errors. |
692 | + a = self.make_branch('a', format='1.6') |
693 | + # Bazaar 1.17 and up make it harder to create branches like this. |
694 | + # It's still worth testing that we don't blow up in the face of them, |
695 | + # so we grovel around a bit to create one anyway. |
696 | + a.get_config().set_user_option('stacked_on_location', a.base) |
697 | + opener = self.make_branch_opener([a.base]) |
698 | + self.assertRaises(BranchLoopError, opener.open, a.base) |
699 | + |
700 | + def test_loop_stacked_branch(self): |
701 | + # raises StackingLoopError if a branch is stacked in such |
702 | + # a way so that it is ultimately stacked on itself. e.g. a stacked on |
703 | + # b stacked on a. |
704 | + a = self.make_branch('a', format='1.6') |
705 | + b = self.make_branch('b', format='1.6') |
706 | + a.set_stacked_on_url(b.base) |
707 | + b.set_stacked_on_url(a.base) |
708 | + opener = self.make_branch_opener([a.base, b.base]) |
709 | + self.assertRaises(BranchLoopError, opener.open, a.base) |
710 | + self.assertRaises(BranchLoopError, opener.open, b.base) |
711 | + |
712 | + def test_custom_opener(self): |
713 | + # A custom function for opening a control dir can be specified. |
714 | + a = self.make_branch('a', format='2a') |
715 | + b = self.make_branch('b', format='2a') |
716 | + b.set_stacked_on_url(a.base) |
717 | + |
718 | + TrackingProber.seen_urls = [] |
719 | + opener = self.make_branch_opener( |
720 | + [a.base, b.base], probers=[TrackingProber]) |
721 | + opener.open(b.base) |
722 | + self.assertEquals( |
723 | + set(TrackingProber.seen_urls), set([b.base, a.base])) |
724 | + |
725 | + def test_custom_opener_with_branch_reference(self): |
726 | + # A custom function for opening a control dir can be specified. |
727 | + a = self.make_branch('a', format='2a') |
728 | + b_dir = self.make_bzrdir('b') |
729 | + b = BranchReferenceFormat().initialize(b_dir, target_branch=a) |
730 | + TrackingProber.seen_urls = [] |
731 | + opener = self.make_branch_opener( |
732 | + [a.base, b.base], probers=[TrackingProber]) |
733 | + opener.open(b.base) |
734 | + self.assertEquals( |
735 | + set(TrackingProber.seen_urls), set([b.base, a.base])) |
736 | + |
737 | + |
738 | +class TestSafeOpen(TestCaseWithTransport): |
739 | + """Tests for `safe_open`.""" |
740 | + |
741 | + def setUp(self): |
742 | + super(TestSafeOpen, self).setUp() |
743 | + SafeBranchOpener.install_hook() |
744 | + |
745 | + def test_hook_does_not_interfere(self): |
746 | + # The transform_fallback_location hook does not interfere with regular |
747 | + # stacked branch access outside of safe_open. |
748 | + self.make_branch('stacked') |
749 | + self.make_branch('stacked-on') |
750 | + Branch.open('stacked').set_stacked_on_url('../stacked-on') |
751 | + Branch.open('stacked') |
752 | + |
753 | + def get_chrooted_scheme(self, relpath): |
754 | + """Create a server that is chrooted to `relpath`. |
755 | + |
756 | + :return: ``(scheme, get_url)`` where ``scheme`` is the scheme of the |
757 | + chroot server and ``get_url`` returns URLs on said server. |
758 | + """ |
759 | + transport = self.get_transport(relpath) |
760 | + chroot_server = chroot.ChrootServer(transport) |
761 | + chroot_server.start_server() |
762 | + self.addCleanup(chroot_server.stop_server) |
763 | + |
764 | + def get_url(relpath): |
765 | + return chroot_server.get_url() + relpath |
766 | + |
767 | + return urlutils.URL.from_string(chroot_server.get_url()).scheme, get_url |
768 | + |
769 | + def test_stacked_within_scheme(self): |
770 | + # A branch that is stacked on a URL of the same scheme is safe to |
771 | + # open. |
772 | + self.get_transport().mkdir('inside') |
773 | + self.make_branch('inside/stacked') |
774 | + self.make_branch('inside/stacked-on') |
775 | + scheme, get_chrooted_url = self.get_chrooted_scheme('inside') |
776 | + Branch.open(get_chrooted_url('stacked')).set_stacked_on_url( |
777 | + get_chrooted_url('stacked-on')) |
778 | + safe_open(scheme, get_chrooted_url('stacked')) |
779 | + |
780 | + def test_stacked_outside_scheme(self): |
781 | + # A branch that is stacked on a URL that is not of the same scheme is |
782 | + # not safe to open. |
783 | + self.get_transport().mkdir('inside') |
784 | + self.get_transport().mkdir('outside') |
785 | + self.make_branch('inside/stacked') |
786 | + self.make_branch('outside/stacked-on') |
787 | + scheme, get_chrooted_url = self.get_chrooted_scheme('inside') |
788 | + Branch.open(get_chrooted_url('stacked')).set_stacked_on_url( |
789 | + self.get_url('outside/stacked-on')) |
790 | + self.assertRaises( |
791 | + BadUrl, safe_open, scheme, get_chrooted_url('stacked')) |
792 | |
793 | === modified file 'bzrlib/workingtree.py' |
794 | --- bzrlib/workingtree.py 2011-12-19 17:39:35 +0000 |
795 | +++ bzrlib/workingtree.py 2011-12-22 01:48:24 +0000 |
796 | @@ -267,8 +267,8 @@ |
797 | """ |
798 | if path is None: |
799 | path = osutils.getcwd() |
800 | - control = controldir.ControlDir.open(path, _unsupported) |
801 | - return control.open_workingtree(_unsupported) |
802 | + control = controldir.ControlDir.open(path, _unsupported=_unsupported) |
803 | + return control.open_workingtree(_unsupported=_unsupported) |
804 | |
805 | @staticmethod |
806 | def open_containing(path=None): |
807 | |
808 | === modified file 'doc/en/release-notes/bzr-2.5.txt' |
809 | --- doc/en/release-notes/bzr-2.5.txt 2011-12-21 21:31:03 +0000 |
810 | +++ doc/en/release-notes/bzr-2.5.txt 2011-12-22 01:48:24 +0000 |
811 | @@ -123,6 +123,8 @@ |
812 | .. Major internal changes, unlikely to be visible to users or plugin |
813 | developers, but interesting for bzr developers. |
814 | |
815 | +* Add new module ``bzrlib.safe_open``. (Jelmer Vernooij, #850843) |
816 | + |
817 | * ``bzrlib.urlutils`` now includes ``quote`` and ``unquote`` functions, |
818 | rather than importing them from ``urllib``. This prevents loading |
819 | of the ``socket``, ``ssl`` and ``urllib`` modules for |
Since this is a fairly isolated piece of code I've put the errors in bzrlib.safe_open. bzrlib.errors is certainly also an option, but that module is already fairly large, and it doesn't seem likely other code will use them.