Merge lp:~maxiberta/launchpad/named-auth-tokens into lp:launchpad
- named-auth-tokens
- Merge into devel
Proposed by
Maximiliano Bertacchini
Status: | Merged |
---|---|
Merged at revision: | 18134 |
Proposed branch: | lp:~maxiberta/launchpad/named-auth-tokens |
Merge into: | lp:launchpad |
Diff against target: |
624 lines (+305/-33) 8 files modified
lib/lp/services/features/flags.py (+6/-0) lib/lp/soyuz/interfaces/archive.py (+84/-6) lib/lp/soyuz/interfaces/archiveauthtoken.py (+35/-9) lib/lp/soyuz/model/archive.py (+58/-0) lib/lp/soyuz/model/archiveauthtoken.py (+23/-9) lib/lp/soyuz/templates/person-archive-subscription.pt (+1/-1) lib/lp/soyuz/tests/test_archive.py (+94/-5) lib/lp/testing/__init__.py (+4/-3) |
To merge this branch: | bzr merge lp:~maxiberta/launchpad/named-auth-tokens |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+299432@code.launchpad.net |
Commit message
Add new named ArchiveAuthToken API.
Description of the change
Add new named ArchiveAuthToken API. This is required for UA customer delivery of kernel livepatches. See required DB changes in https:/
Still missing:
* Hide behind a feature flag (to submit in a different MP, if that's ok).
Also:
* Use "authorize" instead of "authorise" in Archive related classes.
* Some extra cleanups.
To post a comment you must log in.
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote : | # |
Will fix asap. Thanks!
Revision history for this message
Maximiliano Bertacchini (maxiberta) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote : | # |
Updated with feature flag + fixes and improvements.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/services/features/flags.py' |
2 | --- lib/lp/services/features/flags.py 2016-01-25 12:21:31 +0000 |
3 | +++ lib/lp/services/features/flags.py 2016-07-07 21:32:46 +0000 |
4 | @@ -227,6 +227,12 @@ |
5 | 'disabled', |
6 | 'Git recipes', |
7 | 'https://help.launchpad.net/Packaging/SourceBuilds'), |
8 | + ('soyuz.named_auth_token.allow_new', |
9 | + 'boolean', |
10 | + 'If true, allow creation of named authorization tokens for archives.', |
11 | + 'disabled', |
12 | + 'Named authorization tokens for archives', |
13 | + ''), |
14 | ]) |
15 | |
16 | # The set of all flag names that are documented. |
17 | |
18 | === modified file 'lib/lp/soyuz/interfaces/archive.py' |
19 | --- lib/lp/soyuz/interfaces/archive.py 2016-01-26 15:47:37 +0000 |
20 | +++ lib/lp/soyuz/interfaces/archive.py 2016-07-07 21:32:46 +0000 |
21 | @@ -20,6 +20,7 @@ |
22 | 'CannotUploadToPPA', |
23 | 'CannotUploadToPocket', |
24 | 'CannotUploadToSeries', |
25 | + 'DuplicateTokenName', |
26 | 'FULL_COMPONENT_SUPPORT', |
27 | 'IArchive', |
28 | 'IArchiveAdmin', |
29 | @@ -38,6 +39,8 @@ |
30 | 'InvalidPocketForPPA', |
31 | 'IPPA', |
32 | 'MAIN_ARCHIVE_PURPOSES', |
33 | + 'NAMED_AUTH_TOKEN_FEATURE_FLAG', |
34 | + 'NamedAuthTokenFeatureDisabled', |
35 | 'NoRightsForArchive', |
36 | 'NoRightsForComponent', |
37 | 'NoSuchPPA', |
38 | @@ -92,6 +95,7 @@ |
39 | Text, |
40 | TextLine, |
41 | ) |
42 | +from zope.security.interfaces import Unauthorized |
43 | |
44 | from lp import _ |
45 | from lp.app.errors import NameLookupFailed |
46 | @@ -111,6 +115,9 @@ |
47 | from lp.soyuz.interfaces.component import IComponent |
48 | |
49 | |
50 | +NAMED_AUTH_TOKEN_FEATURE_FLAG = u"soyuz.named_auth_token.allow_new" |
51 | + |
52 | + |
53 | @error_status(httplib.BAD_REQUEST) |
54 | class ArchiveDependencyError(Exception): |
55 | """Raised when an `IArchiveDependency` does not fit the context archive. |
56 | @@ -312,6 +319,21 @@ |
57 | self._fmt % {'processor': processor.name}) |
58 | |
59 | |
60 | +@error_status(httplib.CONFLICT) |
61 | +class DuplicateTokenName(Exception): |
62 | + """Raised when creating a named token and an active token for this archive |
63 | + with this name already exists.""" |
64 | + |
65 | + |
66 | +@error_status(httplib.UNAUTHORIZED) |
67 | +class NamedAuthTokenFeatureDisabled(Unauthorized): |
68 | + """Only certain users can create named authorization tokens.""" |
69 | + |
70 | + def __init__(self): |
71 | + super(NamedAuthTokenFeatureDisabled, self).__init__( |
72 | + "You do not have permission to create named authorization tokens") |
73 | + |
74 | + |
75 | class IArchivePublic(IPrivacy, IHasOwner): |
76 | """An Archive interface for publicly available operations.""" |
77 | # Most of this stuff should really be on View, but it's needed for |
78 | @@ -526,12 +548,12 @@ |
79 | """ |
80 | |
81 | def newAuthToken(person, token=None, date_created=None): |
82 | - """Create a new authorisation token. |
83 | + """Create a new authorization token. |
84 | |
85 | - :param person: An IPerson whom this token is for |
86 | + :param person: An IPerson whom this token is for. |
87 | :param token: Optional unicode text to use as the token. One will be |
88 | - generated if not given |
89 | - :param date_created: Optional, defaults to now |
90 | + generated if not given. |
91 | + :param date_created: Optional, defaults to now. |
92 | |
93 | :return: A new IArchiveAuthToken |
94 | """ |
95 | @@ -1324,7 +1346,7 @@ |
96 | @operation_returns_collection_of(Interface) |
97 | @export_read_operation() |
98 | def getQueueAdminsForComponent(component_name): |
99 | - """Return `IArchivePermission` records for authorised queue admins. |
100 | + """Return `IArchivePermission` records for authorized queue admins. |
101 | |
102 | :param component_name: An `IComponent` or textual name for the |
103 | component. |
104 | @@ -1377,7 +1399,7 @@ |
105 | @export_read_operation() |
106 | @operation_for_version("devel") |
107 | def getQueueAdminsForPocket(pocket, distroseries=None): |
108 | - """Return `IArchivePermission` records for authorised queue admins. |
109 | + """Return `IArchivePermission` records for authorized queue admins. |
110 | |
111 | :param pocket: A `PackagePublishingPocket`. |
112 | :param distroseries: An optional `IDistroSeries`. |
113 | @@ -2062,6 +2084,7 @@ |
114 | :return: a `IArchiveDependency` object targeted to the context |
115 | `IArchive` requiring 'dependency' `IArchive`. |
116 | """ |
117 | + |
118 | @operation_parameters( |
119 | dependency=Reference(schema=Interface, required=True), |
120 | # Really IArchive |
121 | @@ -2074,6 +2097,61 @@ |
122 | :param dependency: is an `IArchive` object. |
123 | """ |
124 | |
125 | + @operation_parameters( |
126 | + name=TextLine(title=_("Authorization token name"), required=True), |
127 | + token=TextLine( |
128 | + title=_("Optional secret for this named token"), required=False)) |
129 | + @export_write_operation() |
130 | + @operation_for_version("devel") |
131 | + def newNamedAuthToken(name, token=None): |
132 | + """Create a new named authorization token. |
133 | + |
134 | + :param name: An identifier string for this token. |
135 | + :param token: Optional unicode text to use as the token. One will be |
136 | + generated if not given. |
137 | + |
138 | + :return: A dictionary where the value of `token` is the secret and |
139 | + the value of `archive_url` is the externally-usable archive URL |
140 | + including basic auth. |
141 | + """ |
142 | + |
143 | + @operation_parameters( |
144 | + name=TextLine(title=_("Authorization token name"), required=True)) |
145 | + @export_read_operation() |
146 | + @operation_for_version("devel") |
147 | + def getNamedAuthToken(name): |
148 | + """Return a named authorization token for the given name in this |
149 | + archive. |
150 | + |
151 | + :param name: The identifier string for a token. |
152 | + |
153 | + :return: A dictionary where the value of `token` is the secret and |
154 | + the value of `archive_url` is the externally-usable archive URL |
155 | + including basic auth. |
156 | + :raises NotFoundError: if no matching token could be found. |
157 | + """ |
158 | + |
159 | + @export_read_operation() |
160 | + @operation_for_version("devel") |
161 | + def getNamedAuthTokens(): |
162 | + """Return a list of named authorization tokens for this archive. |
163 | + |
164 | + :return: A list of dictionaries where the value of `token` is the |
165 | + secret and the value of `archive_url` is the externally-usable |
166 | + archive URL including basic auth. |
167 | + """ |
168 | + |
169 | + @operation_parameters( |
170 | + name=TextLine(title=_("Authorization token name"), required=True)) |
171 | + @export_write_operation() |
172 | + @operation_for_version("devel") |
173 | + def revokeNamedAuthToken(name): |
174 | + """Deactivates a named authorization token. |
175 | + |
176 | + :param name: The identifier string for a token. |
177 | + :raises NotFoundError: if no matching token could be found. |
178 | + """ |
179 | + |
180 | |
181 | class IArchiveAdmin(Interface): |
182 | """Archive interface for operations restricted by commercial.""" |
183 | |
184 | === modified file 'lib/lp/soyuz/interfaces/archiveauthtoken.py' |
185 | --- lib/lp/soyuz/interfaces/archiveauthtoken.py 2013-01-07 02:40:55 +0000 |
186 | +++ lib/lp/soyuz/interfaces/archiveauthtoken.py 2016-07-07 21:32:46 +0000 |
187 | @@ -27,16 +27,16 @@ |
188 | |
189 | |
190 | class IArchiveAuthTokenView(Interface): |
191 | - """Interface for Archive Authorisation Tokens requiring launchpad.View.""" |
192 | + """Interface for Archive Authorization Tokens requiring launchpad.View.""" |
193 | id = Int(title=_('ID'), required=True, readonly=True) |
194 | |
195 | archive = Reference( |
196 | IArchive, title=_("Archive"), required=True, readonly=True, |
197 | - description=_("The archive for this authorisation token.")) |
198 | + description=_("The archive for this authorization token.")) |
199 | |
200 | person = Reference( |
201 | - IPerson, title=_("Person"), required=True, readonly=True, |
202 | - description=_("The person for this authorisation token.")) |
203 | + IPerson, title=_("Person"), required=False, readonly=True, |
204 | + description=_("The person for this authorization token.")) |
205 | person_id = Attribute('db person value') |
206 | |
207 | date_created = Datetime( |
208 | @@ -56,9 +56,20 @@ |
209 | description=_( |
210 | "External archive URL including basic auth for this person")) |
211 | |
212 | - def deactivate(self): |
213 | + name = TextLine( |
214 | + title=_("Name"), required=False, readonly=True, |
215 | + description=_( |
216 | + "The name in the case of a named authorization token, or None.")) |
217 | + |
218 | + def deactivate(): |
219 | """Deactivate the token by setting date_deactivated to UTC_NOW.""" |
220 | |
221 | + def asDict(): |
222 | + """Returns a dictionary where the value of `token` is the secret and |
223 | + the value of `archive_url` is the externally-usable archive URL |
224 | + including basic auth. |
225 | + """ |
226 | + |
227 | |
228 | class IArchiveAuthTokenEdit(Interface): |
229 | """Interface for Archive Auth Tokens requiring launchpad.Edit.""" |
230 | @@ -74,15 +85,15 @@ |
231 | def get(token_id): |
232 | """Retrieve a token by its database ID. |
233 | |
234 | - :param token_id: The database ID |
235 | - :return: An object conforming to IArchiveAuthToken |
236 | + :param token_id: The database ID. |
237 | + :return: An object conforming to `IArchiveAuthToken`. |
238 | """ |
239 | |
240 | def getByToken(token): |
241 | """Retrieve a token by its token text. |
242 | |
243 | :param token: The token text for the token. |
244 | - :return: An object conforming to IArchiveAuthToken |
245 | + :return: An object conforming to `IArchiveAuthToken`. |
246 | """ |
247 | |
248 | def getByArchive(archive): |
249 | @@ -97,5 +108,20 @@ |
250 | |
251 | :param archive: The archive to which the token corresponds. |
252 | :param person: The person to which the token corresponds. |
253 | - :return An object conforming to IArchiveAuthToken or None. |
254 | + :return: An `IArchiveAuthToken` or None. |
255 | + """ |
256 | + |
257 | + def getActiveNamedTokenForArchive(archive, name): |
258 | + """Retrieve an active named token for the given archive and name. |
259 | + |
260 | + :param archive: The archive to which the token corresponds. |
261 | + :param name: The name of a named authorization token. |
262 | + :return An object conforming to `IArchiveAuthToken` or None. |
263 | + """ |
264 | + |
265 | + def getActiveNamedTokensForArchive(archive): |
266 | + """Retrieve all active named tokens for the given archive. |
267 | + |
268 | + :param archive: The archive to which the tokens correspond. |
269 | + :return: A result set containing `IArchiveAuthToken`s. |
270 | """ |
271 | |
272 | === modified file 'lib/lp/soyuz/model/archive.py' |
273 | --- lib/lp/soyuz/model/archive.py 2016-03-14 23:42:45 +0000 |
274 | +++ lib/lp/soyuz/model/archive.py 2016-07-07 21:32:46 +0000 |
275 | @@ -110,6 +110,7 @@ |
276 | sqlvalues, |
277 | ) |
278 | from lp.services.database.stormexpr import BulkUpdate |
279 | +from lp.services.features import getFeatureFlag |
280 | from lp.services.job.interfaces.job import JobStatus |
281 | from lp.services.librarian.model import ( |
282 | LibraryFileAlias, |
283 | @@ -149,6 +150,7 @@ |
284 | CannotUploadToSeries, |
285 | ComponentNotFound, |
286 | default_name_by_purpose, |
287 | + DuplicateTokenName, |
288 | FULL_COMPONENT_SUPPORT, |
289 | IArchive, |
290 | IArchiveSet, |
291 | @@ -160,6 +162,8 @@ |
292 | InvalidPocketForPPA, |
293 | IPPA, |
294 | MAIN_ARCHIVE_PURPOSES, |
295 | + NAMED_AUTH_TOKEN_FEATURE_FLAG, |
296 | + NamedAuthTokenFeatureDisabled, |
297 | NoRightsForArchive, |
298 | NoRightsForComponent, |
299 | NoSuchPPA, |
300 | @@ -1948,6 +1952,60 @@ |
301 | IStore(ArchiveAuthToken).add(archive_auth_token) |
302 | return archive_auth_token |
303 | |
304 | + def newNamedAuthToken(self, name, token=None): |
305 | + """See `IArchive`.""" |
306 | + |
307 | + if not getFeatureFlag(NAMED_AUTH_TOKEN_FEATURE_FLAG): |
308 | + raise NamedAuthTokenFeatureDisabled() |
309 | + |
310 | + # Bail if the archive isn't private |
311 | + if not self.private: |
312 | + raise ArchiveNotPrivate("Archive must be private.") |
313 | + |
314 | + try: |
315 | + # Check for duplicate name. |
316 | + self.getNamedAuthToken(name) |
317 | + raise DuplicateTokenName( |
318 | + "An active token with name %s for archive %s already exists." % |
319 | + (name, self.displayname)) |
320 | + except NotFoundError: |
321 | + # No duplicate name found: continue. |
322 | + pass |
323 | + |
324 | + # Now onto the actual token creation: |
325 | + if token is None: |
326 | + token = create_token(20) |
327 | + archive_auth_token = ArchiveAuthToken() |
328 | + archive_auth_token.archive = self |
329 | + archive_auth_token.name = name |
330 | + archive_auth_token.token = token |
331 | + IStore(ArchiveAuthToken).add(archive_auth_token) |
332 | + return archive_auth_token.asDict() |
333 | + |
334 | + def getNamedAuthToken(self, name): |
335 | + """See `IArchive`.""" |
336 | + token_set = getUtility(IArchiveAuthTokenSet) |
337 | + auth_token = token_set.getActiveNamedTokenForArchive(self, name) |
338 | + if auth_token is not None: |
339 | + return auth_token.asDict() |
340 | + else: |
341 | + raise NotFoundError(name) |
342 | + |
343 | + def getNamedAuthTokens(self): |
344 | + """See `IArchive`.""" |
345 | + token_set = getUtility(IArchiveAuthTokenSet) |
346 | + auth_tokens = token_set.getActiveNamedTokensForArchive(self) |
347 | + return [auth_token.asDict() for auth_token in auth_tokens] |
348 | + |
349 | + def revokeNamedAuthToken(self, name): |
350 | + """See `IArchive`.""" |
351 | + token_set = getUtility(IArchiveAuthTokenSet) |
352 | + auth_token = token_set.getActiveNamedTokenForArchive(self, name) |
353 | + if auth_token is not None: |
354 | + auth_token.deactivate() |
355 | + else: |
356 | + raise NotFoundError(name) |
357 | + |
358 | def newSubscription(self, subscriber, registrant, date_expires=None, |
359 | description=None): |
360 | """See `IArchive`.""" |
361 | |
362 | === modified file 'lib/lp/soyuz/model/archiveauthtoken.py' |
363 | --- lib/lp/soyuz/model/archiveauthtoken.py 2015-10-21 09:37:08 +0000 |
364 | +++ lib/lp/soyuz/model/archiveauthtoken.py 2016-07-07 21:32:46 +0000 |
365 | @@ -39,7 +39,7 @@ |
366 | archive_id = Int(name='archive', allow_none=False) |
367 | archive = Reference(archive_id, 'Archive.id') |
368 | |
369 | - person_id = Int(name='person', allow_none=False) |
370 | + person_id = Int(name='person', allow_none=True) |
371 | person = Reference(person_id, 'Person.id') |
372 | |
373 | date_created = DateTime( |
374 | @@ -50,6 +50,8 @@ |
375 | |
376 | token = Unicode(name='token', allow_none=False) |
377 | |
378 | + name = Unicode(name='name', allow_none=True) |
379 | + |
380 | def deactivate(self): |
381 | """See `IArchiveAuthTokenSet`.""" |
382 | self.date_deactivated = UTC_NOW |
383 | @@ -58,10 +60,16 @@ |
384 | def archive_url(self): |
385 | """Return a custom archive url for basic authentication.""" |
386 | normal_url = URI(self.archive.archive_url) |
387 | - auth_url = normal_url.replace( |
388 | - userinfo="%s:%s" % (self.person.name, self.token)) |
389 | + if self.name: |
390 | + name = '+' + self.name |
391 | + else: |
392 | + name = self.person.name |
393 | + auth_url = normal_url.replace(userinfo="%s:%s" % (name, self.token)) |
394 | return str(auth_url) |
395 | |
396 | + def asDict(self): |
397 | + return {"token": self.token, "archive_url": self.archive_url} |
398 | + |
399 | |
400 | @implementer(IArchiveAuthTokenSet) |
401 | class ArchiveAuthTokenSet: |
402 | @@ -87,9 +95,15 @@ |
403 | |
404 | def getActiveTokenForArchiveAndPerson(self, archive, person): |
405 | """See `IArchiveAuthTokenSet`.""" |
406 | - store = Store.of(archive) |
407 | - return store.find( |
408 | - ArchiveAuthToken, |
409 | - ArchiveAuthToken.archive == archive, |
410 | - ArchiveAuthToken.person == person, |
411 | - ArchiveAuthToken.date_deactivated == None).one() |
412 | + return self.getByArchive(archive).find( |
413 | + ArchiveAuthToken.person == person).one() |
414 | + |
415 | + def getActiveNamedTokenForArchive(self, archive, name): |
416 | + """See `IArchiveAuthTokenSet`.""" |
417 | + return self.getByArchive(archive).find( |
418 | + ArchiveAuthToken.name == name).one() |
419 | + |
420 | + def getActiveNamedTokensForArchive(self, archive): |
421 | + """See `IArchiveAuthTokenSet`.""" |
422 | + return self.getByArchive(archive).find( |
423 | + ArchiveAuthToken.name != None) |
424 | |
425 | === modified file 'lib/lp/soyuz/templates/person-archive-subscription.pt' |
426 | --- lib/lp/soyuz/templates/person-archive-subscription.pt 2012-03-01 18:17:56 +0000 |
427 | +++ lib/lp/soyuz/templates/person-archive-subscription.pt 2016-07-07 21:32:46 +0000 |
428 | @@ -50,7 +50,7 @@ |
429 | <div id="regenerate_token" class="portlet" style="clear:both"> |
430 | <h2>Reset password</h2> |
431 | <p>If you believe the security of your password for this access |
432 | - has been compromised, you reset your password. After you've |
433 | + has been compromised, you should reset your password. After you've |
434 | requested a new password, you'll see new "sources.list" entries |
435 | on this page. You'll need to update them on your computer. |
436 | </p> |
437 | |
438 | === modified file 'lib/lp/soyuz/tests/test_archive.py' |
439 | --- lib/lp/soyuz/tests/test_archive.py 2016-04-07 00:04:42 +0000 |
440 | +++ lib/lp/soyuz/tests/test_archive.py 2016-07-07 21:32:46 +0000 |
441 | @@ -41,6 +41,7 @@ |
442 | from lp.registry.interfaces.teammembership import TeamMembershipStatus |
443 | from lp.services.database.interfaces import IStore |
444 | from lp.services.database.sqlbase import sqlvalues |
445 | +from lp.services.features.testing import FeatureFixture |
446 | from lp.services.job.interfaces.job import JobStatus |
447 | from lp.services.propertycache import ( |
448 | clear_property_cache, |
449 | @@ -65,21 +66,26 @@ |
450 | from lp.soyuz.interfaces.archive import ( |
451 | ArchiveDependencyError, |
452 | ArchiveDisabled, |
453 | + ArchiveNotPrivate, |
454 | CannotCopy, |
455 | CannotModifyArchiveProcessor, |
456 | CannotUploadToPocket, |
457 | CannotUploadToPPA, |
458 | CannotUploadToSeries, |
459 | + DuplicateTokenName, |
460 | IArchiveSet, |
461 | InsufficientUploadRights, |
462 | InvalidPocketForPartnerArchive, |
463 | InvalidPocketForPPA, |
464 | + NAMED_AUTH_TOKEN_FEATURE_FLAG, |
465 | + NamedAuthTokenFeatureDisabled, |
466 | NoRightsForArchive, |
467 | NoRightsForComponent, |
468 | NoSuchPPA, |
469 | RedirectedPocket, |
470 | VersionRequiresName, |
471 | ) |
472 | +from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet |
473 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
474 | from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
475 | from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet |
476 | @@ -1246,6 +1252,23 @@ |
477 | self.assertEqual("really secret", self.archive.buildd_secret) |
478 | |
479 | |
480 | +class TestNamedAuthTokenFeatureFlag(TestCaseWithFactory): |
481 | + layer = LaunchpadZopelessLayer |
482 | + |
483 | + def test_feature_flag_disabled(self): |
484 | + # With feature flag disabled, we will not create new named auth tokens. |
485 | + private_ppa = self.factory.makeArchive(private=True) |
486 | + with FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: u""}): |
487 | + self.assertRaises(NamedAuthTokenFeatureDisabled, |
488 | + private_ppa.newNamedAuthToken, u"tokenname") |
489 | + |
490 | + def test_feature_flag_disabled_by_default(self): |
491 | + # Without a feature flag, we will not create new named auth tokens. |
492 | + private_ppa = self.factory.makeArchive(private=True) |
493 | + self.assertRaises(NamedAuthTokenFeatureDisabled, |
494 | + private_ppa.newNamedAuthToken, u"tokenname") |
495 | + |
496 | + |
497 | class TestArchiveTokens(TestCaseWithFactory): |
498 | layer = LaunchpadZopelessLayer |
499 | |
500 | @@ -1255,13 +1278,14 @@ |
501 | self.private_ppa = self.factory.makeArchive(owner=owner, private=True) |
502 | self.joe = self.factory.makePerson(name='joe') |
503 | self.private_ppa.newSubscription(self.joe, owner) |
504 | + self.useFixture(FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: u"on"})) |
505 | |
506 | def test_getAuthToken_with_no_token(self): |
507 | - token = self.private_ppa.getAuthToken(self.joe) |
508 | - self.assertEqual(token, None) |
509 | + self.assertIsNone(self.private_ppa.getAuthToken(self.joe)) |
510 | |
511 | def test_getAuthToken_with_token(self): |
512 | token = self.private_ppa.newAuthToken(self.joe) |
513 | + self.assertIsNone(token.name) |
514 | self.assertEqual(self.private_ppa.getAuthToken(self.joe), token) |
515 | |
516 | def test_getArchiveSubscriptionURL(self): |
517 | @@ -1269,6 +1293,74 @@ |
518 | token = self.private_ppa.getAuthToken(self.joe) |
519 | self.assertEqual(token.archive_url, url) |
520 | |
521 | + def test_newNamedAuthToken_private_archive(self): |
522 | + res = self.private_ppa.newNamedAuthToken(u"tokenname") |
523 | + token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive( |
524 | + self.private_ppa, u"tokenname") |
525 | + self.assertIsNotNone(token) |
526 | + self.assertIsNone(token.person) |
527 | + self.assertEqual("tokenname", token.name) |
528 | + self.assertIsNotNone(token.token) |
529 | + self.assertEqual(self.private_ppa, token.archive) |
530 | + self.assertIn( |
531 | + "://+%s:%s@" % (token.name, token.token), token.archive_url) |
532 | + self.assertDictEqual( |
533 | + {"token": token.token, "archive_url": token.archive_url}, |
534 | + res |
535 | + ) |
536 | + |
537 | + def test_newNamedAuthToken_public_archive(self): |
538 | + public_ppa = self.factory.makeArchive(private=False) |
539 | + self.assertRaises(ArchiveNotPrivate, |
540 | + public_ppa.newNamedAuthToken, u"tokenname") |
541 | + |
542 | + def test_newNamedAuthToken_duplicate_name(self): |
543 | + self.private_ppa.newNamedAuthToken(u"tokenname") |
544 | + self.assertRaises(DuplicateTokenName, |
545 | + self.private_ppa.newNamedAuthToken, u"tokenname") |
546 | + |
547 | + def test_newNamedAuthToken_with_custom_secret(self): |
548 | + self.private_ppa.newNamedAuthToken(u"tokenname", u"somesecret") |
549 | + token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive( |
550 | + self.private_ppa, u"tokenname") |
551 | + self.assertEqual(u"somesecret", token.token) |
552 | + |
553 | + def test_getNamedAuthToken_with_no_token(self): |
554 | + self.assertRaises( |
555 | + NotFoundError, self.private_ppa.getNamedAuthToken, u"tokenname") |
556 | + |
557 | + def test_getNamedAuthToken_with_token(self): |
558 | + res = self.private_ppa.newNamedAuthToken(u"tokenname") |
559 | + self.assertEqual( |
560 | + self.private_ppa.getNamedAuthToken(u"tokenname"), |
561 | + res) |
562 | + |
563 | + def test_revokeNamedAuthToken_with_token(self): |
564 | + self.private_ppa.newNamedAuthToken(u"tokenname") |
565 | + token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive( |
566 | + self.private_ppa, u"tokenname") |
567 | + self.private_ppa.revokeNamedAuthToken(u"tokenname") |
568 | + self.assertIsNotNone(token.date_deactivated) |
569 | + |
570 | + def test_revokeNamedAuthToken_with_no_token(self): |
571 | + self.assertRaises( |
572 | + NotFoundError, self.private_ppa.revokeNamedAuthToken, u"tokenname") |
573 | + |
574 | + def test_getNamedAuthToken_with_revoked_token(self): |
575 | + self.private_ppa.newNamedAuthToken(u"tokenname") |
576 | + self.private_ppa.revokeNamedAuthToken(u"tokenname") |
577 | + self.assertRaises( |
578 | + NotFoundError, self.private_ppa.getNamedAuthToken, u"tokenname") |
579 | + |
580 | + def test_getNamedAuthTokens(self): |
581 | + res1 = self.private_ppa.newNamedAuthToken(u"tokenname1") |
582 | + res2 = self.private_ppa.newNamedAuthToken(u"tokenname2") |
583 | + self.private_ppa.newNamedAuthToken(u"tokenname3") |
584 | + self.private_ppa.revokeNamedAuthToken(u"tokenname3") |
585 | + self.assertContentEqual( |
586 | + [res1, res2], |
587 | + self.private_ppa.getNamedAuthTokens()) |
588 | + |
589 | |
590 | class TestGetBinaryPackageRelease(TestCaseWithFactory): |
591 | """Ensure that getBinaryPackageRelease works as expected.""" |
592 | @@ -3505,9 +3597,6 @@ |
593 | |
594 | layer = LaunchpadFunctionalLayer |
595 | |
596 | - def assertDictEqual(self, one, two): |
597 | - self.assertContentEqual(one.items(), two.items()) |
598 | - |
599 | def test_cprov_build_counters_in_sampledata(self): |
600 | cprov_archive = getUtility(IPersonSet).getByName("cprov").archive |
601 | expected_counters = { |
602 | |
603 | === modified file 'lib/lp/testing/__init__.py' |
604 | --- lib/lp/testing/__init__.py 2015-11-08 01:05:24 +0000 |
605 | +++ lib/lp/testing/__init__.py 2016-07-07 21:32:46 +0000 |
606 | @@ -619,14 +619,15 @@ |
607 | self.assertIsNot( |
608 | None, pattern.search(normalise_whitespace(text)), text) |
609 | |
610 | - def assertIsInstance(self, instance, assert_class): |
611 | + def assertIsInstance(self, instance, assert_class, msg=None): |
612 | """Assert that an instance is an instance of assert_class. |
613 | |
614 | instance and assert_class have the same semantics as the parameters |
615 | to isinstance. |
616 | """ |
617 | - self.assertTrue(zope_isinstance(instance, assert_class), |
618 | - '%r is not an instance of %r' % (instance, assert_class)) |
619 | + if msg is None: |
620 | + msg = '%r is not an instance of %r' % (instance, assert_class) |
621 | + self.assertTrue(zope_isinstance(instance, assert_class), msg) |
622 | |
623 | def assertIsNot(self, expected, observed, msg=None): |
624 | """Assert that `expected` is not the same object as `observed`.""" |
Good start, thanks, but some bits to fix up.