Merge ~cjwatson/turnip:non-ascii-ref-permissions into turnip:master
- Git
- lp:~cjwatson/turnip
- non-ascii-ref-permissions
- Merge into master
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Colin Watson | ||||
Approved revision: | 75e777e8ff5fbbc79fd259d706539b6c8473f6b1 | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~cjwatson/turnip:non-ascii-ref-permissions | ||||
Merge into: | turnip:master | ||||
Prerequisite: | ~cjwatson/turnip:hookrpc-test | ||||
Diff against target: |
521 lines (+181/-49) 6 files modified
turnip/pack/hookrpc.py (+11/-7) turnip/pack/hooks/hook.py (+20/-13) turnip/pack/tests/fake_servers.py (+4/-1) turnip/pack/tests/test_functional.py (+48/-10) turnip/pack/tests/test_hookrpc.py (+39/-12) turnip/pack/tests/test_hooks.py (+59/-6) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tom Wardill (community) | Approve | ||
Launchpad code reviewers | Pending | ||
Review via email: mp+359152@code.launchpad.net |
Commit message
Use new bytes-capable checkRefPermissions protocol
Git ref paths may not be valid UTF-8, so we need to treat them as bytes.
We can't deal perfectly with non-UTF-8 refs - they won't get scanned and
so won't show up in the webservice API or the web UI - but we can at
least allow them to round-trip through Launchpad at the git level. This
migrates to a new version of the checkRefPermissions protocol which can
cope with ref paths being bytes rather than text.
LP: #1517559
Description of the change
https:/
To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/turnip/pack/hookrpc.py b/turnip/pack/hookrpc.py |
2 | index 7d818bf..b024734 100644 |
3 | --- a/turnip/pack/hookrpc.py |
4 | +++ b/turnip/pack/hookrpc.py |
5 | @@ -1,4 +1,4 @@ |
6 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
7 | +# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
8 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | |
10 | """RPC server for Git hooks. |
11 | @@ -20,8 +20,10 @@ from __future__ import ( |
12 | unicode_literals, |
13 | ) |
14 | |
15 | +import base64 |
16 | import json |
17 | |
18 | +from six.moves import xmlrpc_client |
19 | from twisted.internet import ( |
20 | defer, |
21 | protocol, |
22 | @@ -124,22 +126,24 @@ class HookRPCHandler(object): |
23 | def checkRefPermissions(self, proto, args): |
24 | """Get permissions for a set of refs.""" |
25 | cached_permissions = self.ref_permissions[args['key']] |
26 | - missing = [x for x in args['paths'] |
27 | - if x not in cached_permissions] |
28 | + paths = [ |
29 | + base64.b64decode(path.encode('UTF-8')) for path in args['paths']] |
30 | + missing = [x for x in paths if x not in cached_permissions] |
31 | if missing: |
32 | proxy = xmlrpc.Proxy(self.virtinfo_url, allowNone=True) |
33 | result = yield proxy.callRemote( |
34 | b'checkRefPermissions', |
35 | self.ref_paths[args['key']], |
36 | - missing, |
37 | + [xmlrpc_client.Binary(path) for path in missing], |
38 | self.auth_params[args['key']] |
39 | ) |
40 | - for ref, permission in result.items(): |
41 | - cached_permissions[ref] = permission |
42 | + for ref, permission in result: |
43 | + cached_permissions[ref.data] = permission |
44 | # cached_permissions is a shallow copy of the key index for |
45 | # self.ref_permissions, so changes will be updated in that. |
46 | defer.returnValue( |
47 | - {ref: cached_permissions[ref] for ref in args['paths']}) |
48 | + {base64.b64encode(ref).decode('UTF-8'): cached_permissions[ref] |
49 | + for ref in paths}) |
50 | |
51 | @defer.inlineCallbacks |
52 | def notify(self, path): |
53 | diff --git a/turnip/pack/hooks/hook.py b/turnip/pack/hooks/hook.py |
54 | index 1594a4b..22251a2 100755 |
55 | --- a/turnip/pack/hooks/hook.py |
56 | +++ b/turnip/pack/hooks/hook.py |
57 | @@ -1,6 +1,6 @@ |
58 | #!/usr/bin/python |
59 | |
60 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
61 | +# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
62 | # GNU Affero General Public License version 3 (see the file LICENSE). |
63 | |
64 | from __future__ import ( |
65 | @@ -9,6 +9,7 @@ from __future__ import ( |
66 | unicode_literals, |
67 | ) |
68 | |
69 | +import base64 |
70 | import json |
71 | import os |
72 | import socket |
73 | @@ -41,13 +42,13 @@ def determine_permissions_outcome(old, ref, rule_lines): |
74 | if 'create' in rule: |
75 | return |
76 | else: |
77 | - return 'You do not have permission to create %s.' % ref |
78 | + return b'You do not have permission to create ' + ref + b'.' |
79 | # We have push permission, everything is okay |
80 | # force_push is checked later (in update-hook) |
81 | if 'push' in rule: |
82 | return |
83 | # If we're here, there are no matching rules |
84 | - return "You do not have permission to push to %s." % ref |
85 | + return b"You do not have permission to push to " + ref + b"." |
86 | |
87 | |
88 | def match_rules(rule_lines, ref_lines): |
89 | @@ -89,7 +90,7 @@ def match_update_rules(rule_lines, ref_line): |
90 | rule = rule_lines.get(ref, []) |
91 | if 'force_push' in rule: |
92 | return [] |
93 | - return ['You do not have permission to force-push to %s.' % ref] |
94 | + return [b'You do not have permission to force-push to ' + ref + b'.'] |
95 | |
96 | |
97 | def netstring_send(sock, s): |
98 | @@ -120,6 +121,16 @@ def rpc_invoke(sock, method, args): |
99 | return res['result'] |
100 | |
101 | |
102 | +def check_ref_permissions(sock, rpc_key, ref_paths): |
103 | + ref_paths = [base64.b64encode(path).decode('UTF-8') for path in ref_paths] |
104 | + rule_lines = rpc_invoke( |
105 | + sock, b'check_ref_permissions', |
106 | + {'key': rpc_key, 'paths': ref_paths}) |
107 | + return { |
108 | + base64.b64decode(path.encode('UTF-8')): permissions |
109 | + for path, permissions in rule_lines.items()} |
110 | + |
111 | + |
112 | if __name__ == '__main__': |
113 | # Connect to the RPC server, authenticating using the random key |
114 | # from the environment. |
115 | @@ -132,27 +143,23 @@ if __name__ == '__main__': |
116 | # Verify the proposed changes against rules from the server. |
117 | raw_paths = sys.stdin.readlines() |
118 | ref_paths = [p.rstrip(b'\n').split(b' ', 2)[2] for p in raw_paths] |
119 | - rule_lines = rpc_invoke( |
120 | - sock, b'check_ref_permissions', |
121 | - {'key': rpc_key, 'paths': ref_paths}) |
122 | + rule_lines = check_ref_permissions(sock, rpc_key, ref_paths) |
123 | errors = match_rules(rule_lines, raw_paths) |
124 | for error in errors: |
125 | - sys.stdout.write(error + '\n') |
126 | + sys.stdout.write(error + b'\n') |
127 | sys.exit(1 if errors else 0) |
128 | elif hook == 'post-receive': |
129 | # Notify the server about the push if there were any changes. |
130 | # Details of the changes aren't currently included. |
131 | if sys.stdin.readlines(): |
132 | - rule_lines = rpc_invoke(sock, b'notify_push', {'key': rpc_key}) |
133 | + rpc_invoke(sock, b'notify_push', {'key': rpc_key}) |
134 | sys.exit(0) |
135 | elif hook == 'update': |
136 | ref = sys.argv[1] |
137 | - rule_lines = rpc_invoke( |
138 | - sock, b'check_ref_permissions', |
139 | - {'key': rpc_key, 'paths': [ref]}) |
140 | + rule_lines = check_ref_permissions(sock, rpc_key, [ref]) |
141 | errors = match_update_rules(rule_lines, sys.argv[1:4]) |
142 | for error in errors: |
143 | - sys.stdout.write(error + '\n') |
144 | + sys.stdout.write(error + b'\n') |
145 | sys.exit(1 if errors else 0) |
146 | else: |
147 | sys.stderr.write('Invalid hook name: %s' % hook) |
148 | diff --git a/turnip/pack/tests/fake_servers.py b/turnip/pack/tests/fake_servers.py |
149 | index 1e0f310..e5951f0 100644 |
150 | --- a/turnip/pack/tests/fake_servers.py |
151 | +++ b/turnip/pack/tests/fake_servers.py |
152 | @@ -11,6 +11,7 @@ from collections import defaultdict |
153 | import hashlib |
154 | |
155 | from lazr.sshserver.auth import NoSuchPersonWithName |
156 | +from six.moves import xmlrpc_client |
157 | from twisted.web import xmlrpc |
158 | |
159 | __all__ = [ |
160 | @@ -88,4 +89,6 @@ class FakeVirtInfoService(xmlrpc.XMLRPC): |
161 | |
162 | def xmlrpc_checkRefPermissions(self, path, ref_paths, auth_params): |
163 | self.ref_permissions_checks.append((path, ref_paths, auth_params)) |
164 | - return self.ref_permissions |
165 | + return [ |
166 | + (xmlrpc_client.Binary(ref), permissions) |
167 | + for ref, permissions in self.ref_permissions.items()] |
168 | diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py |
169 | index 2fa1782..7d80cdf 100644 |
170 | --- a/turnip/pack/tests/test_functional.py |
171 | +++ b/turnip/pack/tests/test_functional.py |
172 | @@ -81,11 +81,13 @@ class FunctionalTestMixin(object): |
173 | self.virtinfo_url = b'http://localhost:%d/' % self.virtinfo_port |
174 | self.addCleanup(self.virtinfo_listener.stopListening) |
175 | self.virtinfo.ref_permissions = { |
176 | - 'refs/heads/master': ['create', 'push']} |
177 | + b'refs/heads/master': ['create', 'push']} |
178 | |
179 | def startHookRPC(self): |
180 | self.hookrpc_handler = HookRPCHandler(self.virtinfo_url) |
181 | - dir = tempfile.mkdtemp(prefix='turnip-test-hook-') |
182 | + # XXX cjwatson 2018-11-20: Use bytes so that shutil.rmtree doesn't |
183 | + # get confused on Python 2. |
184 | + dir = tempfile.mkdtemp(prefix=b'turnip-test-hook-') |
185 | self.addCleanup(shutil.rmtree, dir, ignore_errors=True) |
186 | |
187 | self.hookrpc_path = os.path.join(dir, 'hookrpc_sock') |
188 | @@ -94,7 +96,9 @@ class FunctionalTestMixin(object): |
189 | self.addCleanup(self.hookrpc_listener.stopListening) |
190 | |
191 | def startPackBackend(self): |
192 | - self.root = tempfile.mkdtemp(prefix='turnip-test-root-') |
193 | + # XXX cjwatson 2018-11-20: Use bytes so that shutil.rmtree doesn't |
194 | + # get confused on Python 2. |
195 | + self.root = tempfile.mkdtemp(prefix=b'turnip-test-root-') |
196 | self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) |
197 | self.backend_listener = reactor.listenTCP( |
198 | 0, |
199 | @@ -224,7 +228,7 @@ class FunctionalTestMixin(object): |
200 | @defer.inlineCallbacks |
201 | def test_no_permissions(self): |
202 | # Update the test ref_permissions |
203 | - self.virtinfo.ref_permissions = {'refs/heads/master': ['push']} |
204 | + self.virtinfo.ref_permissions = {b'refs/heads/master': ['push']} |
205 | # Test a push fails if the user has no permissions to that ref |
206 | test_root = self.useFixture(TempDir()).path |
207 | clone1 = os.path.join(test_root, 'clone1') |
208 | @@ -248,7 +252,7 @@ class FunctionalTestMixin(object): |
209 | error) |
210 | |
211 | # add create, disable push |
212 | - self.virtinfo.ref_permissions = {'refs/heads/master': ['create']} |
213 | + self.virtinfo.ref_permissions = {b'refs/heads/master': ['create']} |
214 | # Can now create the ref |
215 | yield self.assertCommandSuccess( |
216 | (b'git', b'push', b'origin', b'master'), path=clone1) |
217 | @@ -263,10 +267,44 @@ class FunctionalTestMixin(object): |
218 | b"You do not have permission to push to refs/heads/master", error) |
219 | |
220 | @defer.inlineCallbacks |
221 | + def test_push_non_ascii_refs(self): |
222 | + # Pushing non-ASCII refs works. |
223 | + self.virtinfo.ref_permissions = { |
224 | + b'refs/heads/\x80': ['create', 'push'], |
225 | + u'refs/heads/\N{SNOWMAN}'.encode('UTF-8'): ['create', 'push'], |
226 | + } |
227 | + test_root = self.useFixture(TempDir()).path |
228 | + clone1 = os.path.join(test_root, 'clone1') |
229 | + clone2 = os.path.join(test_root, 'clone2') |
230 | + yield self.assertCommandSuccess((b'git', b'clone', self.url, clone1)) |
231 | + yield self.assertCommandSuccess( |
232 | + (b'git', b'config', b'user.name', b'Test User'), path=clone1) |
233 | + yield self.assertCommandSuccess( |
234 | + (b'git', b'config', b'user.email', b'test@example.com'), |
235 | + path=clone1) |
236 | + yield self.assertCommandSuccess( |
237 | + (b'git', b'commit', b'--allow-empty', b'-m', b'Non-ASCII test'), |
238 | + path=clone1) |
239 | + yield self.assertCommandSuccess( |
240 | + (b'git', b'push', b'origin', b'master:\x80', |
241 | + u'master:\N{SNOWMAN}'.encode('UTF-8')), path=clone1) |
242 | + # We get the new branches when we re-clone. |
243 | + yield self.assertCommandSuccess((b'git', b'clone', self.url, clone2)) |
244 | + out = yield self.assertCommandSuccess( |
245 | + (b'git', b'for-each-ref', b'--format=%(refname)', |
246 | + b'refs/remotes/origin/*'), |
247 | + path=clone2) |
248 | + self.assertEqual( |
249 | + sorted([ |
250 | + b'refs/remotes/origin/\x80', |
251 | + u'refs/remotes/origin/\N{SNOWMAN}'.encode('UTF-8')]), |
252 | + sorted(out.splitlines())) |
253 | + |
254 | + @defer.inlineCallbacks |
255 | def test_force_push(self): |
256 | # Update the test ref_permissions |
257 | self.virtinfo.ref_permissions = { |
258 | - 'refs/heads/master': ['create', 'push']} |
259 | + b'refs/heads/master': ['create', 'push']} |
260 | |
261 | # Test a force-push fails if the user has no permissions |
262 | test_root = self.useFixture(TempDir()).path |
263 | @@ -347,7 +385,7 @@ class FunctionalTestMixin(object): |
264 | @defer.inlineCallbacks |
265 | def test_delete_ref(self): |
266 | self.virtinfo.ref_permissions = { |
267 | - 'refs/heads/newref': ['create', 'push', 'force_push']} |
268 | + b'refs/heads/newref': ['create', 'push', 'force_push']} |
269 | test_root = self.useFixture(TempDir()).path |
270 | clone1 = os.path.join(test_root, 'clone1') |
271 | |
272 | @@ -383,7 +421,7 @@ class FunctionalTestMixin(object): |
273 | @defer.inlineCallbacks |
274 | def test_delete_ref_without_permission(self): |
275 | self.virtinfo.ref_permissions = { |
276 | - 'refs/heads/newref': ['create', 'push']} |
277 | + b'refs/heads/newref': ['create', 'push']} |
278 | test_root = self.useFixture(TempDir()).path |
279 | clone1 = os.path.join(test_root, 'clone1') |
280 | |
281 | @@ -471,7 +509,7 @@ class FrontendFunctionalTestMixin(FunctionalTestMixin): |
282 | b'localhost', self.backend_port, self.virtinfo_url)) |
283 | self.virt_port = self.virt_listener.getHost().port |
284 | self.virtinfo.ref_permissions = { |
285 | - 'refs/heads/master': ['create', 'push']} |
286 | + b'refs/heads/master': ['create', 'push']} |
287 | |
288 | @defer.inlineCallbacks |
289 | def tearDown(self): |
290 | @@ -482,7 +520,7 @@ class FrontendFunctionalTestMixin(FunctionalTestMixin): |
291 | @defer.inlineCallbacks |
292 | def test_read_only(self): |
293 | self.virtinfo.ref_permissions = { |
294 | - 'refs/heads/master': ['create', 'push']} |
295 | + b'refs/heads/master': ['create', 'push']} |
296 | test_root = self.useFixture(TempDir()).path |
297 | clone1 = os.path.join(test_root, 'clone1') |
298 | clone2 = os.path.join(test_root, 'clone2') |
299 | diff --git a/turnip/pack/tests/test_hookrpc.py b/turnip/pack/tests/test_hookrpc.py |
300 | index 9af0408..5109ecd 100644 |
301 | --- a/turnip/pack/tests/test_hookrpc.py |
302 | +++ b/turnip/pack/tests/test_hookrpc.py |
303 | @@ -7,11 +7,20 @@ from __future__ import ( |
304 | unicode_literals, |
305 | ) |
306 | |
307 | +import base64 |
308 | import contextlib |
309 | import uuid |
310 | |
311 | +from six.moves import xmlrpc_client |
312 | from testtools import TestCase |
313 | from testtools.deferredruntest import AsynchronousDeferredRunTest |
314 | +from testtools.matchers import ( |
315 | + Equals, |
316 | + IsInstance, |
317 | + MatchesAll, |
318 | + MatchesListwise, |
319 | + MatchesStructure, |
320 | + ) |
321 | from twisted.internet import ( |
322 | defer, |
323 | reactor, |
324 | @@ -164,22 +173,39 @@ class TestHookRPCHandler(TestCase): |
325 | finally: |
326 | self.hookrpc_handler.unregisterKey(key) |
327 | |
328 | + def encodeRefPath(self, ref_path): |
329 | + return base64.b64encode(ref_path).decode('UTF-8') |
330 | + |
331 | def assertCheckedRefPermissions(self, path, ref_paths, auth_params): |
332 | - self.assertEqual( |
333 | - [(path, ref_paths, auth_params)], |
334 | - self.virtinfo.ref_permissions_checks) |
335 | + self.assertThat(self.virtinfo.ref_permissions_checks, MatchesListwise([ |
336 | + MatchesListwise([ |
337 | + Equals(path), |
338 | + MatchesListwise([ |
339 | + MatchesAll( |
340 | + IsInstance(xmlrpc_client.Binary), |
341 | + MatchesStructure.byEquality(data=ref_path)) |
342 | + for ref_path in ref_paths |
343 | + ]), |
344 | + Equals(auth_params), |
345 | + ]), |
346 | + ])) |
347 | |
348 | @defer.inlineCallbacks |
349 | def test_checkRefPermissions_fresh(self): |
350 | self.virtinfo.ref_permissions = { |
351 | - 'refs/heads/master': ['push'], |
352 | - 'refs/heads/next': ['force_push'], |
353 | + b'refs/heads/master': ['push'], |
354 | + b'refs/heads/next': ['force_push'], |
355 | } |
356 | + encoded_paths = [ |
357 | + self.encodeRefPath(ref_path) |
358 | + for ref_path in sorted(self.virtinfo.ref_permissions)] |
359 | with self.registeredKey('/translated', auth_params={'uid': 42}) as key: |
360 | permissions = yield self.hookrpc_handler.checkRefPermissions( |
361 | - None, |
362 | - {'key': key, 'paths': sorted(self.virtinfo.ref_permissions)}) |
363 | - self.assertEqual(self.virtinfo.ref_permissions, permissions) |
364 | + None, {'key': key, 'paths': encoded_paths}) |
365 | + expected_permissions = { |
366 | + self.encodeRefPath(ref_path): perms |
367 | + for ref_path, perms in self.virtinfo.ref_permissions.items()} |
368 | + self.assertEqual(expected_permissions, permissions) |
369 | self.assertCheckedRefPermissions( |
370 | '/translated', [b'refs/heads/master', b'refs/heads/next'], |
371 | {'uid': 42}) |
372 | @@ -187,15 +213,16 @@ class TestHookRPCHandler(TestCase): |
373 | @defer.inlineCallbacks |
374 | def test_checkRefPermissions_cached(self): |
375 | cached_ref_permissions = { |
376 | - 'refs/heads/master': ['push'], |
377 | - 'refs/heads/next': ['force_push'], |
378 | + b'refs/heads/master': ['push'], |
379 | + b'refs/heads/next': ['force_push'], |
380 | } |
381 | + encoded_master = self.encodeRefPath(b'refs/heads/master') |
382 | with self.registeredKey( |
383 | '/translated', auth_params={'uid': 42}, |
384 | permissions=cached_ref_permissions) as key: |
385 | permissions = yield self.hookrpc_handler.checkRefPermissions( |
386 | - None, {'key': key, 'paths': ['refs/heads/master']}) |
387 | - expected_permissions = {'refs/heads/master': ['push']} |
388 | + None, {'key': key, 'paths': [encoded_master]}) |
389 | + expected_permissions = {encoded_master: ['push']} |
390 | self.assertEqual(expected_permissions, permissions) |
391 | self.assertEqual([], self.virtinfo.ref_permissions_checks) |
392 | |
393 | diff --git a/turnip/pack/tests/test_hooks.py b/turnip/pack/tests/test_hooks.py |
394 | index ebf9c80..7aaf356 100644 |
395 | --- a/turnip/pack/tests/test_hooks.py |
396 | +++ b/turnip/pack/tests/test_hooks.py |
397 | @@ -1,4 +1,5 @@ |
398 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
399 | +# -*- coding: utf-8 -*- |
400 | +# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
401 | # GNU Affero General Public License version 3 (see the file LICENSE). |
402 | |
403 | from __future__ import ( |
404 | @@ -7,6 +8,7 @@ from __future__ import ( |
405 | unicode_literals, |
406 | ) |
407 | |
408 | +import base64 |
409 | import os.path |
410 | import uuid |
411 | |
412 | @@ -61,7 +63,9 @@ class MockHookRPCHandler(hookrpc.HookRPCHandler): |
413 | self.notifications.append(self.ref_paths[args['key']]) |
414 | |
415 | def checkRefPermissions(self, proto, args): |
416 | - return self.ref_permissions[args['key']] |
417 | + return { |
418 | + base64.b64encode(ref).decode('UTF-8'): permissions |
419 | + for ref, permissions in self.ref_permissions[args['key']].items()} |
420 | |
421 | |
422 | class MockRef(object): |
423 | @@ -147,7 +151,15 @@ class TestPreReceiveHook(HookTestMixin, TestCase): |
424 | # A single valid ref is accepted. |
425 | yield self.assertAccepted( |
426 | [(b'refs/heads/master', self.old_sha1, self.new_sha1)], |
427 | - {'refs/heads/master': ['push']}) |
428 | + {b'refs/heads/master': ['push']}) |
429 | + |
430 | + @defer.inlineCallbacks |
431 | + def test_accepted_non_ascii(self): |
432 | + # Valid non-ASCII refs are accepted. |
433 | + paths = [b'refs/heads/\x80', u'refs/heads/géag'.encode('UTF-8')] |
434 | + yield self.assertAccepted( |
435 | + [(path, self.old_sha1, self.new_sha1) for path in paths], |
436 | + {path: ['push'] for path in paths}) |
437 | |
438 | @defer.inlineCallbacks |
439 | def test_rejected(self): |
440 | @@ -171,6 +183,16 @@ class TestPreReceiveHook(HookTestMixin, TestCase): |
441 | b"You do not have permission to push " |
442 | b"to refs/heads/super-verboten.\n") |
443 | |
444 | + @defer.inlineCallbacks |
445 | + def test_rejected_non_ascii(self): |
446 | + # Invalid non-ASCII refs are rejected. |
447 | + paths = [b'refs/heads/\x80', u'refs/heads/géag'.encode('UTF-8')] |
448 | + yield self.assertRejected( |
449 | + [(path, self.old_sha1, self.new_sha1) for path in paths], |
450 | + {path: [] for path in paths}, |
451 | + b"You do not have permission to push to refs/heads/\x80.\n" |
452 | + b"You do not have permission to push to refs/heads/g\xc3\xa9ag.\n") |
453 | + |
454 | |
455 | class TestPostReceiveHook(HookTestMixin, TestCase): |
456 | """Tests for the git post-receive hook.""" |
457 | @@ -212,12 +234,12 @@ class TestUpdateHook(TestCase): |
458 | # Creation is determined by an all 0 base sha |
459 | self.assertEqual( |
460 | [], hook.match_update_rules( |
461 | - [], ['ref', pygit2.GIT_OID_HEX_ZERO, 'new'])) |
462 | + {}, [b'ref', pygit2.GIT_OID_HEX_ZERO, 'new'])) |
463 | |
464 | def test_fast_forward(self): |
465 | # If the old sha is a merge ancestor of the new |
466 | self.assertEqual( |
467 | - [], hook.match_update_rules([], ['ref', 'somehex', 'new'])) |
468 | + [], hook.match_update_rules({}, ['ref', 'somehex', 'new'])) |
469 | |
470 | def test_rules_fall_through(self): |
471 | # The default is to deny |
472 | @@ -229,10 +251,27 @@ class TestUpdateHook(TestCase): |
473 | # No matches means deny by default |
474 | output = hook.match_update_rules( |
475 | {'notamatch': []}, |
476 | - ['ref', 'old', 'new']) |
477 | + [b'ref', 'old', 'new']) |
478 | self.assertEqual( |
479 | [b'You do not have permission to force-push to ref.'], output) |
480 | |
481 | + def test_no_matching_non_utf8_ref(self): |
482 | + # An unmatched non-UTF-8 ref is denied. |
483 | + output = hook.match_update_rules( |
484 | + {}, [b'refs/heads/\x80', 'old', 'new']) |
485 | + self.assertEqual( |
486 | + [b'You do not have permission to force-push to refs/heads/\x80.'], |
487 | + output) |
488 | + |
489 | + def test_no_matching_utf8_ref(self): |
490 | + # An unmatched UTF-8 ref is denied. |
491 | + output = hook.match_update_rules( |
492 | + {}, [u'refs/heads/géag'.encode('UTF-8'), 'old', 'new']) |
493 | + self.assertEqual( |
494 | + [b'You do not have permission to force-push to ' |
495 | + b'refs/heads/g\xc3\xa9ag.'], |
496 | + output) |
497 | + |
498 | def test_matching_ref(self): |
499 | # Permission given to force-push |
500 | output = hook.match_update_rules( |
501 | @@ -240,6 +279,20 @@ class TestUpdateHook(TestCase): |
502 | ['ref', 'old', 'new']) |
503 | self.assertEqual([], output) |
504 | |
505 | + def test_matching_non_utf8_ref(self): |
506 | + # A non-UTF-8 ref with force-push permission is accepted. |
507 | + output = hook.match_update_rules( |
508 | + {b'refs/heads/\x80': ['force_push']}, |
509 | + [b'refs/heads/\x80', 'old', 'new']) |
510 | + self.assertEqual([], output) |
511 | + |
512 | + def test_matching_utf8_ref(self): |
513 | + # A UTF-8 ref with force-push permission is accepted. |
514 | + output = hook.match_update_rules( |
515 | + {u'refs/heads/géag'.encode('UTF-8'): ['force_push']}, |
516 | + [u'refs/heads/géag'.encode('UTF-8'), 'old', 'new']) |
517 | + self.assertEqual([], output) |
518 | + |
519 | def test_no_permission(self): |
520 | # User does not have permission to force-push |
521 | output = hook.match_update_rules( |