~bzr-pqm/bzr/bzr.dev

6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
17
"""Branch opening with URL-based restrictions."""
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
18
6402.3.5 by Jelmer Vernooij
Avoid assert.
19
from __future__ import absolute_import
20
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
21
import threading
22
23
from bzrlib import (
24
    errors,
25
    urlutils,
26
    )
27
from bzrlib.branch import Branch
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
28
from bzrlib.controldir import (
29
    ControlDir,
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
30
    )
31
32
6402.3.2 by Jelmer Vernooij
bzrify
33
class BadUrl(errors.BzrError):
34
35
    _fmt = "Tried to access a branch from bad URL %(url)s."
36
37
38
class BranchReferenceForbidden(errors.BzrError):
39
40
    _fmt = ("Trying to mirror a branch reference and the branch type "
41
            "does not allow references.")
42
43
44
class BranchLoopError(errors.BzrError):
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
6402.3.2 by Jelmer Vernooij
bzrify
52
    _fmt = "Encountered a branch cycle"""
53
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
54
55
class BranchOpenPolicy(object):
56
    """Policy on how to open branches.
57
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
58
    In particular, a policy determines which branches are okay to open by
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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):
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
88
        """Check a URL.
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
94
            when it finds a URL it deems to be unacceptable.
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
95
        """
96
        raise NotImplementedError(self.check_one_url)
97
98
6402.3.8 by Jelmer Vernooij
make BlacklistPolicy private
99
class _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
    """
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
105
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
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
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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):
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
116
        if url in self._bad_urls:
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
6402.3.8 by Jelmer Vernooij
make BlacklistPolicy private
129
class AcceptAnythingPolicy(_BlacklistPolicy):
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
130
    """Accept anything, to make testing easier."""
131
132
    def __init__(self):
133
        super(AcceptAnythingPolicy, self).__init__(True, set())
134
135
136
class 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
162
class 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):
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
175
        """Check that `url` is okay to open."""
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
176
        if urlutils.URL.from_string(str(url)).scheme != self.allowed_scheme:
177
            raise BadUrl(url)
178
179
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
180
class BranchOpener(object):
181
    """Branch opener which uses a URL policy.
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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):
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
195
        """Create a new BranchOpener.
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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()
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
203
        self.probers = probers
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
214
        level so that it can be called in setUp for testing `BranchOpener`
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
215
        as bzrlib.tests.TestCase.setUp clears hooks.
216
        """
217
        Branch.hooks.install_named_hook(
218
            'transform_fallback_location',
219
            cls.transform_fallback_locationHook,
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
220
            'BranchOpener.transform_fallback_locationHook')
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
221
222
    def check_and_follow_branch_reference(self, url):
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
223
        """Check URL (and possibly the referenced URL).
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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):
6402.3.5 by Jelmer Vernooij
Avoid assert.
267
        if (self.transform_fallback_locationHook not in
268
                Branch.hooks['transform_fallback_location']):
269
            raise AssertionError("hook not installed")
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
        """
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
284
        bzrdir = ControlDir.open(url, probers=self.probers)
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
285
        return bzrdir.get_branch_reference()
286
287
    def open(self, url):
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
288
        """Open the Bazaar branch at url, first checking it.
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
289
6402.3.11 by Jelmer Vernooij
Avoid the word safe.
290
        What is acceptable means is defined by the policy's `follow_reference` and
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
291
        `check_one_url` methods.
292
        """
6402.3.2 by Jelmer Vernooij
bzrify
293
        if type(url) != str:
294
            raise TypeError
295
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
296
        url = self.check_and_follow_branch_reference(url)
297
298
        def open_branch(url):
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
299
            dir = ControlDir.open(url, probers=self.probers)
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
300
            return dir.open_branch()
301
        return self.run_with_transform_fallback_location_hook_installed(
302
            open_branch, url)
303
304
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
305
def open_only_scheme(allowed_scheme, url):
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
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
    """
6402.3.10 by Jelmer Vernooij
Name changes suggested by mgz.
311
    return BranchOpener(SingleSchemePolicy(allowed_scheme)).open(url)
312
313
314
BranchOpener.install_hook()