~launchpad-pqm/launchpad/devel

13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Safe branch opening."""
5
6
__metaclass__ = type
7
13604.1.7 by Jelmer Vernooij
Fix some imports.
8
from bzrlib import urlutils
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
9
from bzrlib.branch import Branch
10
from bzrlib.bzrdir import BzrDir
11
13637.3.3 by Jelmer Vernooij
Move safe_open to lp.codehosting.safe_open.
12
from lazr.uri import URI
13
13677.5.2 by Jelmer Vernooij
Add per-thread data.
14
import threading
15
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
16
__all__ = [
13604.1.7 by Jelmer Vernooij
Fix some imports.
17
    'AcceptAnythingPolicy',
18
    'BadUrl',
19
    'BlacklistPolicy',
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
20
    'BranchLoopError',
13604.1.12 by Jelmer Vernooij
make the import fascist happy.
21
    'BranchOpenPolicy',
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
22
    'BranchReferenceForbidden',
23
    'SafeBranchOpener',
13604.1.7 by Jelmer Vernooij
Fix some imports.
24
    'WhitelistPolicy',
13637.3.4 by Jelmer Vernooij
Add safe_open to __all__.
25
    'safe_open',
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
26
    ]
27
28
13604.1.7 by Jelmer Vernooij
Fix some imports.
29
# TODO JelmerVernooij 2011-08-06: This module is generic enough to be
30
# in bzrlib, and may be of use to others.
31
32
33
class BadUrl(Exception):
34
    """Tried to access a branch from a bad URL."""
35
36
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
37
class BranchReferenceForbidden(Exception):
38
    """Trying to mirror a branch reference and the branch type does not allow
39
    references.
40
    """
41
42
43
class BranchLoopError(Exception):
44
    """Encountered a branch cycle.
45
46
    A URL may point to a branch reference or it may point to a stacked branch.
47
    In either case, it's possible for there to be a cycle in these references,
48
    and this exception is raised when we detect such a cycle.
49
    """
50
51
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
52
class BranchOpenPolicy:
53
    """Policy on how to open branches.
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
54
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
55
    In particular, a policy determines which branches are safe to open by
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
56
    checking their URLs and deciding whether or not to follow branch
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
57
    references.
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
58
    """
59
60
    def shouldFollowReferences(self):
61
        """Whether we traverse references when mirroring.
62
63
        Subclasses must override this method.
64
65
        If we encounter a branch reference and this returns false, an error is
66
        raised.
67
68
        :returns: A boolean to indicate whether to follow a branch reference.
69
        """
70
        raise NotImplementedError(self.shouldFollowReferences)
71
72
    def transformFallbackLocation(self, branch, url):
73
        """Validate, maybe modify, 'url' to be used as a stacked-on location.
74
75
        :param branch:  The branch that is being opened.
76
        :param url: The URL that the branch provides for its stacked-on
77
            location.
78
        :return: (new_url, check) where 'new_url' is the URL of the branch to
79
            actually open and 'check' is true if 'new_url' needs to be
80
            validated by checkAndFollowBranchReference.
81
        """
82
        raise NotImplementedError(self.transformFallbackLocation)
83
84
    def checkOneURL(self, url):
85
        """Check the safety of the source URL.
86
87
        Subclasses must override this method.
88
89
        :param url: The source URL to check.
90
        :raise BadUrl: subclasses are expected to raise this or a subclass
91
            when it finds a URL it deems to be unsafe.
92
        """
93
        raise NotImplementedError(self.checkOneURL)
94
95
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
96
class BlacklistPolicy(BranchOpenPolicy):
13604.1.7 by Jelmer Vernooij
Fix some imports.
97
    """Branch policy that forbids certain URLs."""
98
99
    def __init__(self, should_follow_references, unsafe_urls=None):
100
        if unsafe_urls is None:
101
            unsafe_urls = set()
102
        self._unsafe_urls = unsafe_urls
103
        self._should_follow_references = should_follow_references
104
105
    def shouldFollowReferences(self):
106
        return self._should_follow_references
107
108
    def checkOneURL(self, url):
109
        if url in self._unsafe_urls:
110
            raise BadUrl(url)
111
112
    def transformFallbackLocation(self, branch, url):
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
113
        """See `BranchOpenPolicy.transformFallbackLocation`.
13604.1.7 by Jelmer Vernooij
Fix some imports.
114
115
        This class is not used for testing our smarter stacking features so we
116
        just do the simplest thing: return the URL that would be used anyway
117
        and don't check it.
118
        """
119
        return urlutils.join(branch.base, url), False
120
121
122
class AcceptAnythingPolicy(BlacklistPolicy):
123
    """Accept anything, to make testing easier."""
124
125
    def __init__(self):
126
        super(AcceptAnythingPolicy, self).__init__(True, set())
127
128
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
129
class WhitelistPolicy(BranchOpenPolicy):
13604.1.7 by Jelmer Vernooij
Fix some imports.
130
    """Branch policy that only allows certain URLs."""
131
132
    def __init__(self, should_follow_references, allowed_urls=None,
133
                 check=False):
134
        if allowed_urls is None:
135
            allowed_urls = []
136
        self.allowed_urls = set(url.rstrip('/') for url in allowed_urls)
137
        self.check = check
138
139
    def shouldFollowReferences(self):
140
        return self._should_follow_references
141
142
    def checkOneURL(self, url):
143
        if url.rstrip('/') not in self.allowed_urls:
144
            raise BadUrl(url)
145
146
    def transformFallbackLocation(self, branch, url):
13604.1.8 by Jelmer Vernooij
BranchPolicy -> BranchOpenPolicy.
147
        """See `BranchOpenPolicy.transformFallbackLocation`.
13604.1.7 by Jelmer Vernooij
Fix some imports.
148
149
        Here we return the URL that would be used anyway and optionally check
150
        it.
151
        """
152
        return urlutils.join(branch.base, url), self.check
153
154
13677.5.5 by Jelmer Vernooij
more docstrings, tests.
155
class SingleSchemePolicy(BranchOpenPolicy):
156
    """Branch open policy that rejects URLs not on the given scheme."""
157
158
    def __init__(self, allowed_scheme):
159
        self.allowed_scheme = allowed_scheme
160
161
    def shouldFollowReferences(self):
162
        return True
163
164
    def transformFallbackLocation(self, branch, url):
165
        return urlutils.join(branch.base, url), True
166
167
    def checkOneURL(self, url):
168
        """Check that `url` is safe to open."""
169
        if URI(url).scheme != self.allowed_scheme:
170
            raise BadUrl(url)
171
172
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
173
class SafeBranchOpener(object):
174
    """Safe branch opener.
175
13677.5.5 by Jelmer Vernooij
more docstrings, tests.
176
    All locations that are opened (stacked-on branches, references) are checked
177
    against a policy object.
178
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
179
    The policy object is expected to have the following methods:
180
    * checkOneURL
181
    * shouldFollowReferences
182
    * transformFallbackLocation
183
    """
184
13677.5.2 by Jelmer Vernooij
Add per-thread data.
185
    _threading_data = threading.local()
186
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
187
    def __init__(self, policy):
188
        self.policy = policy
189
        self._seen_urls = set()
190
13677.5.3 by Jelmer Vernooij
use thread data
191
    @classmethod
192
    def install_hook(cls):
13677.5.1 by Jelmer Vernooij
Move hook installation to method.
193
        """Install the ``transformFallbackLocation`` hook.
194
195
        This is done at module import time, but transformFallbackLocationHook
196
        doesn't do anything unless the `_active_openers` threading.Local object
197
        has a 'opener' attribute in this thread.
198
199
        This is in a module-level function rather than performed at module level
13677.5.4 by Jelmer Vernooij
Install hook globally.
200
        so that it can be called in setUp for testing `SafeBranchOpener` as
13677.5.1 by Jelmer Vernooij
Move hook installation to method.
201
        bzrlib.tests.TestCase.setUp clears hooks.
202
        """
203
        Branch.hooks.install_named_hook(
204
            'transform_fallback_location',
13677.5.3 by Jelmer Vernooij
use thread data
205
            cls.transformFallbackLocationHook,
13677.5.1 by Jelmer Vernooij
Move hook installation to method.
206
            'SafeBranchOpener.transformFallbackLocationHook')
207
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
208
    def checkAndFollowBranchReference(self, url, open_dir=None):
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
209
        """Check URL (and possibly the referenced URL) for safety.
210
211
        This method checks that `url` passes the policy's `checkOneURL`
212
        method, and if `url` refers to a branch reference, it checks whether
213
        references are allowed and whether the reference's URL passes muster
214
        also -- recursively, until a real branch is found.
215
216
        :raise BranchLoopError: If the branch references form a loop.
217
        :raise BranchReferenceForbidden: If this opener forbids branch
218
            references.
219
        """
220
        while True:
221
            if url in self._seen_urls:
222
                raise BranchLoopError()
223
            self._seen_urls.add(url)
224
            self.policy.checkOneURL(url)
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
225
            next_url = self.followReference(url, open_dir=open_dir)
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
226
            if next_url is None:
227
                return url
228
            url = next_url
229
            if not self.policy.shouldFollowReferences():
230
                raise BranchReferenceForbidden(url)
231
13677.5.2 by Jelmer Vernooij
Add per-thread data.
232
    @classmethod
233
    def transformFallbackLocationHook(cls, branch, url):
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
234
        """Installed as the 'transform_fallback_location' Branch hook.
235
236
        This method calls `transformFallbackLocation` on the policy object and
237
        either returns the url it provides or passes it back to
238
        checkAndFollowBranchReference.
239
        """
13677.5.3 by Jelmer Vernooij
use thread data
240
        try:
241
            opener = getattr(cls._threading_data, "opener")
242
        except AttributeError:
243
            return url
13677.5.2 by Jelmer Vernooij
Add per-thread data.
244
        new_url, check = opener.policy.transformFallbackLocation(branch, url)
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
245
        if check:
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
246
            return opener.checkAndFollowBranchReference(new_url,
247
                getattr(cls._threading_data, "open_dir"))
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
248
        else:
249
            return new_url
250
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
251
    def _runWithTransformFallbackLocationHookInstalled(
252
            self, open_dir, callable, *args, **kw):
13677.5.4 by Jelmer Vernooij
Install hook globally.
253
        assert (self.transformFallbackLocationHook in
254
                Branch.hooks['transform_fallback_location'])
13677.5.2 by Jelmer Vernooij
Add per-thread data.
255
        self._threading_data.opener = self
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
256
        self._threading_data.open_dir = open_dir
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
257
        try:
258
            return callable(*args, **kw)
259
        finally:
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
260
            del self._threading_data.open_dir
13677.5.2 by Jelmer Vernooij
Add per-thread data.
261
            del self._threading_data.opener
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
262
            # We reset _seen_urls here to avoid multiple calls to open giving
263
            # spurious loop exceptions.
264
            self._seen_urls = set()
265
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
266
    def followReference(self, url, open_dir=None):
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
267
        """Get the branch-reference value at the specified url.
268
269
        This exists as a separate method only to be overriden in unit tests.
270
        """
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
271
        if open_dir is None:
272
            open_dir = BzrDir.open
273
        bzrdir = open_dir(url)
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
274
        return bzrdir.get_branch_reference()
275
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
276
    def open(self, url, open_dir=None):
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
277
        """Open the Bazaar branch at url, first checking for safety.
278
279
        What safety means is defined by a subclasses `followReference` and
280
        `checkOneURL` methods.
281
        """
13756.3.13 by Jelmer Vernooij
Simplify safe open handling.
282
        url = self.checkAndFollowBranchReference(url, open_dir=open_dir)
283
        if open_dir is None:
284
            open_dir = BzrDir.open
285
        def open_branch(url):
286
            dir = open_dir(url)
287
            return dir.open_branch()
288
        return self._runWithTransformFallbackLocationHookInstalled(
289
            open_dir, open_branch, url)
13637.3.3 by Jelmer Vernooij
Move safe_open to lp.codehosting.safe_open.
290
291
292
def safe_open(allowed_scheme, url):
293
    """Open the branch at `url`, only accessing URLs on `allowed_scheme`.
294
295
    :raises BadUrl: An attempt was made to open a URL that was not on
296
        `allowed_scheme`.
297
    """
13677.5.5 by Jelmer Vernooij
more docstrings, tests.
298
    return SafeBranchOpener(SingleSchemePolicy(allowed_scheme)).open(url)
13677.5.4 by Jelmer Vernooij
Install hook globally.
299
300
301
SafeBranchOpener.install_hook()