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() |