/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
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
17
"""Safe branch opening."""
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
58
    In particular, a policy determines which branches are safe 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 the safety of the source 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 unsafe.
95
        """
96
        raise NotImplementedError(self.check_one_url)
97
98
99
class BlacklistPolicy(BranchOpenPolicy):
100
    """Branch policy that forbids certain URLs."""
101
102
    def __init__(self, should_follow_references, unsafe_urls=None):
103
        if unsafe_urls is None:
104
            unsafe_urls = set()
105
        self._unsafe_urls = unsafe_urls
106
        self._should_follow_references = should_follow_references
107
108
    def should_follow_references(self):
109
        return self._should_follow_references
110
111
    def check_one_url(self, url):
112
        if url in self._unsafe_urls:
113
            raise BadUrl(url)
114
115
    def transform_fallback_location(self, branch, url):
116
        """See `BranchOpenPolicy.transform_fallback_location`.
117
118
        This class is not used for testing our smarter stacking features so we
119
        just do the simplest thing: return the URL that would be used anyway
120
        and don't check it.
121
        """
122
        return urlutils.join(branch.base, url), False
123
124
125
class AcceptAnythingPolicy(BlacklistPolicy):
126
    """Accept anything, to make testing easier."""
127
128
    def __init__(self):
129
        super(AcceptAnythingPolicy, self).__init__(True, set())
130
131
132
class WhitelistPolicy(BranchOpenPolicy):
133
    """Branch policy that only allows certain URLs."""
134
135
    def __init__(self, should_follow_references, allowed_urls=None,
136
                 check=False):
137
        if allowed_urls is None:
138
            allowed_urls = []
139
        self.allowed_urls = set(url.rstrip('/') for url in allowed_urls)
140
        self.check = check
141
142
    def should_follow_references(self):
143
        return self._should_follow_references
144
145
    def check_one_url(self, url):
146
        if url.rstrip('/') not in self.allowed_urls:
147
            raise BadUrl(url)
148
149
    def transform_fallback_location(self, branch, url):
150
        """See `BranchOpenPolicy.transform_fallback_location`.
151
152
        Here we return the URL that would be used anyway and optionally check
153
        it.
154
        """
155
        return urlutils.join(branch.base, url), self.check
156
157
158
class SingleSchemePolicy(BranchOpenPolicy):
159
    """Branch open policy that rejects URLs not on the given scheme."""
160
161
    def __init__(self, allowed_scheme):
162
        self.allowed_scheme = allowed_scheme
163
164
    def should_follow_references(self):
165
        return True
166
167
    def transform_fallback_location(self, branch, url):
168
        return urlutils.join(branch.base, url), True
169
170
    def check_one_url(self, url):
171
        """Check that `url` is safe to open."""
172
        if urlutils.URL.from_string(str(url)).scheme != self.allowed_scheme:
173
            raise BadUrl(url)
174
175
176
class SafeBranchOpener(object):
177
    """Safe branch opener.
178
179
    All locations that are opened (stacked-on branches, references) are
180
    checked against a policy object.
181
182
    The policy object is expected to have the following methods:
183
    * check_one_url 
184
    * should_follow_references
185
    * transform_fallback_location
186
    """
187
188
    _threading_data = threading.local()
189
190
    def __init__(self, policy, probers=None):
191
        """Create a new SafeBranchOpener.
192
193
        :param policy: The opener policy to use.
194
        :param probers: Optional list of probers to allow.
195
            Defaults to local and remote bzr probers.
196
        """
197
        self.policy = policy
198
        self._seen_urls = set()
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
199
        self.probers = probers
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
200
201
    @classmethod
202
    def install_hook(cls):
203
        """Install the ``transform_fallback_location`` hook.
204
205
        This is done at module import time, but transform_fallback_locationHook
206
        doesn't do anything unless the `_active_openers` threading.Local
207
        object has a 'opener' attribute in this thread.
208
209
        This is in a module-level function rather than performed at module
210
        level so that it can be called in setUp for testing `SafeBranchOpener`
211
        as bzrlib.tests.TestCase.setUp clears hooks.
212
        """
213
        Branch.hooks.install_named_hook(
214
            'transform_fallback_location',
215
            cls.transform_fallback_locationHook,
216
            'SafeBranchOpener.transform_fallback_locationHook')
217
218
    def check_and_follow_branch_reference(self, url):
219
        """Check URL (and possibly the referenced URL) for safety.
220
221
        This method checks that `url` passes the policy's `check_one_url`
222
        method, and if `url` refers to a branch reference, it checks whether
223
        references are allowed and whether the reference's URL passes muster
224
        also -- recursively, until a real branch is found.
225
226
        :param url: URL to check
227
        :raise BranchLoopError: If the branch references form a loop.
228
        :raise BranchReferenceForbidden: If this opener forbids branch
229
            references.
230
        """
231
        while True:
232
            if url in self._seen_urls:
233
                raise BranchLoopError()
234
            self._seen_urls.add(url)
235
            self.policy.check_one_url(url)
236
            next_url = self.follow_reference(url)
237
            if next_url is None:
238
                return url
239
            url = next_url
240
            if not self.policy.should_follow_references():
241
                raise BranchReferenceForbidden(url)
242
243
    @classmethod
244
    def transform_fallback_locationHook(cls, branch, url):
245
        """Installed as the 'transform_fallback_location' Branch hook.
246
247
        This method calls `transform_fallback_location` on the policy object and
248
        either returns the url it provides or passes it back to
249
        check_and_follow_branch_reference.
250
        """
251
        try:
252
            opener = getattr(cls._threading_data, "opener")
253
        except AttributeError:
254
            return url
255
        new_url, check = opener.policy.transform_fallback_location(branch, url)
256
        if check:
257
            return opener.check_and_follow_branch_reference(new_url)
258
        else:
259
            return new_url
260
261
    def run_with_transform_fallback_location_hook_installed(
262
            self, callable, *args, **kw):
6402.3.5 by Jelmer Vernooij
Avoid assert.
263
        if (self.transform_fallback_locationHook not in
264
                Branch.hooks['transform_fallback_location']):
265
            raise AssertionError("hook not installed")
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
266
        self._threading_data.opener = self
267
        try:
268
            return callable(*args, **kw)
269
        finally:
270
            del self._threading_data.opener
271
            # We reset _seen_urls here to avoid multiple calls to open giving
272
            # spurious loop exceptions.
273
            self._seen_urls = set()
274
275
    def follow_reference(self, url):
276
        """Get the branch-reference value at the specified url.
277
278
        This exists as a separate method only to be overriden in unit tests.
279
        """
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
280
        bzrdir = ControlDir.open(url, probers=self.probers)
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
281
        return bzrdir.get_branch_reference()
282
283
    def open(self, url):
284
        """Open the Bazaar branch at url, first checking for safety.
285
286
        What safety means is defined by a subclasses `follow_reference` and
287
        `check_one_url` methods.
288
        """
6402.3.2 by Jelmer Vernooij
bzrify
289
        if type(url) != str:
290
            raise TypeError
291
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
292
        url = self.check_and_follow_branch_reference(url)
293
294
        def open_branch(url):
6402.3.3 by Jelmer Vernooij
Simplify safe open a bit more.
295
            dir = ControlDir.open(url, probers=self.probers)
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
296
            return dir.open_branch()
297
        return self.run_with_transform_fallback_location_hook_installed(
298
            open_branch, url)
299
300
301
def safe_open(allowed_scheme, url):
302
    """Open the branch at `url`, only accessing URLs on `allowed_scheme`.
303
304
    :raises BadUrl: An attempt was made to open a URL that was not on
305
        `allowed_scheme`.
306
    """
307
    return SafeBranchOpener(SingleSchemePolicy(allowed_scheme)).open(url)
308
309
310
SafeBranchOpener.install_hook()