Merge lp:~thumper/launchpad/bzr-transport-branch-id-access into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Merged at revision: 12758
Proposed branch: lp:~thumper/launchpad/bzr-transport-branch-id-access
Merge into: lp:launchpad
Prerequisite: lp:~thumper/launchpad/anon-http-branch-id-access
Diff against target: 213 lines (+120/-9)
3 files modified
lib/lp/code/xmlrpc/codehosting.py (+30/-3)
lib/lp/code/xmlrpc/tests/test_codehosting.py (+58/-0)
lib/lp/codehosting/inmemory.py (+32/-6)
To merge this branch: bzr merge lp:~thumper/launchpad/bzr-transport-branch-id-access
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+56288@code.launchpad.net

Description of the change

This branch provides support for the +branch-id alias over
the smartserver and sftp through our VFS.

All access to branches using the +branch-id alias are read
only.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

This is good to land with a couple of additions:

124 + def test_translatePath_branch_id_alias_private_branch(self):

and

135 + def test_translatePath_branch_id_alias_private_branch_no_access(self):

Both need docstrings or explanatory comments so that I don't have to read them to understand what they test.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/xmlrpc/codehosting.py'
2--- lib/lp/code/xmlrpc/codehosting.py 2011-02-24 15:30:54 +0000
3+++ lib/lp/code/xmlrpc/codehosting.py 2011-04-05 23:58:43 +0000
4@@ -59,6 +59,7 @@
5 from lp.code.interfaces.branchtarget import IBranchTarget
6 from lp.code.interfaces.codehosting import (
7 BRANCH_ALIAS_PREFIX,
8+ BRANCH_ID_ALIAS_PREFIX,
9 BRANCH_TRANSPORT,
10 CONTROL_TRANSPORT,
11 ICodehostingAPI,
12@@ -287,7 +288,8 @@
13
14 return run_with_login(login_id, branch_changed)
15
16- def _serializeBranch(self, requester, branch, trailing_path):
17+ def _serializeBranch(self, requester, branch, trailing_path,
18+ force_readonly=False):
19 if requester == LAUNCHPAD_SERVICES:
20 branch = removeSecurityProxy(branch)
21 try:
22@@ -296,10 +298,13 @@
23 raise faults.PermissionDenied()
24 if branch.branch_type == BranchType.REMOTE:
25 return None
26+ if force_readonly:
27+ writable = False
28+ else:
29+ writable = self._canWriteToBranch(requester, branch)
30 return (
31 BRANCH_TRANSPORT,
32- {'id': branch_id,
33- 'writable': self._canWriteToBranch(requester, branch)},
34+ {'id': branch_id, 'writable': writable},
35 trailing_path)
36
37 def _serializeControlDirectory(self, requester, product_path,
38@@ -323,12 +328,34 @@
39 {'default_stack_on': escape('/' + unique_name)},
40 trailing_path)
41
42+ def _translateBranchIdAlias(self, requester, path):
43+ # If the path isn't a branch id alias, nothing more to do.
44+ stripped_path = unescape(path.strip('/'))
45+ if not stripped_path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
46+ return None
47+ try:
48+ parts = stripped_path.split('/', 2)
49+ branch_id = int(parts[1])
50+ except (ValueError, IndexError):
51+ raise faults.PathTranslationError(path)
52+ branch = getUtility(IBranchLookup).get(branch_id)
53+ if branch is None:
54+ raise faults.PathTranslationError(path)
55+ try:
56+ trailing = parts[2]
57+ except IndexError:
58+ trailing = ''
59+ return self._serializeBranch(requester, branch, trailing, True)
60+
61 def translatePath(self, requester_id, path):
62 """See `ICodehostingAPI`."""
63 @return_fault
64 def translate_path(requester):
65 if not path.startswith('/'):
66 return faults.InvalidPath(path)
67+ branch = self._translateBranchIdAlias(requester, path)
68+ if branch is not None:
69+ return branch
70 stripped_path = path.strip('/')
71 for first, second in iter_split(stripped_path, '/'):
72 first = unescape(first)
73
74=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
75--- lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-10-13 03:56:46 +0000
76+++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2011-04-05 23:58:43 +0000
77@@ -41,6 +41,7 @@
78 from lp.code.interfaces.branchtarget import IBranchTarget
79 from lp.code.interfaces.codehosting import (
80 BRANCH_ALIAS_PREFIX,
81+ BRANCH_ID_ALIAS_PREFIX,
82 BRANCH_TRANSPORT,
83 CONTROL_TRANSPORT,
84 )
85@@ -991,6 +992,63 @@
86 requester = self.factory.makePerson()
87 self.assertNotFound(requester, '/%s/.bzr' % BRANCH_ALIAS_PREFIX)
88
89+ def test_translatePath_branch_id_alias_bzrdir_content(self):
90+ # translatePath('/+branch-id/.bzr/.*') *must* return not found, otherwise
91+ # bzr will look for it and we don't have a global bzr dir.
92+ requester = self.factory.makePerson()
93+ self.assertNotFound(
94+ requester, '/%s/.bzr/branch-format' % BRANCH_ID_ALIAS_PREFIX)
95+
96+ def test_translatePath_branch_id_alias_bzrdir(self):
97+ # translatePath('/+branch-id/.bzr') *must* return not found, otherwise
98+ # bzr will look for it and we don't have a global bzr dir.
99+ requester = self.factory.makePerson()
100+ self.assertNotFound(requester, '/%s/.bzr' % BRANCH_ID_ALIAS_PREFIX)
101+
102+ def test_translatePath_branch_id_alias_trailing(self):
103+ # Make sure the trailing path is returned.
104+ requester = self.factory.makePerson()
105+ branch = removeSecurityProxy(self.factory.makeAnyBranch())
106+ path = escape(u'/%s/%s/foo/bar' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
107+ translation = self.codehosting_api.translatePath(requester.id, path)
108+ self.assertEqual(
109+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, 'foo/bar'),
110+ translation)
111+
112+ def test_translatePath_branch_id_alias_owned(self):
113+ # Even if the the requester is the owner, the branch is read only.
114+ requester = self.factory.makePerson()
115+ branch = removeSecurityProxy(
116+ self.factory.makeAnyBranch(
117+ branch_type=BranchType.HOSTED, owner=requester))
118+ path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
119+ translation = self.codehosting_api.translatePath(requester.id, path)
120+ self.assertEqual(
121+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
122+ translation)
123+
124+ def test_translatePath_branch_id_alias_private_branch(self):
125+ # Private branches are accessible but read-only even if you are the
126+ # owner.
127+ requester = self.factory.makePerson()
128+ branch = removeSecurityProxy(
129+ self.factory.makeAnyBranch(
130+ branch_type=BranchType.HOSTED, private=True, owner=requester))
131+ path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
132+ translation = self.codehosting_api.translatePath(requester.id, path)
133+ self.assertEqual(
134+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
135+ translation)
136+
137+ def test_translatePath_branch_id_alias_private_branch_no_access(self):
138+ # Private branches you don't have access to raise permission denied.
139+ requester = self.factory.makePerson()
140+ branch = removeSecurityProxy(
141+ self.factory.makeAnyBranch(
142+ branch_type=BranchType.HOSTED, private=True))
143+ path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
144+ self.assertPermissionDenied(requester, path)
145+
146 def assertTranslationIsControlDirectory(self, translation,
147 default_stacked_on,
148 trailing_path):
149
150=== modified file 'lib/lp/codehosting/inmemory.py'
151--- lib/lp/codehosting/inmemory.py 2011-02-24 15:30:54 +0000
152+++ lib/lp/codehosting/inmemory.py 2011-04-05 23:58:43 +0000
153@@ -37,6 +37,7 @@
154 from lp.code.interfaces.branchtarget import IBranchTarget
155 from lp.code.interfaces.codehosting import (
156 BRANCH_ALIAS_PREFIX,
157+ BRANCH_ID_ALIAS_PREFIX,
158 BRANCH_TRANSPORT,
159 CONTROL_TRANSPORT,
160 LAUNCHPAD_ANONYMOUS,
161@@ -806,21 +807,46 @@
162 {'default_stack_on': escape('/' + default_branch.unique_name)},
163 trailing_path)
164
165- def _serializeBranch(self, requester_id, branch, trailing_path):
166+ def _serializeBranch(self, requester_id, branch, trailing_path,
167+ force_readonly=False):
168 if not self._canRead(requester_id, branch):
169 return faults.PermissionDenied()
170 elif branch.branch_type == BranchType.REMOTE:
171 return None
172+ if force_readonly:
173+ writable = False
174 else:
175- return (
176- BRANCH_TRANSPORT,
177- {'id': branch.id,
178- 'writable': self._canWrite(requester_id, branch),
179- }, trailing_path)
180+ writable = self._canWrite(requester_id, branch)
181+ return (
182+ BRANCH_TRANSPORT,
183+ {'id': branch.id, 'writable': writable},
184+ trailing_path)
185+
186+ def _translateBranchIdAlias(self, requester, path):
187+ # If the path isn't a branch id alias, nothing more to do.
188+ stripped_path = unescape(path.strip('/'))
189+ if not stripped_path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
190+ return None
191+ try:
192+ parts = stripped_path.split('/', 2)
193+ branch_id = int(parts[1])
194+ except (ValueError, IndexError):
195+ return faults.PathTranslationError(path)
196+ branch = self._branch_set.get(branch_id)
197+ if branch is None:
198+ return faults.PathTranslationError(path)
199+ try:
200+ trailing = parts[2]
201+ except IndexError:
202+ trailing = ''
203+ return self._serializeBranch(requester, branch, trailing, True)
204
205 def translatePath(self, requester_id, path):
206 if not path.startswith('/'):
207 return faults.InvalidPath(path)
208+ branch = self._translateBranchIdAlias(requester_id, path)
209+ if branch is not None:
210+ return branch
211 stripped_path = path.strip('/')
212 for first, second in iter_split(stripped_path, '/'):
213 first = unescape(first).encode('utf-8')