← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~leonardr/launchpad/temporary-integration into lp:launchpad

 

Leonard Richardson has proposed merging lp:~leonardr/launchpad/temporary-integration into lp:launchpad with lp:~leonardr/launchpad/oauth-doctest-to-unit-test as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


For the first time, this branch makes it possible to create an OAuth access token with an expiration date. The feature is only available for a "desktop integration" type token, and it's intended to allow the end-user to try out desktop integration or temporarily enable access to their Launchpad account on someone else's computer.

When you go to authorize a "desktop integration" OAuth request token, you'll be presented with the normal "allow" and "deny" actions, but you'll also have "one hour", "one day" and "one week" actions. These actions are the same as the "allow" action, but they have a "duration" set, which is transformed into an expiration date when that action is processed.

The expiration date is stored in IOAuthRequestToken.date_expires, and copied over when the token is approved.

Note that it's possible to have more than one "desktop integration" token for a given computer name. In real life, this will only happen if you gave more than one computer the same name. (This does happen, though; someone might have two computers both called "ubuntu" or "localhost".)

-- 
https://code.launchpad.net/~leonardr/launchpad/temporary-integration/+merge/38836
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~leonardr/launchpad/temporary-integration into lp:launchpad.
=== modified file 'lib/canonical/launchpad/browser/oauth.py'
--- lib/canonical/launchpad/browser/oauth.py	2010-10-19 14:26:14 +0000
+++ lib/canonical/launchpad/browser/oauth.py	2010-10-19 14:26:15 +0000
@@ -9,12 +9,19 @@
     'OAuthTokenAuthorizedView',
     'lookup_oauth_context']
 
+from datetime import (
+    datetime,
+    timedelta,
+    )
+
 from lazr.restful import HTTPResource
+import pytz
 import simplejson
 from zope.component import getUtility
 from zope.formlib.form import (
     Action,
     Actions,
+    expandPrefix,
     )
 from zope.security.interfaces import Unauthorized
 
@@ -63,7 +70,7 @@
         return simplejson.dumps(structure)
 
 
-class OAuthRequestTokenView(LaunchpadView, JSONTokenMixin):
+class OAuthRequestTokenView(LaunchpadFormView, JSONTokenMixin):
     """Where consumers can ask for a request token."""
 
     def __call__(self):
@@ -107,7 +114,21 @@
 
 def token_review_success(form, action, data):
     """The success callback for a button to approve a token."""
-    form.reviewToken(action.permission)
+    form.reviewToken(action.permission, action.duration)
+
+
+class TemporaryIntegrations:
+    """Contains duration constants for temporary integrations."""
+
+    HOUR = "hour"
+    DAY = "day"
+    WEEK = "week"
+
+    DURATION = {
+        HOUR : 60 * 60,
+        DAY : 60 * 60 * 24,
+        WEEK : 60 * 60 * 24 * 7
+        }
 
 
 def create_oauth_permission_actions():
@@ -115,19 +136,37 @@
 
     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.
+    set of actions, omitting special actions like the
+    DESKTOP_INTEGRATION ones.
     """
     all_actions = Actions()
     ordinary_actions = Actions()
+    desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
     for permission in OAuthPermission.items:
         action = Action(
             permission.title, name=permission.name,
             success=token_review_success,
             condition=token_exists_and_is_not_reviewed)
         action.permission = permission
+        action.duration = None
         all_actions.append(action)
-        if permission != OAuthPermission.DESKTOP_INTEGRATION:
+        if permission != desktop_permission:
             ordinary_actions.append(action)
+
+    # Add special actions for the time-limited DESKTOP_INTEGRATION
+    # tokens.
+    for duration in (
+        TemporaryIntegrations.HOUR, TemporaryIntegrations.DAY,
+        TemporaryIntegrations.WEEK):
+        action = Action(
+            ("I'd like to try the integration for one %s." % duration),
+            name=expandPrefix(desktop_permission.name) + duration,
+            success=token_review_success,
+            condition=token_exists_and_is_not_reviewed)
+        action.permission = desktop_permission
+        action.duration = duration
+        all_actions.append(action)
+
     return all_actions, ordinary_actions
 
 
@@ -161,7 +200,8 @@
         used by normal applications.
         """
 
-        allowed_permissions = self.request.form_ng.getAll('allow_permission')
+        allowed_permissions = set(
+            self.request.form_ng.getAll('allow_permission'))
         if len(allowed_permissions) == 0:
             return self.actions_excluding_special_permissions
         actions = Actions()
@@ -174,9 +214,10 @@
 
         # DESKTOP_INTEGRATION cannot be requested as one of several
         # options--it must be the only option (other than
-        # UNAUTHORIZED). If DESKTOP_INTEGRATION is one of several
-        # options, remove it from the list.
+        # UNAUTHORIZED). If there is any item in the list that doesn't
+        # use DESKTOP_INTEGRATION, remove it from the list.
         desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
+
         if (desktop_permission.name in allowed_permissions
             and len(allowed_permissions) > 1):
             allowed_permissions.remove(desktop_permission.name)
@@ -192,10 +233,14 @@
                      "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.
+            # We're going for desktop integration. There are four
+            # possibilities: "allow permanently", "allow for one
+            # hour", "allow for one day", "allow for one week", and
+            # "deny". We'll customize the "allow permanently" and
+            # "deny" message using the hostname provided by the
+            # desktop. We'll use the existing Action objects for the
+            # "temporary integration" actions, without customizing
+            # their messages.
             #
             # Since self.actions is a descriptor that returns copies
             # of Action objects, we can modify the actions we get
@@ -203,22 +248,27 @@
             # else.
             desktop_name = self.token.consumer.integrated_desktop_name
             label = (
-                'Give all programs running on "%s" access '
-                'to my Launchpad account.')
+                'Permanently integrate "%s" into 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.
+            # Bring in all of the temporary integration actions.
+            for action in self.actions:
+                if (action.permission == desktop_permission
+                    and action.name != desktop_permission.name):
+                    actions.append(action)
+
+            # Fionally, customize the "deny" message.
             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:
@@ -244,7 +294,6 @@
         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):
@@ -295,8 +344,16 @@
             raise UnexpectedFormData("Unknown context.")
         self.token_context = context
 
-    def reviewToken(self, permission):
-        self.token.review(self.user, permission, self.token_context)
+    def reviewToken(self, permission, duration):
+        duration_seconds = TemporaryIntegrations.DURATION.get(duration)
+        if duration_seconds is not None:
+            duration_delta = timedelta(seconds=duration_seconds)
+            expiration_date = (
+                datetime.now(pytz.timezone('UTC')) + duration_delta)
+        else:
+            expiration_date = None
+        self.token.review(self.user, permission, self.token_context,
+                          date_expires=expiration_date)
         callback = self.request.form.get('oauth_callback')
         if callback:
             self.next_url = callback
@@ -304,6 +361,7 @@
             self.next_url = (
                 '+token-authorized?oauth_token=%s' % self.token.key)
 
+
 def lookup_oauth_context(context):
     """Transform an OAuth context string into a context object.
 

=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt	2010-10-19 14:26:14 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt	2010-10-19 14:26:15 +0000
@@ -4,14 +4,21 @@
 Launchpad's +authorize-token page in order for the user to authenticate
 and authorize or not the consumer to act on his behalf.
 
+    >>> def request_token_for(consumer):
+    ...     """Helper method to create a request token."""
+    ...     login('salgado@xxxxxxxxxx')
+    ...     token = consumer.newRequestToken()
+    ...     logout()
+    ...     return token
+
     # Create a new request token.
     >>> from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
     >>> from zope.component import getUtility
     >>> from canonical.launchpad.ftests import ANONYMOUS, login, logout
     >>> login('salgado@xxxxxxxxxx')
     >>> consumer = getUtility(IOAuthConsumerSet).getByKey('foobar123451432')
-    >>> token = consumer.newRequestToken()
     >>> logout()
+    >>> token = request_token_for(consumer)
 
 According to the OAuth Core 1.0 spec, the request to the service
 provider's user authorization URL (+authorize-token in our case) must
@@ -71,10 +78,13 @@
 that isn't enough for the application. The user always has the option
 to deny permission altogether.
 
-    >>> def authorize_token_main_content(allow_permission):
+    >>> def authorize_token_browser(allow_permission):
     ...     browser.open(
     ...         "http://launchpad.dev/+authorize-token?%s&%s";
     ...         % (urlencode(params), allow_permission))
+
+    >>> def authorize_token_main_content(allow_permission):
+    ...     authorize_token_browser(allow_permission)
     ...     return find_tag_by_id(browser.contents, 'maincontent')
 
     >>> def print_access_levels_for(allow_permission):
@@ -196,10 +206,7 @@
 +authorized-token.
 
     # Create a new (unreviewed) token.
-    >>> logout()
-    >>> login('salgado@xxxxxxxxxx')
-    >>> token = consumer.newRequestToken()
-    >>> logout()
+    >>> token = request_token_for(consumer)
 
     >>> params = dict(oauth_token=token.key)
     >>> browser.open(
@@ -268,14 +275,15 @@
 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.
+we'll create such a consumer, and then a request token for that consumer.
 
-    >>> login('salgado@xxxxxxxxxx')
-    >>> desktop_key = "System-wide: Ubuntu desktop (mycomputer)"
-    >>> consumer = getUtility(IOAuthConsumerSet).new(desktop_key)
-    >>> token = consumer.newRequestToken()
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> consumer = factory.makeOAuthConsumer(
+    ...     "System-wide: Ubuntu desktop (mycomputer)")
     >>> logout()
 
+    >>> token = request_token_for(consumer)
+
 When a desktop tries to integrate with Launchpad, the user gets a
 special warning about giving access to every program running on their
 desktop.
@@ -298,19 +306,25 @@
     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_for('allow_permission=DESKTOP_INTEGRATION')
-    Give all programs running on "mycomputer" access to my Launchpad account.
+    >>> allow_desktop = 'allow_permission=DESKTOP_INTEGRATION'
+    >>> print_access_levels_for(allow_desktop)
+    Permanently integrate "mycomputer" into my Launchpad account.
+    I'd like to try the integration for one hour.
+    I'd like to try the integration for one day.
+    I'd like to try the integration for one week.
     No, thanks, I don't trust "mycomputer".
 
     >>> print_access_levels_for(
     ...     'allow_permission=DESKTOP_INTEGRATION&allow_permission=UNAUTHORIZED')
-    Give all programs running on "mycomputer" access to my Launchpad account.
+    Permanently integrate "mycomputer" into my Launchpad account.
+    I'd like to try the integration for one hour.
+    I'd like to try the integration for one day.
+    I'd like to try the integration for one week.
     No, thanks, I don't trust "mycomputer".
 
 A desktop may not request a level of access other than
@@ -324,7 +338,7 @@
     ("Change Anything") not supported for desktop-wide use.
 
     >>> print_access_levels_for(
-    ...     'allow_permission=WRITE_PUBLIC&allow_permission=DESKTOP_INTEGRATION')
+    ...     'allow_permission=WRITE_PUBLIC&' + allow_desktop)
     Traceback (most recent call last):
     ...
     Unauthorized: Desktop integration token requested a permission
@@ -335,7 +349,7 @@
 websites into Launchpad.
 
     >>> params['oauth_callback'] = 'http://launchpad.dev/bzr'
-    >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
+    >>> print_access_levels_for(allow_desktop)
     Traceback (most recent call last):
     ...
     Unauthorized: A desktop integration may not specify an OAuth
@@ -349,3 +363,130 @@
     ...
     Unauthorized: A desktop integration may not specify an OAuth
     callback URL.
+
+    >>> del params['oauth_callback']
+
+Accepting full integration
+--------------------------
+
+Now let's create a helper function to go through the entire desktop
+integration process, given the name of the desktop and the level of
+integration desired.
+
+    >>> def integrate_desktop(button_to_click):
+    ...     """Authorize (or don't) a desktop integration request token.
+    ...     The token is authorized for the computer "mycomputer".
+    ...
+    ...     :return: the IOAuthRequestToken, possibly authorized.
+    ...     """
+    ...     token = request_token_for(consumer)
+    ...     params['oauth_token'] = token.key
+    ...     authorize_token_browser(allow_desktop)
+    ...     button = browser.getControl(button_to_click)
+    ...     button.click()
+    ...     return token
+
+If the client chooses a permanent desktop integration, the request
+token is approved and has no expiration date.
+
+    >>> token = integrate_desktop(
+    ...     'Permanently integrate "mycomputer" into my Launchpad account.')
+    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    Almost finished ...
+    The Ubuntu desktop called mycomputer now has access to your
+    Launchpad account. Within a few seconds, you should be able to
+    start using its Launchpad integration features.
+
+    >>> print token.is_reviewed
+    True
+    >>> print token.permission.name
+    DESKTOP_INTEGRATION
+    >>> print token.date_expires
+    None
+
+Accepting time-limited integration
+----------------------------------
+
+If you allow integration for a limited time, the request token is
+reviewed and given an expiration date. Here, we authorize a token for
+one hour.
+
+    >>> token = integrate_desktop(
+    ...     "I'd like to try the integration for one hour.")
+
+    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    Almost finished ...
+    The Ubuntu desktop called mycomputer now has access to your
+    Launchpad account. Within a few seconds, you should be able to
+    start using its Launchpad integration features.
+    The integration you just authorized will expire in 59 minutes. At
+    that time, you'll have to re-authorize the Ubuntu desktop called
+    mycomputer, if you want to keep using its Launchpad integration
+    features.
+
+    >>> print token.is_reviewed
+    True
+    >>> print token.permission.name
+    DESKTOP_INTEGRATION
+    >>> token.date_expires is None
+    False
+
+Note that a single computer (in this case "mycomputer") may have more
+than one desktop integration token. This is because there's no way to
+know that a user hasn't given more than one computer the same name
+(eg. "ubuntu" or "localhost"). The assignment of computer names to
+integration tokens is a useful convention, not something we try to
+enforce.
+
+Here we authorize a token for one day.
+
+    >>> token = integrate_desktop(
+    ...     "I'd like to try the integration for one day.")
+
+    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    Almost finished ...
+    The integration you just authorized will expire in 23 hours.
+    ...
+
+    >>> print token.is_reviewed
+    True
+    >>> token.date_expires is None
+    False
+
+Here, we authorize a token for a week. The expiration time is given as
+a date.
+
+    >>> token = integrate_desktop(
+    ...     "I'd like to try the integration for one week.")
+
+    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    Almost finished ...
+    The integration you just authorized will expire 2...
+    ...
+
+    >>> print token.is_reviewed
+    True
+    >>> print token.permission.name
+    DESKTOP_INTEGRATION
+    >>> token.date_expires is None
+    False
+
+Declining integration
+---------------------
+
+If the client declines integration, the request token is reviewed but
+cannot be exchanged for an access token.
+
+    >>> token = integrate_desktop(
+    ...     """No, thanks, I don't trust "mycomputer".""")
+
+    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    You decided against desktop integration
+    You decided not to give the Ubuntu desktop called mycomputer
+    access to your Launchpad account. You can always change your mind
+    later.
+
+    >>> print token.is_reviewed
+    True
+    >>> print token.permission.name
+    UNAUTHORIZED

=== modified file 'lib/canonical/launchpad/templates/oauth-authorize.pt'
--- lib/canonical/launchpad/templates/oauth-authorize.pt	2010-10-19 14:26:14 +0000
+++ lib/canonical/launchpad/templates/oauth-authorize.pt	2010-10-19 14:26:15 +0000
@@ -73,6 +73,8 @@
              <tr tal:repeat="action view/visible_actions">
                <td style="text-align: right">
                  <tal:action replace="structure action/render" />
+                 <input type="hidden" name="allow_permission"
+                  tal:attributes="value action/permission/name" />
                </td>
 
                <tal:web-integration-token
@@ -90,6 +92,7 @@
             <input type="hidden" name="oauth_token"
                    tal:condition="request/form/oauth_token|nothing"
                    tal:attributes="value request/form/oauth_token" />
+
             <input type="hidden" name="oauth_callback"
                    tal:condition="request/form/oauth_callback|nothing"
                    tal:attributes="value request/form/oauth_callback" />

=== modified file 'lib/canonical/launchpad/templates/token-authorized.pt'
--- lib/canonical/launchpad/templates/token-authorized.pt	2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/token-authorized.pt	2010-10-19 14:26:15 +0000
@@ -8,25 +8,71 @@
 >
 <body>
   <div class="top-portlet" metal:fill-slot="main">
-    <tal:unauthorized condition="view/token/permission/enumvalue:UNAUTHORIZED">
-      <h1>Access not granted to application</h1>
-      <p>
-        The application identified as
-        <strong tal:content="view/token/consumer/key">key</strong> has not
-        been given access to your protected resources on Launchpad.
-      </p>
-    </tal:unauthorized>
-
-    <tal:authorized condition="not:view/token/permission/enumvalue:UNAUTHORIZED">
-      <h1>Almost finished ...</h1>
-      <p>
+
+    <tal:desktop condition="view/token/consumer/is_integrated_desktop">
+
+      <tal:unauthorized condition="view/token/permission/enumvalue:UNAUTHORIZED">
+        <h1>You decided against desktop integration</h1>
+
+        <p>
+          You decided not to give the
+          <tal:desktop replace="structure view/token/consumer/integrated_desktop_type" />
+          called
+          <strong tal:content="view/token/consumer/integrated_desktop_name">hostname</strong>
+          access to your Launchpad account. You can always change your
+          mind later.
+        </p>
+      </tal:unauthorized>
+
+      <tal:authorized condition="not:view/token/permission/enumvalue:UNAUTHORIZED">
+        <h1>Almost finished ...</h1>
+
+        <p>The
+          <tal:desktop replace="structure view/token/consumer/integrated_desktop_type" />
+          called
+          <strong tal:content="view/token/consumer/integrated_desktop_name">hostname</strong>
+          now has access to your Launchpad account. Within a few
+          seconds, you should be able to start using its Launchpad
+          integration features.</p>
+
+        <p tal:condition="view/token/date_expires">
+          The integration you just authorized will expire
+          <tal:date
+           replace="structure view/token/date_expires/fmt:approximatedate" />.
+          At that time, you'll have to re-authorize the
+          <tal:desktop replace="structure view/token/consumer/integrated_desktop_type" />
+          called
+          <strong tal:content="view/token/consumer/integrated_desktop_name">hostname</strong>,
+          if you want to keep using its Launchpad integration features.
+
+        </p>
+
+      </tal:authorized>
+    </tal:desktop>
+
+    <tal:application condition="not:view/token/consumer/is_integrated_desktop">
+
+     <tal:unauthorized condition="view/token/permission/enumvalue:UNAUTHORIZED">
+       <h1>Access not granted to application</h1>
+       <p>
+         The application identified as
+         <strong tal:content="view/token/consumer/key">key</strong> has not
+         been given access to your protected resources on Launchpad.
+       </p>
+     </tal:unauthorized>
+
+     <tal:authorized condition="not:view/token/permission/enumvalue:UNAUTHORIZED">
+       <h1>Almost finished ...</h1>
+       <p>
         To finish authorizing the application identified as
         <strong tal:content="view/token/consumer/key">key</strong> to access
         Launchpad on your behalf you should go back to the application window
         in which you started the process and inform it that you have done your
         part of the process.
-      </p>
-    </tal:authorized>
+       </p>
+     </tal:authorized>
+
+    </tal:application>
   </div>
 
 </body>