Merge lp:~jelmer/bzr/safe-open into lp:bzr

Proposed by Jelmer Vernooij
Status: Superseded
Proposed branch: lp:~jelmer/bzr/safe-open
Merge into: lp:bzr
Prerequisite: lp:~jelmer/bzr/simplify-server-probing
Diff against target: 709 lines (+675/-0)
4 files modified
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/test_url_policy_open.py (+357/-0)
bzrlib/url_policy_open.py (+314/-0)
doc/en/release-notes/bzr-2.5.txt (+3/-0)
To merge this branch: bzr merge lp:~jelmer/bzr/safe-open
Reviewer Review Type Date Requested Status
Martin Packman (community) Needs Fixing
Review via email: mp+86645@code.launchpad.net

This proposal supersedes a proposal from 2011-12-22.

This proposal has been superseded by a proposal from 2012-01-19.

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 : Posted in a previous version of this proposal

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.

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

Moving this up to the bzrlib level seems reasonable, but it really needs a better name. The problem with 'safe' is you need context to understand what it actually means and the term is too widely used for completely different things.

+"""Safe branch opening."""

Okay, something to do with branches.

+ def check_one_url(self, url):
+ """Check the safety of the source URL.

Something like same domain restrictions?

+class SafeBranchOpener(object):
+ """Safe branch opener.

Hm.

+def safe_open(allowed_scheme, url):
+ """Open the branch at `url`, only accessing URLs on `allowed_scheme`.

Aha! A useful docstring on this function... which actually does something much more specific than the name or module content implies.

So this is really about opening branches with certain URL based checks added. The naming should really reflect that limited scope.

+class BlacklistPolicy(BranchOpenPolicy):
+ """Branch policy that forbids certain URLs."""
+
+ def __init__(self, should_follow_references, unsafe_urls=None):
+ if unsafe_urls is None:
+ unsafe_urls = set()
+ self._unsafe_urls = unsafe_urls

This is unsafe, bzrlib doesn't normalise URLs enough to ensure there's only one canonical form. Also, forgetting a param meaning "no restrictions" isn't a great interface. I'd just add an underscore to the class name for now to discourage use.

There's some more to nitpick over with the code, but it doesn't seem that important.

review: Needs Fixing
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Am 24/12/11 13:26, schrieb Martin Packman:
> Review: Needs Fixing
>
> Moving this up to the bzrlib level seems reasonable, but it really needs a better name. The problem with 'safe' is you need context to understand what it actually means and the term is too widely used for completely different things.
Hmm, interesting point. I took the name from launchpad, but I guess I've
just gotten used to it meaning this... What about "bzrlib.hookable_open"
or "bzrlib.policy_open" ?

> +def safe_open(allowed_scheme, url):
> + """Open the branch at `url`, only accessing URLs on `allowed_scheme`.
>
> Aha! A useful docstring on this function... which actually does something much more specific than the name or module content implies.
>
>
> So this is really about opening branches with certain URL based checks added. The naming should really reflect that limited scope.

I'm not sure I follow. Do you mean e.g. renaming it to e.g.
open_with_specific_scheme ?
>
> +class BlacklistPolicy(BranchOpenPolicy):
> + """Branch policy that forbids certain URLs."""
> +
> + def __init__(self, should_follow_references, unsafe_urls=None):
> + if unsafe_urls is None:
> + unsafe_urls = set()
> + self._unsafe_urls = unsafe_urls
>
> This is unsafe, bzrlib doesn't normalise URLs enough to ensure there's only one canonical form. Also, forgetting a param meaning "no restrictions" isn't a great interface. I'd just add an underscore to the class name for now to discourage use.
>
> There's some more to nitpick over with the code, but it doesn't seem that important.
This is only really used in the Launchpad testsuite. I guess I could
just leave it over in the Launchpad code.

Cheers,

Jelmer

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

> Hmm, interesting point. I took the name from launchpad, but I guess I've
> just gotten used to it meaning this... What about "bzrlib.hookable_open"
> or "bzrlib.policy_open" ?

Something like that, but unless there's any thought to extending the api beyond just looking at urls, I think that should be reflected in the name. I'm not bursting with ideas though... limited_location_open? restrict_url_open? That this is only branches not repos or trees is also relevant. Perhaps we can bikeshed on IRC?

With a module rename, I'd change SafeBranchOpener to just BranchOpener and fix all the docstrings with 'safe' in them to say something meaningful as well.

> I'm not sure I follow. Do you mean e.g. renaming it to e.g.
> open_with_specific_scheme ?

Yup. Or open_only_scheme, open_branch_with_scheme, something like that.

> This is only really used in the Launchpad testsuite. I guess I could
> just leave it over in the Launchpad code.

Yeah, I thought it probably wasn't in actual use. I don't mind bringing it across anyway, with either an underscore or just a docstring stating that limitation.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2011-12-24 10:10:59 +0000
+++ bzrlib/tests/__init__.py 2012-01-19 01:06:23 +0000
@@ -4056,6 +4056,7 @@
4056 'bzrlib.tests.test_revisiontree',4056 'bzrlib.tests.test_revisiontree',
4057 'bzrlib.tests.test_rio',4057 'bzrlib.tests.test_rio',
4058 'bzrlib.tests.test_rules',4058 'bzrlib.tests.test_rules',
4059 'bzrlib.tests.test_url_policy_open',
4059 'bzrlib.tests.test_sampler',4060 'bzrlib.tests.test_sampler',
4060 'bzrlib.tests.test_scenarios',4061 'bzrlib.tests.test_scenarios',
4061 'bzrlib.tests.test_script',4062 'bzrlib.tests.test_script',
40624063
=== added file 'bzrlib/tests/test_url_policy_open.py'
--- bzrlib/tests/test_url_policy_open.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_url_policy_open.py 2012-01-19 01:06:23 +0000
@@ -0,0 +1,357 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tests for the branch open with specific URL policy code."""
18
19from bzrlib import urlutils
20from bzrlib.branch import (
21 Branch,
22 BranchReferenceFormat,
23 )
24from bzrlib.bzrdir import (
25 BzrDir,
26 BzrProber,
27 )
28from bzrlib.controldir import ControlDirFormat
29from bzrlib.errors import NotBranchError
30from bzrlib.url_policy_open import (
31 BadUrl,
32 _BlacklistPolicy,
33 BranchLoopError,
34 BranchReferenceForbidden,
35 open_only_scheme,
36 BranchOpener,
37 WhitelistPolicy,
38 )
39from bzrlib.tests import (
40 TestCase,
41 TestCaseWithTransport,
42 )
43from bzrlib.transport import chroot
44
45
46class TestBranchOpenerCheckAndFollowBranchReference(TestCase):
47 """Unit tests for `BranchOpener.check_and_follow_branch_reference`."""
48
49 def setUp(self):
50 super(TestBranchOpenerCheckAndFollowBranchReference, self).setUp()
51 BranchOpener.install_hook()
52
53 class StubbedBranchOpener(BranchOpener):
54 """BranchOpener that provides canned answers.
55
56 We implement the methods we need to to be able to control all the
57 inputs to the `follow_reference` method, which is what is
58 being tested in this class.
59 """
60
61 def __init__(self, references, policy):
62 parent_cls = TestBranchOpenerCheckAndFollowBranchReference
63 super(parent_cls.StubbedBranchOpener, self).__init__(policy)
64 self._reference_values = {}
65 for i in range(len(references) - 1):
66 self._reference_values[references[i]] = references[i + 1]
67 self.follow_reference_calls = []
68
69 def follow_reference(self, url):
70 self.follow_reference_calls.append(url)
71 return self._reference_values[url]
72
73 def make_branch_opener(self, should_follow_references, references,
74 unsafe_urls=None):
75 policy = _BlacklistPolicy(should_follow_references, unsafe_urls)
76 opener = self.StubbedBranchOpener(references, policy)
77 return opener
78
79 def test_check_initial_url(self):
80 # check_and_follow_branch_reference rejects all URLs that are not
81 # allowed.
82 opener = self.make_branch_opener(None, [], set(['a']))
83 self.assertRaises(
84 BadUrl, opener.check_and_follow_branch_reference, 'a')
85
86 def test_not_reference(self):
87 # When branch references are forbidden, check_and_follow_branch_reference
88 # does not raise on non-references.
89 opener = self.make_branch_opener(False, ['a', None])
90 self.assertEquals(
91 'a', opener.check_and_follow_branch_reference('a'))
92 self.assertEquals(['a'], opener.follow_reference_calls)
93
94 def test_branch_reference_forbidden(self):
95 # check_and_follow_branch_reference raises BranchReferenceForbidden if
96 # branch references are forbidden and the source URL points to a
97 # branch reference.
98 opener = self.make_branch_opener(False, ['a', 'b'])
99 self.assertRaises(
100 BranchReferenceForbidden,
101 opener.check_and_follow_branch_reference, 'a')
102 self.assertEquals(['a'], opener.follow_reference_calls)
103
104 def test_allowed_reference(self):
105 # check_and_follow_branch_reference does not raise if following references
106 # is allowed and the source URL points to a branch reference to a
107 # permitted location.
108 opener = self.make_branch_opener(True, ['a', 'b', None])
109 self.assertEquals(
110 'b', opener.check_and_follow_branch_reference('a'))
111 self.assertEquals(['a', 'b'], opener.follow_reference_calls)
112
113 def test_check_referenced_urls(self):
114 # check_and_follow_branch_reference checks if the URL a reference points
115 # to is safe.
116 opener = self.make_branch_opener(
117 True, ['a', 'b', None], unsafe_urls=set('b'))
118 self.assertRaises(
119 BadUrl, opener.check_and_follow_branch_reference, 'a')
120 self.assertEquals(['a'], opener.follow_reference_calls)
121
122 def test_self_referencing_branch(self):
123 # check_and_follow_branch_reference raises BranchReferenceLoopError if
124 # following references is allowed and the source url points to a
125 # self-referencing branch reference.
126 opener = self.make_branch_opener(True, ['a', 'a'])
127 self.assertRaises(
128 BranchLoopError, opener.check_and_follow_branch_reference, 'a')
129 self.assertEquals(['a'], opener.follow_reference_calls)
130
131 def test_branch_reference_loop(self):
132 # check_and_follow_branch_reference raises BranchReferenceLoopError if
133 # following references is allowed and the source url points to a loop
134 # of branch references.
135 references = ['a', 'b', 'a']
136 opener = self.make_branch_opener(True, references)
137 self.assertRaises(
138 BranchLoopError, opener.check_and_follow_branch_reference, 'a')
139 self.assertEquals(['a', 'b'], opener.follow_reference_calls)
140
141
142class TrackingProber(BzrProber):
143 """Subclass of BzrProber which tracks URLs it has been asked to open."""
144
145 seen_urls = []
146
147 @classmethod
148 def probe_transport(klass, transport):
149 klass.seen_urls.append(transport.base)
150 return BzrProber.probe_transport(transport)
151
152
153class TestBranchOpenerStacking(TestCaseWithTransport):
154
155 def setUp(self):
156 super(TestBranchOpenerStacking, self).setUp()
157 BranchOpener.install_hook()
158
159 def make_branch_opener(self, allowed_urls, probers=None):
160 policy = WhitelistPolicy(True, allowed_urls, True)
161 return BranchOpener(policy, probers)
162
163 def test_probers(self):
164 # Only the specified probers should be used
165 b = self.make_branch('branch')
166 opener = self.make_branch_opener([b.base], probers=[])
167 self.assertRaises(NotBranchError, opener.open, b.base)
168 opener = self.make_branch_opener([b.base], probers=[BzrProber])
169 self.assertEquals(b.base, opener.open(b.base).base)
170
171 def test_default_probers(self):
172 # If no probers are specified to the constructor
173 # of BranchOpener, then a safe set will be used,
174 # rather than all probers registered in bzr.
175 self.addCleanup(ControlDirFormat.unregister_prober, TrackingProber)
176 ControlDirFormat.register_prober(TrackingProber)
177 # Open a location without any branches, so that all probers are
178 # tried.
179 # First, check that the TrackingProber tracks correctly.
180 TrackingProber.seen_urls = []
181 opener = self.make_branch_opener(["."], probers=[TrackingProber])
182 self.assertRaises(NotBranchError, opener.open, ".")
183 self.assertEquals(1, len(TrackingProber.seen_urls))
184 TrackingProber.seen_urls = []
185 # And make sure it's registered in such a way that BzrDir.open would
186 # use it.
187 self.assertRaises(NotBranchError, BzrDir.open, ".")
188 self.assertEquals(1, len(TrackingProber.seen_urls))
189
190 def test_allowed_url(self):
191 # the opener does not raise an exception for branches stacked on
192 # branches with allowed URLs.
193 stacked_on_branch = self.make_branch('base-branch', format='1.6')
194 stacked_branch = self.make_branch('stacked-branch', format='1.6')
195 stacked_branch.set_stacked_on_url(stacked_on_branch.base)
196 opener = self.make_branch_opener(
197 [stacked_branch.base, stacked_on_branch.base])
198 # This doesn't raise an exception.
199 opener.open(stacked_branch.base)
200
201 def test_nstackable_repository(self):
202 # treats branches with UnstackableRepositoryFormats as
203 # being not stacked.
204 branch = self.make_branch('unstacked', format='knit')
205 opener = self.make_branch_opener([branch.base])
206 # This doesn't raise an exception.
207 opener.open(branch.base)
208
209 def test_allowed_relative_url(self):
210 # passes on absolute urls to check_one_url, even if the
211 # value of stacked_on_location in the config is set to a relative URL.
212 stacked_on_branch = self.make_branch('base-branch', format='1.6')
213 stacked_branch = self.make_branch('stacked-branch', format='1.6')
214 stacked_branch.set_stacked_on_url('../base-branch')
215 opener = self.make_branch_opener(
216 [stacked_branch.base, stacked_on_branch.base])
217 # Note that stacked_on_branch.base is not '../base-branch', it's an
218 # absolute URL.
219 self.assertNotEqual('../base-branch', stacked_on_branch.base)
220 # This doesn't raise an exception.
221 opener.open(stacked_branch.base)
222
223 def test_allowed_relative_nested(self):
224 # Relative URLs are resolved relative to the stacked branch.
225 self.get_transport().mkdir('subdir')
226 a = self.make_branch('subdir/a', format='1.6')
227 b = self.make_branch('b', format='1.6')
228 b.set_stacked_on_url('../subdir/a')
229 c = self.make_branch('subdir/c', format='1.6')
230 c.set_stacked_on_url('../../b')
231 opener = self.make_branch_opener([c.base, b.base, a.base])
232 # This doesn't raise an exception.
233 opener.open(c.base)
234
235 def test_forbidden_url(self):
236 # raises a BadUrl exception if a branch is stacked on a
237 # branch with a forbidden URL.
238 stacked_on_branch = self.make_branch('base-branch', format='1.6')
239 stacked_branch = self.make_branch('stacked-branch', format='1.6')
240 stacked_branch.set_stacked_on_url(stacked_on_branch.base)
241 opener = self.make_branch_opener([stacked_branch.base])
242 self.assertRaises(BadUrl, opener.open, stacked_branch.base)
243
244 def test_forbidden_url_nested(self):
245 # raises a BadUrl exception if a branch is stacked on a
246 # branch that is in turn stacked on a branch with a forbidden URL.
247 a = self.make_branch('a', format='1.6')
248 b = self.make_branch('b', format='1.6')
249 b.set_stacked_on_url(a.base)
250 c = self.make_branch('c', format='1.6')
251 c.set_stacked_on_url(b.base)
252 opener = self.make_branch_opener([c.base, b.base])
253 self.assertRaises(BadUrl, opener.open, c.base)
254
255 def test_self_stacked_branch(self):
256 # raises StackingLoopError if a branch is stacked on
257 # itself. This avoids infinite recursion errors.
258 a = self.make_branch('a', format='1.6')
259 # Bazaar 1.17 and up make it harder to create branches like this.
260 # It's still worth testing that we don't blow up in the face of them,
261 # so we grovel around a bit to create one anyway.
262 a.get_config().set_user_option('stacked_on_location', a.base)
263 opener = self.make_branch_opener([a.base])
264 self.assertRaises(BranchLoopError, opener.open, a.base)
265
266 def test_loop_stacked_branch(self):
267 # raises StackingLoopError if a branch is stacked in such
268 # a way so that it is ultimately stacked on itself. e.g. a stacked on
269 # b stacked on a.
270 a = self.make_branch('a', format='1.6')
271 b = self.make_branch('b', format='1.6')
272 a.set_stacked_on_url(b.base)
273 b.set_stacked_on_url(a.base)
274 opener = self.make_branch_opener([a.base, b.base])
275 self.assertRaises(BranchLoopError, opener.open, a.base)
276 self.assertRaises(BranchLoopError, opener.open, b.base)
277
278 def test_custom_opener(self):
279 # A custom function for opening a control dir can be specified.
280 a = self.make_branch('a', format='2a')
281 b = self.make_branch('b', format='2a')
282 b.set_stacked_on_url(a.base)
283
284 TrackingProber.seen_urls = []
285 opener = self.make_branch_opener(
286 [a.base, b.base], probers=[TrackingProber])
287 opener.open(b.base)
288 self.assertEquals(
289 set(TrackingProber.seen_urls), set([b.base, a.base]))
290
291 def test_custom_opener_with_branch_reference(self):
292 # A custom function for opening a control dir can be specified.
293 a = self.make_branch('a', format='2a')
294 b_dir = self.make_bzrdir('b')
295 b = BranchReferenceFormat().initialize(b_dir, target_branch=a)
296 TrackingProber.seen_urls = []
297 opener = self.make_branch_opener(
298 [a.base, b.base], probers=[TrackingProber])
299 opener.open(b.base)
300 self.assertEquals(
301 set(TrackingProber.seen_urls), set([b.base, a.base]))
302
303
304class TestOpenOnlyScheme(TestCaseWithTransport):
305 """Tests for `open_only_scheme`."""
306
307 def setUp(self):
308 super(TestOpenOnlyScheme, self).setUp()
309 BranchOpener.install_hook()
310
311 def test_hook_does_not_interfere(self):
312 # The transform_fallback_location hook does not interfere with regular
313 # stacked branch access outside of open_only_scheme.
314 self.make_branch('stacked')
315 self.make_branch('stacked-on')
316 Branch.open('stacked').set_stacked_on_url('../stacked-on')
317 Branch.open('stacked')
318
319 def get_chrooted_scheme(self, relpath):
320 """Create a server that is chrooted to `relpath`.
321
322 :return: ``(scheme, get_url)`` where ``scheme`` is the scheme of the
323 chroot server and ``get_url`` returns URLs on said server.
324 """
325 transport = self.get_transport(relpath)
326 chroot_server = chroot.ChrootServer(transport)
327 chroot_server.start_server()
328 self.addCleanup(chroot_server.stop_server)
329
330 def get_url(relpath):
331 return chroot_server.get_url() + relpath
332
333 return urlutils.URL.from_string(chroot_server.get_url()).scheme, get_url
334
335 def test_stacked_within_scheme(self):
336 # A branch that is stacked on a URL of the same scheme is safe to
337 # open.
338 self.get_transport().mkdir('inside')
339 self.make_branch('inside/stacked')
340 self.make_branch('inside/stacked-on')
341 scheme, get_chrooted_url = self.get_chrooted_scheme('inside')
342 Branch.open(get_chrooted_url('stacked')).set_stacked_on_url(
343 get_chrooted_url('stacked-on'))
344 open_only_scheme(scheme, get_chrooted_url('stacked'))
345
346 def test_stacked_outside_scheme(self):
347 # A branch that is stacked on a URL that is not of the same scheme is
348 # not safe to open.
349 self.get_transport().mkdir('inside')
350 self.get_transport().mkdir('outside')
351 self.make_branch('inside/stacked')
352 self.make_branch('outside/stacked-on')
353 scheme, get_chrooted_url = self.get_chrooted_scheme('inside')
354 Branch.open(get_chrooted_url('stacked')).set_stacked_on_url(
355 self.get_url('outside/stacked-on'))
356 self.assertRaises(
357 BadUrl, open_only_scheme, scheme, get_chrooted_url('stacked'))
0358
=== added file 'bzrlib/url_policy_open.py'
--- bzrlib/url_policy_open.py 1970-01-01 00:00:00 +0000
+++ bzrlib/url_policy_open.py 2012-01-19 01:06:23 +0000
@@ -0,0 +1,314 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Branch opening with URL-based restrictions."""
18
19from __future__ import absolute_import
20
21import threading
22
23from bzrlib import (
24 errors,
25 urlutils,
26 )
27from bzrlib.branch import Branch
28from bzrlib.controldir import (
29 ControlDir,
30 )
31
32
33class BadUrl(errors.BzrError):
34
35 _fmt = "Tried to access a branch from bad URL %(url)s."
36
37
38class BranchReferenceForbidden(errors.BzrError):
39
40 _fmt = ("Trying to mirror a branch reference and the branch type "
41 "does not allow references.")
42
43
44class BranchLoopError(errors.BzrError):
45 """Encountered a branch cycle.
46
47 A URL may point to a branch reference or it may point to a stacked branch.
48 In either case, it's possible for there to be a cycle in these references,
49 and this exception is raised when we detect such a cycle.
50 """
51
52 _fmt = "Encountered a branch cycle"""
53
54
55class BranchOpenPolicy(object):
56 """Policy on how to open branches.
57
58 In particular, a policy determines which branches are okay to open by
59 checking their URLs and deciding whether or not to follow branch
60 references.
61 """
62
63 def should_follow_references(self):
64 """Whether we traverse references when mirroring.
65
66 Subclasses must override this method.
67
68 If we encounter a branch reference and this returns false, an error is
69 raised.
70
71 :returns: A boolean to indicate whether to follow a branch reference.
72 """
73 raise NotImplementedError(self.should_follow_references)
74
75 def transform_fallback_location(self, branch, url):
76 """Validate, maybe modify, 'url' to be used as a stacked-on location.
77
78 :param branch: The branch that is being opened.
79 :param url: The URL that the branch provides for its stacked-on
80 location.
81 :return: (new_url, check) where 'new_url' is the URL of the branch to
82 actually open and 'check' is true if 'new_url' needs to be
83 validated by check_and_follow_branch_reference.
84 """
85 raise NotImplementedError(self.transform_fallback_location)
86
87 def check_one_url(self, url):
88 """Check a URL.
89
90 Subclasses must override this method.
91
92 :param url: The source URL to check.
93 :raise BadUrl: subclasses are expected to raise this or a subclass
94 when it finds a URL it deems to be unacceptable.
95 """
96 raise NotImplementedError(self.check_one_url)
97
98
99class _BlacklistPolicy(BranchOpenPolicy):
100 """Branch policy that forbids certain URLs.
101
102 This doesn't cope with various alternative spellings of URLs,
103 with e.g. url encoding. It's mostly useful for tests.
104 """
105
106 def __init__(self, should_follow_references, bad_urls=None):
107 if bad_urls is None:
108 bad_urls = set()
109 self._bad_urls = bad_urls
110 self._should_follow_references = should_follow_references
111
112 def should_follow_references(self):
113 return self._should_follow_references
114
115 def check_one_url(self, url):
116 if url in self._bad_urls:
117 raise BadUrl(url)
118
119 def transform_fallback_location(self, branch, url):
120 """See `BranchOpenPolicy.transform_fallback_location`.
121
122 This class is not used for testing our smarter stacking features so we
123 just do the simplest thing: return the URL that would be used anyway
124 and don't check it.
125 """
126 return urlutils.join(branch.base, url), False
127
128
129class AcceptAnythingPolicy(_BlacklistPolicy):
130 """Accept anything, to make testing easier."""
131
132 def __init__(self):
133 super(AcceptAnythingPolicy, self).__init__(True, set())
134
135
136class WhitelistPolicy(BranchOpenPolicy):
137 """Branch policy that only allows certain URLs."""
138
139 def __init__(self, should_follow_references, allowed_urls=None,
140 check=False):
141 if allowed_urls is None:
142 allowed_urls = []
143 self.allowed_urls = set(url.rstrip('/') for url in allowed_urls)
144 self.check = check
145
146 def should_follow_references(self):
147 return self._should_follow_references
148
149 def check_one_url(self, url):
150 if url.rstrip('/') not in self.allowed_urls:
151 raise BadUrl(url)
152
153 def transform_fallback_location(self, branch, url):
154 """See `BranchOpenPolicy.transform_fallback_location`.
155
156 Here we return the URL that would be used anyway and optionally check
157 it.
158 """
159 return urlutils.join(branch.base, url), self.check
160
161
162class SingleSchemePolicy(BranchOpenPolicy):
163 """Branch open policy that rejects URLs not on the given scheme."""
164
165 def __init__(self, allowed_scheme):
166 self.allowed_scheme = allowed_scheme
167
168 def should_follow_references(self):
169 return True
170
171 def transform_fallback_location(self, branch, url):
172 return urlutils.join(branch.base, url), True
173
174 def check_one_url(self, url):
175 """Check that `url` is okay to open."""
176 if urlutils.URL.from_string(str(url)).scheme != self.allowed_scheme:
177 raise BadUrl(url)
178
179
180class BranchOpener(object):
181 """Branch opener which uses a URL policy.
182
183 All locations that are opened (stacked-on branches, references) are
184 checked against a policy object.
185
186 The policy object is expected to have the following methods:
187 * check_one_url
188 * should_follow_references
189 * transform_fallback_location
190 """
191
192 _threading_data = threading.local()
193
194 def __init__(self, policy, probers=None):
195 """Create a new BranchOpener.
196
197 :param policy: The opener policy to use.
198 :param probers: Optional list of probers to allow.
199 Defaults to local and remote bzr probers.
200 """
201 self.policy = policy
202 self._seen_urls = set()
203 self.probers = probers
204
205 @classmethod
206 def install_hook(cls):
207 """Install the ``transform_fallback_location`` hook.
208
209 This is done at module import time, but transform_fallback_locationHook
210 doesn't do anything unless the `_active_openers` threading.Local
211 object has a 'opener' attribute in this thread.
212
213 This is in a module-level function rather than performed at module
214 level so that it can be called in setUp for testing `BranchOpener`
215 as bzrlib.tests.TestCase.setUp clears hooks.
216 """
217 Branch.hooks.install_named_hook(
218 'transform_fallback_location',
219 cls.transform_fallback_locationHook,
220 'BranchOpener.transform_fallback_locationHook')
221
222 def check_and_follow_branch_reference(self, url):
223 """Check URL (and possibly the referenced URL).
224
225 This method checks that `url` passes the policy's `check_one_url`
226 method, and if `url` refers to a branch reference, it checks whether
227 references are allowed and whether the reference's URL passes muster
228 also -- recursively, until a real branch is found.
229
230 :param url: URL to check
231 :raise BranchLoopError: If the branch references form a loop.
232 :raise BranchReferenceForbidden: If this opener forbids branch
233 references.
234 """
235 while True:
236 if url in self._seen_urls:
237 raise BranchLoopError()
238 self._seen_urls.add(url)
239 self.policy.check_one_url(url)
240 next_url = self.follow_reference(url)
241 if next_url is None:
242 return url
243 url = next_url
244 if not self.policy.should_follow_references():
245 raise BranchReferenceForbidden(url)
246
247 @classmethod
248 def transform_fallback_locationHook(cls, branch, url):
249 """Installed as the 'transform_fallback_location' Branch hook.
250
251 This method calls `transform_fallback_location` on the policy object and
252 either returns the url it provides or passes it back to
253 check_and_follow_branch_reference.
254 """
255 try:
256 opener = getattr(cls._threading_data, "opener")
257 except AttributeError:
258 return url
259 new_url, check = opener.policy.transform_fallback_location(branch, url)
260 if check:
261 return opener.check_and_follow_branch_reference(new_url)
262 else:
263 return new_url
264
265 def run_with_transform_fallback_location_hook_installed(
266 self, callable, *args, **kw):
267 if (self.transform_fallback_locationHook not in
268 Branch.hooks['transform_fallback_location']):
269 raise AssertionError("hook not installed")
270 self._threading_data.opener = self
271 try:
272 return callable(*args, **kw)
273 finally:
274 del self._threading_data.opener
275 # We reset _seen_urls here to avoid multiple calls to open giving
276 # spurious loop exceptions.
277 self._seen_urls = set()
278
279 def follow_reference(self, url):
280 """Get the branch-reference value at the specified url.
281
282 This exists as a separate method only to be overriden in unit tests.
283 """
284 bzrdir = ControlDir.open(url, probers=self.probers)
285 return bzrdir.get_branch_reference()
286
287 def open(self, url):
288 """Open the Bazaar branch at url, first checking it.
289
290 What is acceptable means is defined by the policy's `follow_reference` and
291 `check_one_url` methods.
292 """
293 if type(url) != str:
294 raise TypeError
295
296 url = self.check_and_follow_branch_reference(url)
297
298 def open_branch(url):
299 dir = ControlDir.open(url, probers=self.probers)
300 return dir.open_branch()
301 return self.run_with_transform_fallback_location_hook_installed(
302 open_branch, url)
303
304
305def open_only_scheme(allowed_scheme, url):
306 """Open the branch at `url`, only accessing URLs on `allowed_scheme`.
307
308 :raises BadUrl: An attempt was made to open a URL that was not on
309 `allowed_scheme`.
310 """
311 return BranchOpener(SingleSchemePolicy(allowed_scheme)).open(url)
312
313
314BranchOpener.install_hook()
0315
=== modified file 'doc/en/release-notes/bzr-2.5.txt'
--- doc/en/release-notes/bzr-2.5.txt 2012-01-18 19:40:10 +0000
+++ doc/en/release-notes/bzr-2.5.txt 2012-01-19 01:06:23 +0000
@@ -56,10 +56,13 @@
56.. Major internal changes, unlikely to be visible to users or plugin 56.. Major internal changes, unlikely to be visible to users or plugin
57 developers, but interesting for bzr developers.57 developers, but interesting for bzr developers.
5858
59* Add new module ``bzrlib.url_policy_open``. (Jelmer Vernooij, #850843)
60
59* ``MutableTree`` has two new hooks ``pre_transform`` and61* ``MutableTree`` has two new hooks ``pre_transform`` and
60 ``post_transform`` that are called for tree transform operations.62 ``post_transform`` that are called for tree transform operations.
61 (Jelmer Vernooij, #912084)63 (Jelmer Vernooij, #912084)
6264
65
63Testing66Testing
64*******67*******
6568