← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Leonard Richardson has proposed merging lp:~leonardr/launchpad/rename-grant-permissions into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


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
-- 
https://code.launchpad.net/~leonardr/launchpad/rename-grant-permissions/+merge/37590
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~leonardr/launchpad/rename-grant-permissions into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/browser/oauth.py'
--- lib/canonical/launchpad/browser/oauth.py	2010-09-20 16:45:03 +0000
+++ lib/canonical/launchpad/browser/oauth.py	2010-10-05 11:52:47 +0000
@@ -16,6 +16,7 @@
     Action,
     Actions,
     )
+from zope.security.interfaces import Unauthorized
 
 from canonical.launchpad.interfaces.oauth import (
     IOAuthConsumerSet,
@@ -88,11 +89,13 @@
 
         token = consumer.newRequestToken()
         if self.request.headers.get('Accept') == HTTPResource.JSON_TYPE:
-            # Don't show the client the GRANT_PERMISSIONS access
+            # Don't show the client the DESKTOP_INTEGRATION access
             # level. If they have a legitimate need to use it, they'll
             # already know about it.
-            permissions = [permission for permission in OAuthPermission.items
-                           if permission != OAuthPermission.GRANT_PERMISSIONS]
+            permissions = [
+                permission for permission in OAuthPermission.items
+                if (permission != OAuthPermission.DESKTOP_INTEGRATION)
+                ]
             return self.getJSONRepresentation(
                 permissions, token, include_secret=True)
         return u'oauth_token=%s&oauth_token_secret=%s' % (
@@ -102,26 +105,36 @@
     return form.token is not None and not form.token.is_reviewed
 
 
+def token_review_success(form, action, data):
+    """The success callback for a button to approve a token."""
+    form.reviewToken(action.permission)
+
+
 def create_oauth_permission_actions():
-    """Return a list of `Action`s for each possible `OAuthPermission`."""
-    actions = Actions()
-    actions_excluding_grant_permissions = Actions()
-    def success(form, action, data):
-        form.reviewToken(action.permission)
+    """Return two `Actions` objects containing each possible `OAuthPermission`.
+
+    The first `Actions` object contains every action supported by the
+    OAuthAuthorizeTokenView. The second list contains a good default
+    set of actions, omitting special permissions like DESKTOP_INTEGRATION.
+    """
+    all_actions = Actions()
+    ordinary_actions = Actions()
     for permission in OAuthPermission.items:
         action = Action(
-            permission.title, name=permission.name, success=success,
+            permission.title, name=permission.name,
+            success=token_review_success,
             condition=token_exists_and_is_not_reviewed)
         action.permission = permission
-        actions.append(action)
-        if permission != OAuthPermission.GRANT_PERMISSIONS:
-            actions_excluding_grant_permissions.append(action)
-    return actions, actions_excluding_grant_permissions
+        all_actions.append(action)
+        if permission != OAuthPermission.DESKTOP_INTEGRATION:
+            ordinary_actions.append(action)
+    return all_actions, ordinary_actions
+
 
 class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
     """Where users authorize consumers to access Launchpad on their behalf."""
 
-    actions, actions_excluding_grant_permissions = (
+    actions, actions_excluding_special_permissions = (
         create_oauth_permission_actions())
     label = "Authorize application to access Launchpad on your behalf"
     schema = IOAuthRequestToken
@@ -130,7 +143,7 @@
 
     @property
     def visible_actions(self):
-        """Restrict the actions to the subset the client can make use of.
+        """Restrict the actions to a subset to be presented to the client.
 
         Not all client programs can function with all levels of
         access. For instance, a client that needs to modify the
@@ -150,7 +163,7 @@
 
         allowed_permissions = self.request.form_ng.getAll('allow_permission')
         if len(allowed_permissions) == 0:
-            return self.actions_excluding_grant_permissions
+            return self.actions_excluding_special_permissions
         actions = Actions()
 
         # UNAUTHORIZED is always one of the options. If the client
@@ -159,18 +172,59 @@
         if OAuthPermission.UNAUTHORIZED.name in allowed_permissions:
             allowed_permissions.remove(OAuthPermission.UNAUTHORIZED.name)
 
-        # GRANT_PERMISSIONS cannot be requested as one of several
+        # DESKTOP_INTEGRATION cannot be requested as one of several
         # options--it must be the only option (other than
-        # UNAUTHORIZED). If GRANT_PERMISSIONS is one of several
+        # UNAUTHORIZED). If DESKTOP_INTEGRATION is one of several
         # options, remove it from the list.
-        if (OAuthPermission.GRANT_PERMISSIONS.name in allowed_permissions
+        desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
+        if (desktop_permission.name in allowed_permissions
             and len(allowed_permissions) > 1):
-            allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name)
-
-        for action in self.actions:
-            if (action.permission.name in allowed_permissions
-                or action.permission is OAuthPermission.UNAUTHORIZED):
-                actions.append(action)
+            allowed_permissions.remove(desktop_permission.name)
+
+        if desktop_permission.name in allowed_permissions:
+            if not self.token.consumer.is_integrated_desktop:
+                # Consumers may only ask for desktop integration if
+                # they give a desktop type (eg. "Ubuntu") and a
+                # user-recognizable desktop name (eg. the hostname).
+                raise Unauthorized(
+                    ('Consumer "%s" asked for desktop integration, '
+                     "but didn't say what kind of desktop it is, or name "
+                     "the computer being integrated."
+                     % self.token.consumer.key))
+
+            # We're going for desktop integration. The only two
+            # possibilities are "allow" and "deny". We'll customize
+            # the "allow" message using the hostname provided by the
+            # desktop.
+            #
+            # Since self.actions is a descriptor that returns copies
+            # of Action objects, we can modify the actions we get
+            # in-place without ruining the Action objects for everyone
+            # else.
+            desktop_name = self.token.consumer.integrated_desktop_name
+            label = (
+                'Give all programs running on "%s" access '
+                'to my Launchpad account.')
+            allow_action = [
+                action for action in self.actions
+                if action.name == desktop_permission.name][0]
+            allow_action.label = label % desktop_name
+            actions.append(allow_action)
+
+            # We'll customize the "deny" message as well.
+            label = "No, thanks, I don't trust "%s"."
+            deny_action = [
+                action for action in self.actions
+                if action.name == OAuthPermission.UNAUTHORIZED.name][0]
+            deny_action.label = label % desktop_name
+            actions.append(deny_action)
+
+        else:
+            # We're going for web-based integration.
+            for action in self.actions_excluding_special_permissions:
+                if (action.permission.name in allowed_permissions
+                    or action.permission is OAuthPermission.UNAUTHORIZED):
+                    actions.append(action)
 
         if len(list(actions)) == 1:
             # The only visible action is UNAUTHORIZED. That means the
@@ -179,8 +233,8 @@
             # UNAUTHORIZED). Rather than present the end-user with an
             # impossible situation where their only option is to deny
             # access, we'll present the full range of actions (except
-            # for GRANT_PERMISSIONS).
-            return self.actions_excluding_grant_permissions
+            # for special permissions like DESKTOP_INTEGRATION).
+            return self.actions_excluding_special_permissions
         return actions
 
     def initialize(self):
@@ -189,6 +243,31 @@
         key = form.get('oauth_token')
         if key:
             self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)
+
+
+        callback = self.request.form.get('oauth_callback')
+        if (self.token is not None
+            and self.token.consumer.is_integrated_desktop):
+            # Nip problems in the bud by appling special rules about
+            # what desktop integrations are allowed to do.
+            if callback is not None:
+                # A desktop integration is not allowed to specify a callback.
+                raise Unauthorized(
+                    "A desktop integration may not specify an "
+                    "OAuth callback URL.")
+            # A desktop integration token can only have one of two
+            # permission levels: "Desktop Integration" and
+            # "Unauthorized". It shouldn't even be able to ask for any
+            # other level.
+            for action in self.visible_actions:
+                if action.permission not in (
+                    OAuthPermission.DESKTOP_INTEGRATION,
+                    OAuthPermission.UNAUTHORIZED):
+                    raise Unauthorized(
+                        ("Desktop integration token requested a permission "
+                         '("%s") not supported for desktop-wide use.')
+                         % action.label)
+
         super(OAuthAuthorizeTokenView, self).initialize()
 
     def render(self):

=== modified file 'lib/canonical/launchpad/database/oauth.py'
--- lib/canonical/launchpad/database/oauth.py	2010-09-20 16:45:03 +0000
+++ lib/canonical/launchpad/database/oauth.py	2010-10-05 11:52:47 +0000
@@ -15,6 +15,7 @@
     timedelta,
     )
 
+import re
 import pytz
 from sqlobject import (
     BoolCol,
@@ -93,6 +94,7 @@
 
     getStore = _get_store
 
+
 class OAuthConsumer(OAuthBase):
     """See `IOAuthConsumer`."""
     implements(IOAuthConsumer)
@@ -102,6 +104,51 @@
     key = StringCol(notNull=True)
     secret = StringCol(notNull=False, default='')
 
+    # This regular expression singles out a consumer key that
+    # represents any and all apps running on a specific computer
+    # (usually a desktop). For instance:
+    #
+    # System-wide: Ubuntu desktop (hostname1)
+    #  - An Ubuntu desktop called "hostname1"
+    # System-wide: Windows desktop (Computer Name)
+    #  - A Windows desktop called "Computer Name"
+    # System-wide: Mac OS desktop (hostname2)
+    #  - A Macintosh desktop called "hostname2"
+    # System-wide Android phone (Bob's Phone)
+    #  - An Android phone called "Bob's Phone"
+    integrated_desktop_re = re.compile("^System-wide: (.*) \(([^)]*)\)$")
+
+    def _integrated_desktop_match_group(self, position):
+        """Return information about a desktop integration token.
+
+        A convenience method that runs the desktop integration regular
+        expression against the consumer key.
+
+        :param position: The match group to return if the regular
+        expression matches.
+
+        :return: The value of one of the match groups, or None.
+        """
+        match = self.integrated_desktop_re.match(self.key)
+        if match is None:
+            return None
+        return match.groups()[position]
+
+    @property
+    def is_integrated_desktop(self):
+        """See `IOAuthConsumer`."""
+        return self.integrated_desktop_re.match(self.key) is not None
+
+    @property
+    def integrated_desktop_type(self):
+        """See `IOAuthConsumer`."""
+        return self._integrated_desktop_match_group(0)
+
+    @property
+    def integrated_desktop_name(self):
+        """See `IOAuthConsumer`."""
+        return self._integrated_desktop_match_group(1)
+
     def newRequestToken(self):
         """See `IOAuthConsumer`."""
         key, secret = create_token_key_and_secret(table=OAuthRequestToken)

=== modified file 'lib/canonical/launchpad/doc/oauth.txt'
--- lib/canonical/launchpad/doc/oauth.txt	2010-04-16 15:06:55 +0000
+++ lib/canonical/launchpad/doc/oauth.txt	2010-10-05 11:52:47 +0000
@@ -38,6 +38,45 @@
     ...
     AssertionError: ...
 
+Desktop consumers
+=================
+
+In a web context, each application is represented by a unique consumer
+key. But a typical user sitting at a typical desktop (or other
+personal computer), using multiple desktop applications that integrate
+with Launchpad, is represented by a single consumer key. The user's
+session as a whole is a single "consumer", and the consumer key is
+expected to contain structured information: the type of system
+(usually the operating system plus the word "desktop") and a string
+that the end-user would recognize as identifying their computer.
+
+    >>> desktop_key = consumer_set.new(
+    ...     "System-wide: Ubuntu desktop (hostname)")
+    >>> desktop_key.is_integrated_desktop
+    True
+    >>> print desktop_key.integrated_desktop_type
+    Ubuntu desktop
+    >>> print desktop_key.integrated_desktop_name
+    hostname
+
+    >>> desktop_key = consumer_set.new(
+    ...     "System-wide: Android phone (My Phone)")
+    >>> desktop_key.is_integrated_desktop
+    True
+    >>> print desktop_key.integrated_desktop_type
+    Android phone
+    >>> print desktop_key.integrated_desktop_name
+    My Phone
+
+A normal OAuth consumer does not have this information.
+
+    >>> ordinary_key = consumer_set.new("Not a desktop at all.")
+    >>> ordinary_key.is_integrated_desktop
+    False
+    >>> print ordinary_key.integrated_desktop_type
+    None
+    >>> print ordinary_key.integrated_desktop_name
+    None
 
 Request tokens
 ==============

=== modified file 'lib/canonical/launchpad/doc/webapp-authorization.txt'
--- lib/canonical/launchpad/doc/webapp-authorization.txt	2010-08-24 16:44:42 +0000
+++ lib/canonical/launchpad/doc/webapp-authorization.txt	2010-10-05 11:52:47 +0000
@@ -79,24 +79,16 @@
     >>> check_permission('launchpad.View', bug_1)
     False
 
-Now consider a principal authorized to create OAuth tokens. Whenever
-it's not creating OAuth tokens, it has a level of permission
-equivalent to READ_PUBLIC.
+A token used for desktop integration has a level of permission
+equivalent to WRITE_PUBLIC.
 
-    >>> principal.access_level = AccessLevel.GRANT_PERMISSIONS
+    >>> principal.access_level = AccessLevel.DESKTOP_INTEGRATION
     >>> setupInteraction(principal)
     >>> check_permission('launchpad.View', bug_1)
-    False
+    True
 
     >>> check_permission('launchpad.Edit', sample_person)
-    False
-
-This may seem useless from a security standpoint, since once a
-malicious client is authorized to create OAuth tokens, it can escalate
-its privileges at any time by creating a new token for itself. The
-security benefit is more subtle: by discouraging feature creep in
-clients that have this super-access level, we reduce the risk that a
-bug in a _trusted_ client will enable privilege escalation attacks.
+    True
 
 Users logged in through the web application have full access, which
 means they can read/change any object they have access to.

=== modified file 'lib/canonical/launchpad/interfaces/oauth.py'
--- lib/canonical/launchpad/interfaces/oauth.py	2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/interfaces/oauth.py	2010-10-05 11:52:47 +0000
@@ -64,6 +64,23 @@
         description=_('The secret which, if not empty, should be used by the '
                       'consumer to sign its requests.'))
 
+    is_integrated_desktop = Attribute(
+        """This attribute is true if the consumer corresponds to a
+        user account on a personal computer or similar device.""")
+
+    integrated_desktop_name = Attribute(
+        """If the consumer corresponds to a user account on a personal
+        computer or similar device, this is the self-reported name of
+        the computer. If the consumer is a specific web or desktop
+        application, this is None.""")
+
+    integrated_desktop_type = Attribute(
+        """If the consumer corresponds to a user account on a personal
+        computer or similar device, this is the self-reported type of
+        that computer (usually the operating system plus the word
+        "desktop"). If the consumer is a specific web or desktop
+        application, this is None.""")
+
     def newRequestToken():
         """Return a new `IOAuthRequestToken` with a random key and secret.
 

=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt	2010-09-20 16:45:03 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt	2010-10-05 11:52:47 +0000
@@ -45,7 +45,7 @@
     See all applications authorized to access Launchpad on your behalf.
 
 This page contains one submit button for each item of OAuthPermission,
-except for 'Grant Permissions', which must be specifically requested.
+except for 'Desktop Integration', which must be specifically requested.
 
     >>> browser.getControl('No Access')
     <SubmitControl...
@@ -58,6 +58,7 @@
     >>> browser.getControl('Change Anything')
     <SubmitControl...
 
+    # XXX FIXME
     >>> browser.getControl('Grant Permissions')
     Traceback (most recent call last):
     ...
@@ -74,11 +75,14 @@
 that isn't enough for the application. The user always has the option
 to deny permission altogether.
 
-    >>> def print_access_levels(allow_permission):
+    >>> def authorize_token_main_content(allow_permission):
     ...     browser.open(
     ...         "http://launchpad.dev/+authorize-token?%s&%s";
     ...         % (urlencode(params), allow_permission))
-    ...     main_content = find_tag_by_id(browser.contents, 'maincontent')
+    ...     return find_tag_by_id(browser.contents, 'maincontent')
+
+    >>> def print_access_levels(allow_permission):
+    ...     main_content = authorize_token_main_content(allow_permission)
     ...     actions = main_content.findAll('input', attrs={'type': 'submit'})
     ...     for action in actions:
     ...         print action['value']
@@ -89,28 +93,9 @@
     Change Non-Private Data
     Change Anything
 
-The only time the 'Grant Permissions' permission shows up in this list
-is if the client specifically requests it, and no other
-permission. (Also requesting UNAUTHORIZED is okay--it will show up
-anyway.)
-
-    >>> print_access_levels('allow_permission=GRANT_PERMISSIONS')
-    No Access
-    Grant Permissions
-
-    >>> print_access_levels(
-    ...     'allow_permission=GRANT_PERMISSIONS&allow_permission=UNAUTHORIZED')
-    No Access
-    Grant Permissions
-
-    >>> print_access_levels(
-    ...     'allow_permission=WRITE_PUBLIC&allow_permission=GRANT_PERMISSIONS')
-    No Access
-    Change Non-Private Data
-
 If an application doesn't specify any valid access levels, or only
 specifies the UNAUTHORIZED access level, Launchpad will show all the
-access levels, except for GRANT_PERMISSIONS.
+access levels, except for DESKTOP_INTEGRATION.
 
     >>> print_access_levels('')
     No Access
@@ -126,6 +111,20 @@
     Read Anything
     Change Anything
 
+An application may not request the DESKTOP_INTEGRATION access level
+unless its consumer key matches a certain pattern. (Successful desktop
+integration has its own section, below.)
+
+    >>> allow_permission = "allow_permission=DESKTOP_INTEGRATION"
+    >>> browser.open(
+    ...     "http://launchpad.dev/+authorize-token?%s&%s";
+    ...         % (urlencode(params), allow_permission))
+    Traceback (most recent call last):
+    ...
+    Unauthorized: Consumer "foobar123451432" asked for desktop
+    integration, but didn't say what kind of desktop it is, or name
+    the computer being integrated.
+
 An application may also specify a context, so that the access granted
 by the user is restricted to things related to that context.
 
@@ -263,3 +262,94 @@
     This request for accessing Launchpad on your behalf has been
     reviewed ... ago.
     See all applications authorized to access Launchpad on your behalf.
+
+Desktop integration
+===================
+
+The test case given above shows how to integrate a single application
+or website into Launchpad. But it's also possible to integrate an
+entire desktop environment into Launchpad.
+
+The desktop integration option is only available for OAuth consumers
+that say what kind of desktop they are (eg. Ubuntu) and give a name
+that a user can identify with their computer (eg. the hostname). Here,
+we'll create such a token.
+
+    >>> login('salgado@xxxxxxxxxx')
+    >>> desktop_key = "System-wide: Ubuntu desktop (mycomputer)"
+    >>> consumer = getUtility(IOAuthConsumerSet).new(desktop_key)
+    >>> token = consumer.newRequestToken()
+    >>> logout()
+
+When a desktop tries to integrate with Launchpad, the user gets a
+special warning about giving access to every program running on their
+desktop.
+
+    >>> params = dict(oauth_token=token.key)
+    >>> print extract_text(
+    ...     authorize_token_main_content(
+    ...         'allow_permission=DESKTOP_INTEGRATION'))
+    The Ubuntu desktop called mycomputer wants access to your
+    Launchpad account. If you allow the integration, all applications
+    running on mycomputer will have read-write access to your
+    Launchpad account, including to your private data.
+    If you're using a public computer, if mycomputer is not the
+    computer you're using right now, or if something just doesn't feel
+    right about this situation, you should click "No, thanks, I don't
+    trust 'mycomputer'", or close this window now. You can always try
+    again later.
+    Even if you decide to allow the integration, you can change your
+    mind later.
+    See all applications authorized to access Launchpad on your behalf.
+
+
+The only time the 'Desktop Integration' permission shows up in the
+list of permissions is if the client specifically requests it, and no
+other permission. (Also requesting UNAUTHORIZED is okay--it will show
+up anyway.)
+
+    >>> print_access_levels('allow_permission=DESKTOP_INTEGRATION')
+    Give all programs running on "mycomputer" access to my Launchpad account.
+    No, thanks, I don't trust "mycomputer".
+
+    >>> print_access_levels(
+    ...     'allow_permission=DESKTOP_INTEGRATION&allow_permission=UNAUTHORIZED')
+    Give all programs running on "mycomputer" access to my Launchpad account.
+    No, thanks, I don't trust "mycomputer".
+
+A desktop may not request a level of access other than
+DESKTOP_INTEGRATION, since the whole point is to have a permission
+level that specifically applies across the entire desktop.
+
+    >>> print_access_levels('allow_permission=WRITE_PRIVATE')
+    Traceback (most recent call last):
+    ...
+    Unauthorized: Desktop integration token requested a permission
+    ("Change Anything") not supported for desktop-wide use.
+
+    >>> print_access_levels(
+    ...     'allow_permission=WRITE_PUBLIC&allow_permission=DESKTOP_INTEGRATION')
+    Traceback (most recent call last):
+    ...
+    Unauthorized: Desktop integration token requested a permission
+    ("Change Non-Private Data") not supported for desktop-wide use.
+
+You can't specify a callback URL when authorizing a desktop-wide
+token, since callback URLs should only be used when integrating
+websites into Launchpad.
+
+    >>> params['oauth_callback'] = 'http://launchpad.dev/bzr'
+    >>> print_access_levels('allow_permission=DESKTOP_INTEGRATION')
+    Traceback (most recent call last):
+    ...
+    Unauthorized: A desktop integration may not specify an OAuth
+    callback URL.
+
+This is true even if the desktop token isn't asking for the
+DESKTOP_INTEGRATION permission.
+
+    >>> print_access_levels('allow_permission=WRITE_PRIVATE')
+    Traceback (most recent call last):
+    ...
+    Unauthorized: A desktop integration may not specify an OAuth
+    callback URL.

=== modified file 'lib/canonical/launchpad/templates/oauth-authorize.pt'
--- lib/canonical/launchpad/templates/oauth-authorize.pt	2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/oauth-authorize.pt	2010-10-05 11:52:47 +0000
@@ -21,28 +21,61 @@
       <tal:token-not-reviewed condition="not:token/is_reviewed">
         <div metal:use-macro="context/@@launchpad_form/form">
           <div metal:fill-slot="extra_top">
-            <p>The application identified as
-              <strong tal:content="token/consumer/key">consumer</strong>
-              wants to access
-              <tal:has-context condition="view/token_context">
-                things related to
-                <strong tal:content="view/token_context/title">Context</strong>
-                in
-              </tal:has-context>
-              Launchpad on your behalf. What level of access
-              do you want to grant?</p>
-
-            <table>
-              <tr tal:repeat="action view/visible_actions">
-                <td style="text-align: right">
-                  <tal:action replace="structure action/render" />
-                </td>
-                <td>
-                  <span class="lesser"
-                    tal:content="action/permission/description" />
-                </td>
-              </tr>
-            </table>
+
+           <tal:desktop-integration-token condition="token/consumer/is_integrated_desktop">
+             <p>The
+               <tal:desktop replace="structure
+               token/consumer/integrated_desktop_type" />
+             called
+             <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
+             wants access to your Launchpad account. If you allow the
+             integration, all applications running
+             on <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
+             will have read-write access to your Launchpad account,
+             including to your private data.</p>
+
+             <p>If you're using a public computer, if
+             <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
+             is not the computer you're using right now, or if
+             something just doesn't feel right about this situation,
+             you should click "No, thanks, I don't trust
+             '<tal:hostname replace="structure
+              token/consumer/integrated_desktop_name" />'",
+             or close this window now. You can always try
+             again later.</p>
+
+             <p>Even if you decide to allow the integration, you can
+             change your mind later.</p>
+           </tal:desktop-integration-token>
+
+           <tal:web-integration-token condition="not:token/consumer/is_integrated_desktop">
+             <p>The application identified as
+               <strong tal:content="token/consumer/key">consumer</strong>
+               wants to access
+               <tal:has-context condition="view/token_context">
+                 things related to
+                 <strong tal:content="view/token_context/title">Context</strong>
+                 in
+               </tal:has-context>
+               Launchpad on your behalf. What level of access
+               do you want to grant?</p>
+           </tal:web-integration-token>
+
+           <table>
+             <tr tal:repeat="action view/visible_actions">
+               <td style="text-align: right">
+                 <tal:action replace="structure action/render" />
+               </td>
+
+               <tal:web-integration-token
+                  condition="not:token/consumer/is_integrated_desktop">
+                 <td>
+                   <span class="lesser"
+                     tal:content="action/permission/description" />
+                 </td>
+               </tal:web-integration-token>
+             </tr>
+           </table>
           </div>
 
           <div metal:fill-slot="extra_bottom">

=== modified file 'lib/canonical/launchpad/webapp/authorization.py'
--- lib/canonical/launchpad/webapp/authorization.py	2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/authorization.py	2010-10-05 11:52:47 +0000
@@ -61,7 +61,8 @@
         lp_permission = getUtility(ILaunchpadPermission, permission)
         if lp_permission.access_level == "write":
             required_access_level = [
-                AccessLevel.WRITE_PUBLIC, AccessLevel.WRITE_PRIVATE]
+                AccessLevel.WRITE_PUBLIC, AccessLevel.WRITE_PRIVATE,
+                AccessLevel.DESKTOP_INTEGRATION]
             if access_level not in required_access_level:
                 return False
         elif lp_permission.access_level == "read":
@@ -80,7 +81,8 @@
         access to private objects, return False.  Return True otherwise.
         """
         private_access_levels = [
-            AccessLevel.READ_PRIVATE, AccessLevel.WRITE_PRIVATE]
+            AccessLevel.READ_PRIVATE, AccessLevel.WRITE_PRIVATE,
+            AccessLevel.DESKTOP_INTEGRATION]
         if access_level in private_access_levels:
             # The user has access to private objects. Return early,
             # before checking whether the object is private, since

=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
--- lib/canonical/launchpad/webapp/interfaces.py	2010-09-12 11:43:36 +0000
+++ lib/canonical/launchpad/webapp/interfaces.py	2010-10-05 11:52:47 +0000
@@ -527,14 +527,13 @@
         for reading and changing anything, including private data.
         """)
 
-    GRANT_PERMISSIONS = DBItem(60, """
-        Grant Permissions
+    DESKTOP_INTEGRATION = DBItem(60, """
+        Desktop Integration
 
-        The application will be able to grant access to your Launchpad
-        account to any other application. This is a very powerful
-        level of access. You should not grant this level of access to
-        any application except the official Launchpad credential
-        manager.
+        Every application running on your desktop will have read-write
+        access to your Launchpad account, including to your private
+        data. You should not allow this unless you trust the computer
+        you're using right now.
         """)
 
 class AccessLevel(DBEnumeratedType):