Merge lp:~leonardr/launchpad/rename-grant-permissions into lp:launchpad

Proposed by Leonard Richardson
Status: Merged
Merged at revision: 11753
Proposed branch: lp:~leonardr/launchpad/rename-grant-permissions
Merge into: lp:launchpad
Diff against target: 787 lines (+433/-131)
9 files modified
lib/canonical/launchpad/browser/oauth.py (+106/-27)
lib/canonical/launchpad/database/oauth.py (+47/-0)
lib/canonical/launchpad/doc/oauth.txt (+39/-0)
lib/canonical/launchpad/doc/webapp-authorization.txt (+5/-13)
lib/canonical/launchpad/interfaces/oauth.py (+17/-0)
lib/canonical/launchpad/pagetests/oauth/authorize-token.txt (+146/-60)
lib/canonical/launchpad/templates/oauth-authorize.pt (+63/-22)
lib/canonical/launchpad/webapp/authorization.py (+4/-2)
lib/canonical/launchpad/webapp/interfaces.py (+6/-7)
To merge this branch: bzr merge lp:~leonardr/launchpad/rename-grant-permissions
Reviewer Review Type Date Requested Status
Matthew Revell (community) text Approve
Henning Eggers (community) ui* Approve
Curtis Hovey (community) ui Approve
Graham Binns (community) code Approve
Review via email: mp+37590@code.launchpad.net

Description of the change

Earlier, I accidentally proposed this branch for merging into db-devel instead of devel. I make this mistake pretty much every time, and ordinarily I'd just delete the merge proposal and try again, but the old merge proposal acquired significant history before I noticed my mistake. So here's a new merge proposal, and the history is at https://code.edge.launchpad.net/~leonardr/launchpad/rename-grant-permissions/+merge/36363

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Leonard.

We discussed the information presented to the user. We agreed to use "choose" instead of "click" in the text. You explained that launchpadlib provides the OS that appears in the message--we trust it to be right. I was concerned about the exaggeration regarding "all applications running on mycomputer". You noted that "knowing the truth is a prerequisite to knowing that that's an exaggeration". The message is written for the casual user and we are happy to explain that to anyone who asks or report a bug. Maybe an FAQ will be needed in the future

review: Approve (ui)
Revision history for this message
Henning Eggers (henninge) wrote :

I'd like for the page to have a heading of some sort. Otherwise I think it's ok.

Shouldn't you run the text by mrevell? We usually do that for narrative like that.

review: Approve (ui*)
Revision history for this message
Matthew Revell (matthew.revell) :
review: Approve (text)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/oauth.py'
2--- lib/canonical/launchpad/browser/oauth.py 2010-09-20 16:45:03 +0000
3+++ lib/canonical/launchpad/browser/oauth.py 2010-10-14 17:12:48 +0000
4@@ -16,6 +16,7 @@
5 Action,
6 Actions,
7 )
8+from zope.security.interfaces import Unauthorized
9
10 from canonical.launchpad.interfaces.oauth import (
11 IOAuthConsumerSet,
12@@ -88,11 +89,13 @@
13
14 token = consumer.newRequestToken()
15 if self.request.headers.get('Accept') == HTTPResource.JSON_TYPE:
16- # Don't show the client the GRANT_PERMISSIONS access
17+ # Don't show the client the DESKTOP_INTEGRATION access
18 # level. If they have a legitimate need to use it, they'll
19 # already know about it.
20- permissions = [permission for permission in OAuthPermission.items
21- if permission != OAuthPermission.GRANT_PERMISSIONS]
22+ permissions = [
23+ permission for permission in OAuthPermission.items
24+ if (permission != OAuthPermission.DESKTOP_INTEGRATION)
25+ ]
26 return self.getJSONRepresentation(
27 permissions, token, include_secret=True)
28 return u'oauth_token=%s&oauth_token_secret=%s' % (
29@@ -102,26 +105,36 @@
30 return form.token is not None and not form.token.is_reviewed
31
32
33+def token_review_success(form, action, data):
34+ """The success callback for a button to approve a token."""
35+ form.reviewToken(action.permission)
36+
37+
38 def create_oauth_permission_actions():
39- """Return a list of `Action`s for each possible `OAuthPermission`."""
40- actions = Actions()
41- actions_excluding_grant_permissions = Actions()
42- def success(form, action, data):
43- form.reviewToken(action.permission)
44+ """Return two `Actions` objects containing each possible `OAuthPermission`.
45+
46+ The first `Actions` object contains every action supported by the
47+ OAuthAuthorizeTokenView. The second list contains a good default
48+ set of actions, omitting special permissions like DESKTOP_INTEGRATION.
49+ """
50+ all_actions = Actions()
51+ ordinary_actions = Actions()
52 for permission in OAuthPermission.items:
53 action = Action(
54- permission.title, name=permission.name, success=success,
55+ permission.title, name=permission.name,
56+ success=token_review_success,
57 condition=token_exists_and_is_not_reviewed)
58 action.permission = permission
59- actions.append(action)
60- if permission != OAuthPermission.GRANT_PERMISSIONS:
61- actions_excluding_grant_permissions.append(action)
62- return actions, actions_excluding_grant_permissions
63+ all_actions.append(action)
64+ if permission != OAuthPermission.DESKTOP_INTEGRATION:
65+ ordinary_actions.append(action)
66+ return all_actions, ordinary_actions
67+
68
69 class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
70 """Where users authorize consumers to access Launchpad on their behalf."""
71
72- actions, actions_excluding_grant_permissions = (
73+ actions, actions_excluding_special_permissions = (
74 create_oauth_permission_actions())
75 label = "Authorize application to access Launchpad on your behalf"
76 schema = IOAuthRequestToken
77@@ -130,7 +143,7 @@
78
79 @property
80 def visible_actions(self):
81- """Restrict the actions to the subset the client can make use of.
82+ """Restrict the actions to a subset to be presented to the client.
83
84 Not all client programs can function with all levels of
85 access. For instance, a client that needs to modify the
86@@ -150,7 +163,7 @@
87
88 allowed_permissions = self.request.form_ng.getAll('allow_permission')
89 if len(allowed_permissions) == 0:
90- return self.actions_excluding_grant_permissions
91+ return self.actions_excluding_special_permissions
92 actions = Actions()
93
94 # UNAUTHORIZED is always one of the options. If the client
95@@ -159,18 +172,59 @@
96 if OAuthPermission.UNAUTHORIZED.name in allowed_permissions:
97 allowed_permissions.remove(OAuthPermission.UNAUTHORIZED.name)
98
99- # GRANT_PERMISSIONS cannot be requested as one of several
100+ # DESKTOP_INTEGRATION cannot be requested as one of several
101 # options--it must be the only option (other than
102- # UNAUTHORIZED). If GRANT_PERMISSIONS is one of several
103+ # UNAUTHORIZED). If DESKTOP_INTEGRATION is one of several
104 # options, remove it from the list.
105- if (OAuthPermission.GRANT_PERMISSIONS.name in allowed_permissions
106+ desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
107+ if (desktop_permission.name in allowed_permissions
108 and len(allowed_permissions) > 1):
109- allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name)
110-
111- for action in self.actions:
112- if (action.permission.name in allowed_permissions
113- or action.permission is OAuthPermission.UNAUTHORIZED):
114- actions.append(action)
115+ allowed_permissions.remove(desktop_permission.name)
116+
117+ if desktop_permission.name in allowed_permissions:
118+ if not self.token.consumer.is_integrated_desktop:
119+ # Consumers may only ask for desktop integration if
120+ # they give a desktop type (eg. "Ubuntu") and a
121+ # user-recognizable desktop name (eg. the hostname).
122+ raise Unauthorized(
123+ ('Consumer "%s" asked for desktop integration, '
124+ "but didn't say what kind of desktop it is, or name "
125+ "the computer being integrated."
126+ % self.token.consumer.key))
127+
128+ # We're going for desktop integration. The only two
129+ # possibilities are "allow" and "deny". We'll customize
130+ # the "allow" message using the hostname provided by the
131+ # desktop.
132+ #
133+ # Since self.actions is a descriptor that returns copies
134+ # of Action objects, we can modify the actions we get
135+ # in-place without ruining the Action objects for everyone
136+ # else.
137+ desktop_name = self.token.consumer.integrated_desktop_name
138+ label = (
139+ 'Give all programs running on "%s" access '
140+ 'to my Launchpad account.')
141+ allow_action = [
142+ action for action in self.actions
143+ if action.name == desktop_permission.name][0]
144+ allow_action.label = label % desktop_name
145+ actions.append(allow_action)
146+
147+ # We'll customize the "deny" message as well.
148+ label = "No, thanks, I don't trust "%s"."
149+ deny_action = [
150+ action for action in self.actions
151+ if action.name == OAuthPermission.UNAUTHORIZED.name][0]
152+ deny_action.label = label % desktop_name
153+ actions.append(deny_action)
154+
155+ else:
156+ # We're going for web-based integration.
157+ for action in self.actions_excluding_special_permissions:
158+ if (action.permission.name in allowed_permissions
159+ or action.permission is OAuthPermission.UNAUTHORIZED):
160+ actions.append(action)
161
162 if len(list(actions)) == 1:
163 # The only visible action is UNAUTHORIZED. That means the
164@@ -179,8 +233,8 @@
165 # UNAUTHORIZED). Rather than present the end-user with an
166 # impossible situation where their only option is to deny
167 # access, we'll present the full range of actions (except
168- # for GRANT_PERMISSIONS).
169- return self.actions_excluding_grant_permissions
170+ # for special permissions like DESKTOP_INTEGRATION).
171+ return self.actions_excluding_special_permissions
172 return actions
173
174 def initialize(self):
175@@ -189,6 +243,31 @@
176 key = form.get('oauth_token')
177 if key:
178 self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)
179+
180+
181+ callback = self.request.form.get('oauth_callback')
182+ if (self.token is not None
183+ and self.token.consumer.is_integrated_desktop):
184+ # Nip problems in the bud by appling special rules about
185+ # what desktop integrations are allowed to do.
186+ if callback is not None:
187+ # A desktop integration is not allowed to specify a callback.
188+ raise Unauthorized(
189+ "A desktop integration may not specify an "
190+ "OAuth callback URL.")
191+ # A desktop integration token can only have one of two
192+ # permission levels: "Desktop Integration" and
193+ # "Unauthorized". It shouldn't even be able to ask for any
194+ # other level.
195+ for action in self.visible_actions:
196+ if action.permission not in (
197+ OAuthPermission.DESKTOP_INTEGRATION,
198+ OAuthPermission.UNAUTHORIZED):
199+ raise Unauthorized(
200+ ("Desktop integration token requested a permission "
201+ '("%s") not supported for desktop-wide use.')
202+ % action.label)
203+
204 super(OAuthAuthorizeTokenView, self).initialize()
205
206 def render(self):
207
208=== modified file 'lib/canonical/launchpad/database/oauth.py'
209--- lib/canonical/launchpad/database/oauth.py 2010-10-03 15:30:06 +0000
210+++ lib/canonical/launchpad/database/oauth.py 2010-10-14 17:12:48 +0000
211@@ -15,6 +15,7 @@
212 timedelta,
213 )
214
215+import re
216 import pytz
217 from sqlobject import (
218 BoolCol,
219@@ -93,6 +94,7 @@
220
221 getStore = _get_store
222
223+
224 class OAuthConsumer(OAuthBase):
225 """See `IOAuthConsumer`."""
226 implements(IOAuthConsumer)
227@@ -102,6 +104,51 @@
228 key = StringCol(notNull=True)
229 secret = StringCol(notNull=False, default='')
230
231+ # This regular expression singles out a consumer key that
232+ # represents any and all apps running on a specific computer
233+ # (usually a desktop). For instance:
234+ #
235+ # System-wide: Ubuntu desktop (hostname1)
236+ # - An Ubuntu desktop called "hostname1"
237+ # System-wide: Windows desktop (Computer Name)
238+ # - A Windows desktop called "Computer Name"
239+ # System-wide: Mac OS desktop (hostname2)
240+ # - A Macintosh desktop called "hostname2"
241+ # System-wide Android phone (Bob's Phone)
242+ # - An Android phone called "Bob's Phone"
243+ integrated_desktop_re = re.compile("^System-wide: (.*) \(([^)]*)\)$")
244+
245+ def _integrated_desktop_match_group(self, position):
246+ """Return information about a desktop integration token.
247+
248+ A convenience method that runs the desktop integration regular
249+ expression against the consumer key.
250+
251+ :param position: The match group to return if the regular
252+ expression matches.
253+
254+ :return: The value of one of the match groups, or None.
255+ """
256+ match = self.integrated_desktop_re.match(self.key)
257+ if match is None:
258+ return None
259+ return match.groups()[position]
260+
261+ @property
262+ def is_integrated_desktop(self):
263+ """See `IOAuthConsumer`."""
264+ return self.integrated_desktop_re.match(self.key) is not None
265+
266+ @property
267+ def integrated_desktop_type(self):
268+ """See `IOAuthConsumer`."""
269+ return self._integrated_desktop_match_group(0)
270+
271+ @property
272+ def integrated_desktop_name(self):
273+ """See `IOAuthConsumer`."""
274+ return self._integrated_desktop_match_group(1)
275+
276 def newRequestToken(self):
277 """See `IOAuthConsumer`."""
278 key, secret = create_token_key_and_secret(table=OAuthRequestToken)
279
280=== modified file 'lib/canonical/launchpad/doc/oauth.txt'
281--- lib/canonical/launchpad/doc/oauth.txt 2010-10-03 15:30:06 +0000
282+++ lib/canonical/launchpad/doc/oauth.txt 2010-10-14 17:12:48 +0000
283@@ -38,6 +38,45 @@
284 ...
285 AssertionError: ...
286
287+Desktop consumers
288+=================
289+
290+In a web context, each application is represented by a unique consumer
291+key. But a typical user sitting at a typical desktop (or other
292+personal computer), using multiple desktop applications that integrate
293+with Launchpad, is represented by a single consumer key. The user's
294+session as a whole is a single "consumer", and the consumer key is
295+expected to contain structured information: the type of system
296+(usually the operating system plus the word "desktop") and a string
297+that the end-user would recognize as identifying their computer.
298+
299+ >>> desktop_key = consumer_set.new(
300+ ... "System-wide: Ubuntu desktop (hostname)")
301+ >>> desktop_key.is_integrated_desktop
302+ True
303+ >>> print desktop_key.integrated_desktop_type
304+ Ubuntu desktop
305+ >>> print desktop_key.integrated_desktop_name
306+ hostname
307+
308+ >>> desktop_key = consumer_set.new(
309+ ... "System-wide: Android phone (My Phone)")
310+ >>> desktop_key.is_integrated_desktop
311+ True
312+ >>> print desktop_key.integrated_desktop_type
313+ Android phone
314+ >>> print desktop_key.integrated_desktop_name
315+ My Phone
316+
317+A normal OAuth consumer does not have this information.
318+
319+ >>> ordinary_key = consumer_set.new("Not a desktop at all.")
320+ >>> ordinary_key.is_integrated_desktop
321+ False
322+ >>> print ordinary_key.integrated_desktop_type
323+ None
324+ >>> print ordinary_key.integrated_desktop_name
325+ None
326
327 Request tokens
328 ==============
329
330=== modified file 'lib/canonical/launchpad/doc/webapp-authorization.txt'
331--- lib/canonical/launchpad/doc/webapp-authorization.txt 2010-10-03 15:30:06 +0000
332+++ lib/canonical/launchpad/doc/webapp-authorization.txt 2010-10-14 17:12:48 +0000
333@@ -79,24 +79,16 @@
334 >>> check_permission('launchpad.View', bug_1)
335 False
336
337-Now consider a principal authorized to create OAuth tokens. Whenever
338-it's not creating OAuth tokens, it has a level of permission
339-equivalent to READ_PUBLIC.
340+A token used for desktop integration has a level of permission
341+equivalent to WRITE_PUBLIC.
342
343- >>> principal.access_level = AccessLevel.GRANT_PERMISSIONS
344+ >>> principal.access_level = AccessLevel.DESKTOP_INTEGRATION
345 >>> setupInteraction(principal)
346 >>> check_permission('launchpad.View', bug_1)
347- False
348+ True
349
350 >>> check_permission('launchpad.Edit', sample_person)
351- False
352-
353-This may seem useless from a security standpoint, since once a
354-malicious client is authorized to create OAuth tokens, it can escalate
355-its privileges at any time by creating a new token for itself. The
356-security benefit is more subtle: by discouraging feature creep in
357-clients that have this super-access level, we reduce the risk that a
358-bug in a _trusted_ client will enable privilege escalation attacks.
359+ True
360
361 Users logged in through the web application have full access, which
362 means they can read/change any object they have access to.
363
364=== modified file 'lib/canonical/launchpad/interfaces/oauth.py'
365--- lib/canonical/launchpad/interfaces/oauth.py 2010-08-20 20:31:18 +0000
366+++ lib/canonical/launchpad/interfaces/oauth.py 2010-10-14 17:12:48 +0000
367@@ -64,6 +64,23 @@
368 description=_('The secret which, if not empty, should be used by the '
369 'consumer to sign its requests.'))
370
371+ is_integrated_desktop = Attribute(
372+ """This attribute is true if the consumer corresponds to a
373+ user account on a personal computer or similar device.""")
374+
375+ integrated_desktop_name = Attribute(
376+ """If the consumer corresponds to a user account on a personal
377+ computer or similar device, this is the self-reported name of
378+ the computer. If the consumer is a specific web or desktop
379+ application, this is None.""")
380+
381+ integrated_desktop_type = Attribute(
382+ """If the consumer corresponds to a user account on a personal
383+ computer or similar device, this is the self-reported type of
384+ that computer (usually the operating system plus the word
385+ "desktop"). If the consumer is a specific web or desktop
386+ application, this is None.""")
387+
388 def newRequestToken():
389 """Return a new `IOAuthRequestToken` with a random key and secret.
390
391
392=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
393--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-10-03 15:30:06 +0000
394+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-10-14 17:12:48 +0000
395@@ -39,32 +39,29 @@
396
397 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
398 >>> print extract_text(main_content)
399+ Integrating foobar123451432 into your Launchpad account
400 The application identified as foobar123451432 wants to access Launchpad on
401 your behalf. What level of access do you want to grant?
402 ...
403 See all applications authorized to access Launchpad on your behalf.
404
405 This page contains one submit button for each item of OAuthPermission,
406-except for 'Grant Permissions', which must be specifically requested.
407-
408- >>> browser.getControl('No Access')
409- <SubmitControl...
410- >>> browser.getControl('Read Non-Private Data')
411- <SubmitControl...
412- >>> browser.getControl('Change Non-Private Data')
413- <SubmitControl...
414- >>> browser.getControl('Read Anything')
415- <SubmitControl...
416- >>> browser.getControl('Change Anything')
417- <SubmitControl...
418-
419- >>> browser.getControl('Grant Permissions')
420- Traceback (most recent call last):
421- ...
422- LookupError: label 'Grant Permissions'
423-
424+except for 'Desktop Integration', which must be specifically requested.
425+
426+ >>> def print_access_levels(main_content):
427+ ... actions = main_content.findAll('input', attrs={'type': 'submit'})
428+ ... for action in actions:
429+ ... print action['value']
430+
431+ >>> print_access_levels(main_content)
432+ No Access
433+ Read Non-Private Data
434+ Change Non-Private Data
435+ Read Anything
436+ Change Anything
437+
438+ >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
439 >>> actions = main_content.findAll('input', attrs={'type': 'submit'})
440- >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
441 >>> len(actions) == len(OAuthPermission.items) - 1
442 True
443
444@@ -74,57 +71,53 @@
445 that isn't enough for the application. The user always has the option
446 to deny permission altogether.
447
448- >>> def print_access_levels(allow_permission):
449+ >>> def authorize_token_main_content(allow_permission):
450 ... browser.open(
451 ... "http://launchpad.dev/+authorize-token?%s&%s"
452 ... % (urlencode(params), allow_permission))
453- ... main_content = find_tag_by_id(browser.contents, 'maincontent')
454- ... actions = main_content.findAll('input', attrs={'type': 'submit'})
455- ... for action in actions:
456- ... print action['value']
457-
458- >>> print_access_levels(
459+ ... return find_tag_by_id(browser.contents, 'maincontent')
460+
461+ >>> def print_access_levels_for(allow_permission):
462+ ... main_content = authorize_token_main_content(allow_permission)
463+ ... print_access_levels(main_content)
464+
465+ >>> print_access_levels_for(
466 ... 'allow_permission=WRITE_PUBLIC&allow_permission=WRITE_PRIVATE')
467 No Access
468 Change Non-Private Data
469 Change Anything
470
471-The only time the 'Grant Permissions' permission shows up in this list
472-is if the client specifically requests it, and no other
473-permission. (Also requesting UNAUTHORIZED is okay--it will show up
474-anyway.)
475-
476- >>> print_access_levels('allow_permission=GRANT_PERMISSIONS')
477- No Access
478- Grant Permissions
479-
480- >>> print_access_levels(
481- ... 'allow_permission=GRANT_PERMISSIONS&allow_permission=UNAUTHORIZED')
482- No Access
483- Grant Permissions
484-
485- >>> print_access_levels(
486- ... 'allow_permission=WRITE_PUBLIC&allow_permission=GRANT_PERMISSIONS')
487- No Access
488- Change Non-Private Data
489-
490 If an application doesn't specify any valid access levels, or only
491 specifies the UNAUTHORIZED access level, Launchpad will show all the
492-access levels, except for GRANT_PERMISSIONS.
493-
494- >>> print_access_levels('')
495- No Access
496- Read Non-Private Data
497- Change Non-Private Data
498- Read Anything
499- Change Anything
500-
501- >>> print_access_levels('allow_permission=UNAUTHORIZED')
502- No Access
503- Read Non-Private Data
504- Change Non-Private Data
505- Read Anything
506- Change Anything
507+access levels, except for DESKTOP_INTEGRATION.
508+
509+ >>> print_access_levels_for('')
510+ No Access
511+ Read Non-Private Data
512+ Change Non-Private Data
513+ Read Anything
514+ Change Anything
515+
516+ >>> print_access_levels_for('allow_permission=UNAUTHORIZED')
517+ No Access
518+ Read Non-Private Data
519+ Change Non-Private Data
520+ Read Anything
521+ Change Anything
522+
523+An application may not request the DESKTOP_INTEGRATION access level
524+unless its consumer key matches a certain pattern. (Successful desktop
525+integration has its own section, below.)
526+
527+ >>> allow_permission = "allow_permission=DESKTOP_INTEGRATION"
528+ >>> browser.open(
529+ ... "http://launchpad.dev/+authorize-token?%s&%s"
530+ ... % (urlencode(params), allow_permission))
531+ Traceback (most recent call last):
532+ ...
533+ Unauthorized: Consumer "foobar123451432" asked for desktop
534+ integration, but didn't say what kind of desktop it is, or name
535+ the computer being integrated.
536
537 An application may also specify a context, so that the access granted
538 by the user is restricted to things related to that context.
539@@ -136,6 +129,7 @@
540 ... % urlencode(params_with_context))
541 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
542 >>> print extract_text(main_content)
543+ Integrating foobar123451432 into your Launchpad account
544 The application...wants to access things related to Mozilla Firefox...
545
546 A client other than a web browser may request a JSON representation of
547@@ -263,3 +257,95 @@
548 This request for accessing Launchpad on your behalf has been
549 reviewed ... ago.
550 See all applications authorized to access Launchpad on your behalf.
551+
552+Desktop integration
553+===================
554+
555+The test case given above shows how to integrate a single application
556+or website into Launchpad. But it's also possible to integrate an
557+entire desktop environment into Launchpad.
558+
559+The desktop integration option is only available for OAuth consumers
560+that say what kind of desktop they are (eg. Ubuntu) and give a name
561+that a user can identify with their computer (eg. the hostname). Here,
562+we'll create such a token.
563+
564+ >>> login('salgado@ubuntu.com')
565+ >>> desktop_key = "System-wide: Ubuntu desktop (mycomputer)"
566+ >>> consumer = getUtility(IOAuthConsumerSet).new(desktop_key)
567+ >>> token = consumer.newRequestToken()
568+ >>> logout()
569+
570+When a desktop tries to integrate with Launchpad, the user gets a
571+special warning about giving access to every program running on their
572+desktop.
573+
574+ >>> params = dict(oauth_token=token.key)
575+ >>> print extract_text(
576+ ... authorize_token_main_content(
577+ ... 'allow_permission=DESKTOP_INTEGRATION'))
578+ Integrating mycomputer into your Launchpad account
579+ The Ubuntu desktop called mycomputer wants access to your
580+ Launchpad account. If you allow the integration, all applications
581+ running on mycomputer will have read-write access to your
582+ Launchpad account, including to your private data.
583+ If you're using a public computer, if mycomputer is not the
584+ computer you're using right now, or if something just doesn't feel
585+ right about this situation, you should choose "No, thanks, I don't
586+ trust 'mycomputer'", or close this window now. You can always try
587+ again later.
588+ Even if you decide to allow the integration, you can change your
589+ mind later.
590+ See all applications authorized to access Launchpad on your behalf.
591+
592+
593+The only time the 'Desktop Integration' permission shows up in the
594+list of permissions is if the client specifically requests it, and no
595+other permission. (Also requesting UNAUTHORIZED is okay--it will show
596+up anyway.)
597+
598+ >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
599+ Give all programs running on "mycomputer" access to my Launchpad account.
600+ No, thanks, I don't trust "mycomputer".
601+
602+ >>> print_access_levels_for(
603+ ... 'allow_permission=DESKTOP_INTEGRATION&allow_permission=UNAUTHORIZED')
604+ Give all programs running on "mycomputer" access to my Launchpad account.
605+ No, thanks, I don't trust "mycomputer".
606+
607+A desktop may not request a level of access other than
608+DESKTOP_INTEGRATION, since the whole point is to have a permission
609+level that specifically applies across the entire desktop.
610+
611+ >>> print_access_levels_for('allow_permission=WRITE_PRIVATE')
612+ Traceback (most recent call last):
613+ ...
614+ Unauthorized: Desktop integration token requested a permission
615+ ("Change Anything") not supported for desktop-wide use.
616+
617+ >>> print_access_levels_for(
618+ ... 'allow_permission=WRITE_PUBLIC&allow_permission=DESKTOP_INTEGRATION')
619+ Traceback (most recent call last):
620+ ...
621+ Unauthorized: Desktop integration token requested a permission
622+ ("Change Non-Private Data") not supported for desktop-wide use.
623+
624+You can't specify a callback URL when authorizing a desktop-wide
625+token, since callback URLs should only be used when integrating
626+websites into Launchpad.
627+
628+ >>> params['oauth_callback'] = 'http://launchpad.dev/bzr'
629+ >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
630+ Traceback (most recent call last):
631+ ...
632+ Unauthorized: A desktop integration may not specify an OAuth
633+ callback URL.
634+
635+This is true even if the desktop token isn't asking for the
636+DESKTOP_INTEGRATION permission.
637+
638+ >>> print_access_levels_for('allow_permission=WRITE_PRIVATE')
639+ Traceback (most recent call last):
640+ ...
641+ Unauthorized: A desktop integration may not specify an OAuth
642+ callback URL.
643
644=== modified file 'lib/canonical/launchpad/templates/oauth-authorize.pt'
645--- lib/canonical/launchpad/templates/oauth-authorize.pt 2009-07-17 17:59:07 +0000
646+++ lib/canonical/launchpad/templates/oauth-authorize.pt 2010-10-14 17:12:48 +0000
647@@ -21,28 +21,69 @@
648 <tal:token-not-reviewed condition="not:token/is_reviewed">
649 <div metal:use-macro="context/@@launchpad_form/form">
650 <div metal:fill-slot="extra_top">
651- <p>The application identified as
652- <strong tal:content="token/consumer/key">consumer</strong>
653- wants to access
654- <tal:has-context condition="view/token_context">
655- things related to
656- <strong tal:content="view/token_context/title">Context</strong>
657- in
658- </tal:has-context>
659- Launchpad on your behalf. What level of access
660- do you want to grant?</p>
661-
662- <table>
663- <tr tal:repeat="action view/visible_actions">
664- <td style="text-align: right">
665- <tal:action replace="structure action/render" />
666- </td>
667- <td>
668- <span class="lesser"
669- tal:content="action/permission/description" />
670- </td>
671- </tr>
672- </table>
673+
674+ <tal:desktop-integration-token condition="token/consumer/is_integrated_desktop">
675+ <h1>Integrating
676+ <tal:hostname replace="structure
677+ token/consumer/integrated_desktop_name" />
678+ into your Launchpad account</h1>
679+ <p>The
680+ <tal:desktop replace="structure
681+ token/consumer/integrated_desktop_type" />
682+ called
683+ <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
684+ wants access to your Launchpad account. If you allow the
685+ integration, all applications running
686+ on <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
687+ will have read-write access to your Launchpad account,
688+ including to your private data.</p>
689+
690+ <p>If you're using a public computer, if
691+ <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
692+ is not the computer you're using right now, or if
693+ something just doesn't feel right about this situation,
694+ you should choose "No, thanks, I don't trust
695+ '<tal:hostname replace="structure
696+ token/consumer/integrated_desktop_name" />'",
697+ or close this window now. You can always try
698+ again later.</p>
699+
700+ <p>Even if you decide to allow the integration, you can
701+ change your mind later.</p>
702+ </tal:desktop-integration-token>
703+
704+ <tal:web-integration-token condition="not:token/consumer/is_integrated_desktop">
705+ <h1>Integrating
706+ <tal:hostname replace="structure token/consumer/key" />
707+ into your Launchpad account</h1>
708+
709+ <p>The application identified as
710+ <strong tal:content="token/consumer/key">consumer</strong>
711+ wants to access
712+ <tal:has-context condition="view/token_context">
713+ things related to
714+ <strong tal:content="view/token_context/title">Context</strong>
715+ in
716+ </tal:has-context>
717+ Launchpad on your behalf. What level of access
718+ do you want to grant?</p>
719+ </tal:web-integration-token>
720+
721+ <table>
722+ <tr tal:repeat="action view/visible_actions">
723+ <td style="text-align: right">
724+ <tal:action replace="structure action/render" />
725+ </td>
726+
727+ <tal:web-integration-token
728+ condition="not:token/consumer/is_integrated_desktop">
729+ <td>
730+ <span class="lesser"
731+ tal:content="action/permission/description" />
732+ </td>
733+ </tal:web-integration-token>
734+ </tr>
735+ </table>
736 </div>
737
738 <div metal:fill-slot="extra_bottom">
739
740=== modified file 'lib/canonical/launchpad/webapp/authorization.py'
741--- lib/canonical/launchpad/webapp/authorization.py 2010-08-20 20:31:18 +0000
742+++ lib/canonical/launchpad/webapp/authorization.py 2010-10-14 17:12:48 +0000
743@@ -61,7 +61,8 @@
744 lp_permission = getUtility(ILaunchpadPermission, permission)
745 if lp_permission.access_level == "write":
746 required_access_level = [
747- AccessLevel.WRITE_PUBLIC, AccessLevel.WRITE_PRIVATE]
748+ AccessLevel.WRITE_PUBLIC, AccessLevel.WRITE_PRIVATE,
749+ AccessLevel.DESKTOP_INTEGRATION]
750 if access_level not in required_access_level:
751 return False
752 elif lp_permission.access_level == "read":
753@@ -80,7 +81,8 @@
754 access to private objects, return False. Return True otherwise.
755 """
756 private_access_levels = [
757- AccessLevel.READ_PRIVATE, AccessLevel.WRITE_PRIVATE]
758+ AccessLevel.READ_PRIVATE, AccessLevel.WRITE_PRIVATE,
759+ AccessLevel.DESKTOP_INTEGRATION]
760 if access_level in private_access_levels:
761 # The user has access to private objects. Return early,
762 # before checking whether the object is private, since
763
764=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
765--- lib/canonical/launchpad/webapp/interfaces.py 2010-09-24 09:42:01 +0000
766+++ lib/canonical/launchpad/webapp/interfaces.py 2010-10-14 17:12:48 +0000
767@@ -531,14 +531,13 @@
768 for reading and changing anything, including private data.
769 """)
770
771- GRANT_PERMISSIONS = DBItem(60, """
772- Grant Permissions
773+ DESKTOP_INTEGRATION = DBItem(60, """
774+ Desktop Integration
775
776- The application will be able to grant access to your Launchpad
777- account to any other application. This is a very powerful
778- level of access. You should not grant this level of access to
779- any application except the official Launchpad credential
780- manager.
781+ Every application running on your desktop will have read-write
782+ access to your Launchpad account, including to your private
783+ data. You should not allow this unless you trust the computer
784+ you're using right now.
785 """)
786
787 class AccessLevel(DBEnumeratedType):