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
=== 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-14 17:12:48 +0000
@@ -16,6 +16,7 @@
16 Action,16 Action,
17 Actions,17 Actions,
18 )18 )
19from zope.security.interfaces import Unauthorized
1920
20from canonical.launchpad.interfaces.oauth import (21from canonical.launchpad.interfaces.oauth import (
21 IOAuthConsumerSet,22 IOAuthConsumerSet,
@@ -88,11 +89,13 @@
8889
89 token = consumer.newRequestToken()90 token = consumer.newRequestToken()
90 if self.request.headers.get('Accept') == HTTPResource.JSON_TYPE:91 if self.request.headers.get('Accept') == HTTPResource.JSON_TYPE:
91 # Don't show the client the GRANT_PERMISSIONS access92 # Don't show the client the DESKTOP_INTEGRATION access
92 # level. If they have a legitimate need to use it, they'll93 # level. If they have a legitimate need to use it, they'll
93 # already know about it.94 # already know about it.
94 permissions = [permission for permission in OAuthPermission.items95 permissions = [
95 if permission != OAuthPermission.GRANT_PERMISSIONS]96 permission for permission in OAuthPermission.items
97 if (permission != OAuthPermission.DESKTOP_INTEGRATION)
98 ]
96 return self.getJSONRepresentation(99 return self.getJSONRepresentation(
97 permissions, token, include_secret=True)100 permissions, token, include_secret=True)
98 return u'oauth_token=%s&oauth_token_secret=%s' % (101 return u'oauth_token=%s&oauth_token_secret=%s' % (
@@ -102,26 +105,36 @@
102 return form.token is not None and not form.token.is_reviewed105 return form.token is not None and not form.token.is_reviewed
103106
104107
108def token_review_success(form, action, data):
109 """The success callback for a button to approve a token."""
110 form.reviewToken(action.permission)
111
112
105def create_oauth_permission_actions():113def create_oauth_permission_actions():
106 """Return a list of `Action`s for each possible `OAuthPermission`."""114 """Return two `Actions` objects containing each possible `OAuthPermission`.
107 actions = Actions()115
108 actions_excluding_grant_permissions = Actions()116 The first `Actions` object contains every action supported by the
109 def success(form, action, data):117 OAuthAuthorizeTokenView. The second list contains a good default
110 form.reviewToken(action.permission)118 set of actions, omitting special permissions like DESKTOP_INTEGRATION.
119 """
120 all_actions = Actions()
121 ordinary_actions = Actions()
111 for permission in OAuthPermission.items:122 for permission in OAuthPermission.items:
112 action = Action(123 action = Action(
113 permission.title, name=permission.name, success=success,124 permission.title, name=permission.name,
125 success=token_review_success,
114 condition=token_exists_and_is_not_reviewed)126 condition=token_exists_and_is_not_reviewed)
115 action.permission = permission127 action.permission = permission
116 actions.append(action)128 all_actions.append(action)
117 if permission != OAuthPermission.GRANT_PERMISSIONS:129 if permission != OAuthPermission.DESKTOP_INTEGRATION:
118 actions_excluding_grant_permissions.append(action)130 ordinary_actions.append(action)
119 return actions, actions_excluding_grant_permissions131 return all_actions, ordinary_actions
132
120133
121class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):134class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
122 """Where users authorize consumers to access Launchpad on their behalf."""135 """Where users authorize consumers to access Launchpad on their behalf."""
123136
124 actions, actions_excluding_grant_permissions = (137 actions, actions_excluding_special_permissions = (
125 create_oauth_permission_actions())138 create_oauth_permission_actions())
126 label = "Authorize application to access Launchpad on your behalf"139 label = "Authorize application to access Launchpad on your behalf"
127 schema = IOAuthRequestToken140 schema = IOAuthRequestToken
@@ -130,7 +143,7 @@
130143
131 @property144 @property
132 def visible_actions(self):145 def visible_actions(self):
133 """Restrict the actions to the subset the client can make use of.146 """Restrict the actions to a subset to be presented to the client.
134147
135 Not all client programs can function with all levels of148 Not all client programs can function with all levels of
136 access. For instance, a client that needs to modify the149 access. For instance, a client that needs to modify the
@@ -150,7 +163,7 @@
150163
151 allowed_permissions = self.request.form_ng.getAll('allow_permission')164 allowed_permissions = self.request.form_ng.getAll('allow_permission')
152 if len(allowed_permissions) == 0:165 if len(allowed_permissions) == 0:
153 return self.actions_excluding_grant_permissions166 return self.actions_excluding_special_permissions
154 actions = Actions()167 actions = Actions()
155168
156 # UNAUTHORIZED is always one of the options. If the client169 # UNAUTHORIZED is always one of the options. If the client
@@ -159,18 +172,59 @@
159 if OAuthPermission.UNAUTHORIZED.name in allowed_permissions:172 if OAuthPermission.UNAUTHORIZED.name in allowed_permissions:
160 allowed_permissions.remove(OAuthPermission.UNAUTHORIZED.name)173 allowed_permissions.remove(OAuthPermission.UNAUTHORIZED.name)
161174
162 # GRANT_PERMISSIONS cannot be requested as one of several175 # DESKTOP_INTEGRATION cannot be requested as one of several
163 # options--it must be the only option (other than176 # options--it must be the only option (other than
164 # UNAUTHORIZED). If GRANT_PERMISSIONS is one of several177 # UNAUTHORIZED). If DESKTOP_INTEGRATION is one of several
165 # options, remove it from the list.178 # options, remove it from the list.
166 if (OAuthPermission.GRANT_PERMISSIONS.name in allowed_permissions179 desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
180 if (desktop_permission.name in allowed_permissions
167 and len(allowed_permissions) > 1):181 and len(allowed_permissions) > 1):
168 allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name)182 allowed_permissions.remove(desktop_permission.name)
169183
170 for action in self.actions:184 if desktop_permission.name in allowed_permissions:
171 if (action.permission.name in allowed_permissions185 if not self.token.consumer.is_integrated_desktop:
172 or action.permission is OAuthPermission.UNAUTHORIZED):186 # Consumers may only ask for desktop integration if
173 actions.append(action)187 # they give a desktop type (eg. "Ubuntu") and a
188 # user-recognizable desktop name (eg. the hostname).
189 raise Unauthorized(
190 ('Consumer "%s" asked for desktop integration, '
191 "but didn't say what kind of desktop it is, or name "
192 "the computer being integrated."
193 % self.token.consumer.key))
194
195 # We're going for desktop integration. The only two
196 # possibilities are "allow" and "deny". We'll customize
197 # the "allow" message using the hostname provided by the
198 # desktop.
199 #
200 # Since self.actions is a descriptor that returns copies
201 # of Action objects, we can modify the actions we get
202 # in-place without ruining the Action objects for everyone
203 # else.
204 desktop_name = self.token.consumer.integrated_desktop_name
205 label = (
206 'Give all programs running on "%s" access '
207 'to my Launchpad account.')
208 allow_action = [
209 action for action in self.actions
210 if action.name == desktop_permission.name][0]
211 allow_action.label = label % desktop_name
212 actions.append(allow_action)
213
214 # We'll customize the "deny" message as well.
215 label = "No, thanks, I don't trust "%s"."
216 deny_action = [
217 action for action in self.actions
218 if action.name == OAuthPermission.UNAUTHORIZED.name][0]
219 deny_action.label = label % desktop_name
220 actions.append(deny_action)
221
222 else:
223 # We're going for web-based integration.
224 for action in self.actions_excluding_special_permissions:
225 if (action.permission.name in allowed_permissions
226 or action.permission is OAuthPermission.UNAUTHORIZED):
227 actions.append(action)
174228
175 if len(list(actions)) == 1:229 if len(list(actions)) == 1:
176 # The only visible action is UNAUTHORIZED. That means the230 # The only visible action is UNAUTHORIZED. That means the
@@ -179,8 +233,8 @@
179 # UNAUTHORIZED). Rather than present the end-user with an233 # UNAUTHORIZED). Rather than present the end-user with an
180 # impossible situation where their only option is to deny234 # impossible situation where their only option is to deny
181 # access, we'll present the full range of actions (except235 # access, we'll present the full range of actions (except
182 # for GRANT_PERMISSIONS).236 # for special permissions like DESKTOP_INTEGRATION).
183 return self.actions_excluding_grant_permissions237 return self.actions_excluding_special_permissions
184 return actions238 return actions
185239
186 def initialize(self):240 def initialize(self):
@@ -189,6 +243,31 @@
189 key = form.get('oauth_token')243 key = form.get('oauth_token')
190 if key:244 if key:
191 self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)245 self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)
246
247
248 callback = self.request.form.get('oauth_callback')
249 if (self.token is not None
250 and self.token.consumer.is_integrated_desktop):
251 # Nip problems in the bud by appling special rules about
252 # what desktop integrations are allowed to do.
253 if callback is not None:
254 # A desktop integration is not allowed to specify a callback.
255 raise Unauthorized(
256 "A desktop integration may not specify an "
257 "OAuth callback URL.")
258 # A desktop integration token can only have one of two
259 # permission levels: "Desktop Integration" and
260 # "Unauthorized". It shouldn't even be able to ask for any
261 # other level.
262 for action in self.visible_actions:
263 if action.permission not in (
264 OAuthPermission.DESKTOP_INTEGRATION,
265 OAuthPermission.UNAUTHORIZED):
266 raise Unauthorized(
267 ("Desktop integration token requested a permission "
268 '("%s") not supported for desktop-wide use.')
269 % action.label)
270
192 super(OAuthAuthorizeTokenView, self).initialize()271 super(OAuthAuthorizeTokenView, self).initialize()
193272
194 def render(self):273 def render(self):
195274
=== modified file 'lib/canonical/launchpad/database/oauth.py'
--- lib/canonical/launchpad/database/oauth.py 2010-10-03 15:30:06 +0000
+++ lib/canonical/launchpad/database/oauth.py 2010-10-14 17:12:48 +0000
@@ -15,6 +15,7 @@
15 timedelta,15 timedelta,
16 )16 )
1717
18import re
18import pytz19import pytz
19from sqlobject import (20from sqlobject import (
20 BoolCol,21 BoolCol,
@@ -93,6 +94,7 @@
9394
94 getStore = _get_store95 getStore = _get_store
9596
97
96class OAuthConsumer(OAuthBase):98class OAuthConsumer(OAuthBase):
97 """See `IOAuthConsumer`."""99 """See `IOAuthConsumer`."""
98 implements(IOAuthConsumer)100 implements(IOAuthConsumer)
@@ -102,6 +104,51 @@
102 key = StringCol(notNull=True)104 key = StringCol(notNull=True)
103 secret = StringCol(notNull=False, default='')105 secret = StringCol(notNull=False, default='')
104106
107 # This regular expression singles out a consumer key that
108 # represents any and all apps running on a specific computer
109 # (usually a desktop). For instance:
110 #
111 # System-wide: Ubuntu desktop (hostname1)
112 # - An Ubuntu desktop called "hostname1"
113 # System-wide: Windows desktop (Computer Name)
114 # - A Windows desktop called "Computer Name"
115 # System-wide: Mac OS desktop (hostname2)
116 # - A Macintosh desktop called "hostname2"
117 # System-wide Android phone (Bob's Phone)
118 # - An Android phone called "Bob's Phone"
119 integrated_desktop_re = re.compile("^System-wide: (.*) \(([^)]*)\)$")
120
121 def _integrated_desktop_match_group(self, position):
122 """Return information about a desktop integration token.
123
124 A convenience method that runs the desktop integration regular
125 expression against the consumer key.
126
127 :param position: The match group to return if the regular
128 expression matches.
129
130 :return: The value of one of the match groups, or None.
131 """
132 match = self.integrated_desktop_re.match(self.key)
133 if match is None:
134 return None
135 return match.groups()[position]
136
137 @property
138 def is_integrated_desktop(self):
139 """See `IOAuthConsumer`."""
140 return self.integrated_desktop_re.match(self.key) is not None
141
142 @property
143 def integrated_desktop_type(self):
144 """See `IOAuthConsumer`."""
145 return self._integrated_desktop_match_group(0)
146
147 @property
148 def integrated_desktop_name(self):
149 """See `IOAuthConsumer`."""
150 return self._integrated_desktop_match_group(1)
151
105 def newRequestToken(self):152 def newRequestToken(self):
106 """See `IOAuthConsumer`."""153 """See `IOAuthConsumer`."""
107 key, secret = create_token_key_and_secret(table=OAuthRequestToken)154 key, secret = create_token_key_and_secret(table=OAuthRequestToken)
108155
=== modified file 'lib/canonical/launchpad/doc/oauth.txt'
--- lib/canonical/launchpad/doc/oauth.txt 2010-10-03 15:30:06 +0000
+++ lib/canonical/launchpad/doc/oauth.txt 2010-10-14 17:12:48 +0000
@@ -38,6 +38,45 @@
38 ...38 ...
39 AssertionError: ...39 AssertionError: ...
4040
41Desktop consumers
42=================
43
44In a web context, each application is represented by a unique consumer
45key. But a typical user sitting at a typical desktop (or other
46personal computer), using multiple desktop applications that integrate
47with Launchpad, is represented by a single consumer key. The user's
48session as a whole is a single "consumer", and the consumer key is
49expected to contain structured information: the type of system
50(usually the operating system plus the word "desktop") and a string
51that the end-user would recognize as identifying their computer.
52
53 >>> desktop_key = consumer_set.new(
54 ... "System-wide: Ubuntu desktop (hostname)")
55 >>> desktop_key.is_integrated_desktop
56 True
57 >>> print desktop_key.integrated_desktop_type
58 Ubuntu desktop
59 >>> print desktop_key.integrated_desktop_name
60 hostname
61
62 >>> desktop_key = consumer_set.new(
63 ... "System-wide: Android phone (My Phone)")
64 >>> desktop_key.is_integrated_desktop
65 True
66 >>> print desktop_key.integrated_desktop_type
67 Android phone
68 >>> print desktop_key.integrated_desktop_name
69 My Phone
70
71A normal OAuth consumer does not have this information.
72
73 >>> ordinary_key = consumer_set.new("Not a desktop at all.")
74 >>> ordinary_key.is_integrated_desktop
75 False
76 >>> print ordinary_key.integrated_desktop_type
77 None
78 >>> print ordinary_key.integrated_desktop_name
79 None
4180
42Request tokens81Request tokens
43==============82==============
4483
=== modified file 'lib/canonical/launchpad/doc/webapp-authorization.txt'
--- lib/canonical/launchpad/doc/webapp-authorization.txt 2010-10-03 15:30:06 +0000
+++ lib/canonical/launchpad/doc/webapp-authorization.txt 2010-10-14 17:12:48 +0000
@@ -79,24 +79,16 @@
79 >>> check_permission('launchpad.View', bug_1)79 >>> check_permission('launchpad.View', bug_1)
80 False80 False
8181
82Now consider a principal authorized to create OAuth tokens. Whenever82A token used for desktop integration has a level of permission
83it's not creating OAuth tokens, it has a level of permission83equivalent to WRITE_PUBLIC.
84equivalent to READ_PUBLIC.
8584
86 >>> principal.access_level = AccessLevel.GRANT_PERMISSIONS85 >>> principal.access_level = AccessLevel.DESKTOP_INTEGRATION
87 >>> setupInteraction(principal)86 >>> setupInteraction(principal)
88 >>> check_permission('launchpad.View', bug_1)87 >>> check_permission('launchpad.View', bug_1)
89 False88 True
9089
91 >>> check_permission('launchpad.Edit', sample_person)90 >>> check_permission('launchpad.Edit', sample_person)
92 False91 True
93
94This may seem useless from a security standpoint, since once a
95malicious client is authorized to create OAuth tokens, it can escalate
96its privileges at any time by creating a new token for itself. The
97security benefit is more subtle: by discouraging feature creep in
98clients that have this super-access level, we reduce the risk that a
99bug in a _trusted_ client will enable privilege escalation attacks.
10092
101Users logged in through the web application have full access, which93Users logged in through the web application have full access, which
102means they can read/change any object they have access to.94means they can read/change any object they have access to.
10395
=== 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-14 17:12:48 +0000
@@ -64,6 +64,23 @@
64 description=_('The secret which, if not empty, should be used by the '64 description=_('The secret which, if not empty, should be used by the '
65 'consumer to sign its requests.'))65 'consumer to sign its requests.'))
6666
67 is_integrated_desktop = Attribute(
68 """This attribute is true if the consumer corresponds to a
69 user account on a personal computer or similar device.""")
70
71 integrated_desktop_name = Attribute(
72 """If the consumer corresponds to a user account on a personal
73 computer or similar device, this is the self-reported name of
74 the computer. If the consumer is a specific web or desktop
75 application, this is None.""")
76
77 integrated_desktop_type = Attribute(
78 """If the consumer corresponds to a user account on a personal
79 computer or similar device, this is the self-reported type of
80 that computer (usually the operating system plus the word
81 "desktop"). If the consumer is a specific web or desktop
82 application, this is None.""")
83
67 def newRequestToken():84 def newRequestToken():
68 """Return a new `IOAuthRequestToken` with a random key and secret.85 """Return a new `IOAuthRequestToken` with a random key and secret.
6986
7087
=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-10-03 15:30:06 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-10-14 17:12:48 +0000
@@ -39,32 +39,29 @@
3939
40 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')40 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
41 >>> print extract_text(main_content)41 >>> print extract_text(main_content)
42 Integrating foobar123451432 into your Launchpad account
42 The application identified as foobar123451432 wants to access Launchpad on43 The application identified as foobar123451432 wants to access Launchpad on
43 your behalf. What level of access do you want to grant?44 your behalf. What level of access do you want to grant?
44 ...45 ...
45 See all applications authorized to access Launchpad on your behalf.46 See all applications authorized to access Launchpad on your behalf.
4647
47This page contains one submit button for each item of OAuthPermission,48This page contains one submit button for each item of OAuthPermission,
48except for 'Grant Permissions', which must be specifically requested.49except for 'Desktop Integration', which must be specifically requested.
4950
50 >>> browser.getControl('No Access')51 >>> def print_access_levels(main_content):
51 <SubmitControl...52 ... actions = main_content.findAll('input', attrs={'type': 'submit'})
52 >>> browser.getControl('Read Non-Private Data')53 ... for action in actions:
53 <SubmitControl...54 ... print action['value']
54 >>> browser.getControl('Change Non-Private Data')55
55 <SubmitControl...56 >>> print_access_levels(main_content)
56 >>> browser.getControl('Read Anything')57 No Access
57 <SubmitControl...58 Read Non-Private Data
58 >>> browser.getControl('Change Anything')59 Change Non-Private Data
59 <SubmitControl...60 Read Anything
6061 Change Anything
61 >>> browser.getControl('Grant Permissions')62
62 Traceback (most recent call last):63 >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
63 ...
64 LookupError: label 'Grant Permissions'
65
66 >>> actions = main_content.findAll('input', attrs={'type': 'submit'})64 >>> actions = main_content.findAll('input', attrs={'type': 'submit'})
67 >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
68 >>> len(actions) == len(OAuthPermission.items) - 165 >>> len(actions) == len(OAuthPermission.items) - 1
69 True66 True
7067
@@ -74,57 +71,53 @@
74that isn't enough for the application. The user always has the option71that isn't enough for the application. The user always has the option
75to deny permission altogether.72to deny permission altogether.
7673
77 >>> def print_access_levels(allow_permission):74 >>> def authorize_token_main_content(allow_permission):
78 ... browser.open(75 ... browser.open(
79 ... "http://launchpad.dev/+authorize-token?%s&%s"76 ... "http://launchpad.dev/+authorize-token?%s&%s"
80 ... % (urlencode(params), allow_permission))77 ... % (urlencode(params), allow_permission))
81 ... main_content = find_tag_by_id(browser.contents, 'maincontent')78 ... return find_tag_by_id(browser.contents, 'maincontent')
82 ... actions = main_content.findAll('input', attrs={'type': 'submit'})79
83 ... for action in actions:80 >>> def print_access_levels_for(allow_permission):
84 ... print action['value']81 ... main_content = authorize_token_main_content(allow_permission)
8582 ... print_access_levels(main_content)
86 >>> print_access_levels(83
84 >>> print_access_levels_for(
87 ... 'allow_permission=WRITE_PUBLIC&allow_permission=WRITE_PRIVATE')85 ... 'allow_permission=WRITE_PUBLIC&allow_permission=WRITE_PRIVATE')
88 No Access86 No Access
89 Change Non-Private Data87 Change Non-Private Data
90 Change Anything88 Change Anything
9189
92The only time the 'Grant Permissions' permission shows up in this list
93is if the client specifically requests it, and no other
94permission. (Also requesting UNAUTHORIZED is okay--it will show up
95anyway.)
96
97 >>> print_access_levels('allow_permission=GRANT_PERMISSIONS')
98 No Access
99 Grant Permissions
100
101 >>> print_access_levels(
102 ... 'allow_permission=GRANT_PERMISSIONS&allow_permission=UNAUTHORIZED')
103 No Access
104 Grant Permissions
105
106 >>> print_access_levels(
107 ... 'allow_permission=WRITE_PUBLIC&allow_permission=GRANT_PERMISSIONS')
108 No Access
109 Change Non-Private Data
110
111If an application doesn't specify any valid access levels, or only90If an application doesn't specify any valid access levels, or only
112specifies the UNAUTHORIZED access level, Launchpad will show all the91specifies the UNAUTHORIZED access level, Launchpad will show all the
113access levels, except for GRANT_PERMISSIONS.92access levels, except for DESKTOP_INTEGRATION.
11493
115 >>> print_access_levels('')94 >>> print_access_levels_for('')
116 No Access95 No Access
117 Read Non-Private Data96 Read Non-Private Data
118 Change Non-Private Data97 Change Non-Private Data
119 Read Anything98 Read Anything
120 Change Anything99 Change Anything
121100
122 >>> print_access_levels('allow_permission=UNAUTHORIZED')101 >>> print_access_levels_for('allow_permission=UNAUTHORIZED')
123 No Access102 No Access
124 Read Non-Private Data103 Read Non-Private Data
125 Change Non-Private Data104 Change Non-Private Data
126 Read Anything105 Read Anything
127 Change Anything106 Change Anything
107
108An application may not request the DESKTOP_INTEGRATION access level
109unless its consumer key matches a certain pattern. (Successful desktop
110integration has its own section, below.)
111
112 >>> allow_permission = "allow_permission=DESKTOP_INTEGRATION"
113 >>> browser.open(
114 ... "http://launchpad.dev/+authorize-token?%s&%s"
115 ... % (urlencode(params), allow_permission))
116 Traceback (most recent call last):
117 ...
118 Unauthorized: Consumer "foobar123451432" asked for desktop
119 integration, but didn't say what kind of desktop it is, or name
120 the computer being integrated.
128121
129An application may also specify a context, so that the access granted122An application may also specify a context, so that the access granted
130by the user is restricted to things related to that context.123by the user is restricted to things related to that context.
@@ -136,6 +129,7 @@
136 ... % urlencode(params_with_context))129 ... % urlencode(params_with_context))
137 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')130 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
138 >>> print extract_text(main_content)131 >>> print extract_text(main_content)
132 Integrating foobar123451432 into your Launchpad account
139 The application...wants to access things related to Mozilla Firefox...133 The application...wants to access things related to Mozilla Firefox...
140134
141A client other than a web browser may request a JSON representation of135A client other than a web browser may request a JSON representation of
@@ -263,3 +257,95 @@
263 This request for accessing Launchpad on your behalf has been257 This request for accessing Launchpad on your behalf has been
264 reviewed ... ago.258 reviewed ... ago.
265 See all applications authorized to access Launchpad on your behalf.259 See all applications authorized to access Launchpad on your behalf.
260
261Desktop integration
262===================
263
264The test case given above shows how to integrate a single application
265or website into Launchpad. But it's also possible to integrate an
266entire desktop environment into Launchpad.
267
268The desktop integration option is only available for OAuth consumers
269that say what kind of desktop they are (eg. Ubuntu) and give a name
270that a user can identify with their computer (eg. the hostname). Here,
271we'll create such a token.
272
273 >>> login('salgado@ubuntu.com')
274 >>> desktop_key = "System-wide: Ubuntu desktop (mycomputer)"
275 >>> consumer = getUtility(IOAuthConsumerSet).new(desktop_key)
276 >>> token = consumer.newRequestToken()
277 >>> logout()
278
279When a desktop tries to integrate with Launchpad, the user gets a
280special warning about giving access to every program running on their
281desktop.
282
283 >>> params = dict(oauth_token=token.key)
284 >>> print extract_text(
285 ... authorize_token_main_content(
286 ... 'allow_permission=DESKTOP_INTEGRATION'))
287 Integrating mycomputer into your Launchpad account
288 The Ubuntu desktop called mycomputer wants access to your
289 Launchpad account. If you allow the integration, all applications
290 running on mycomputer will have read-write access to your
291 Launchpad account, including to your private data.
292 If you're using a public computer, if mycomputer is not the
293 computer you're using right now, or if something just doesn't feel
294 right about this situation, you should choose "No, thanks, I don't
295 trust 'mycomputer'", or close this window now. You can always try
296 again later.
297 Even if you decide to allow the integration, you can change your
298 mind later.
299 See all applications authorized to access Launchpad on your behalf.
300
301
302The only time the 'Desktop Integration' permission shows up in the
303list of permissions is if the client specifically requests it, and no
304other permission. (Also requesting UNAUTHORIZED is okay--it will show
305up anyway.)
306
307 >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
308 Give all programs running on "mycomputer" access to my Launchpad account.
309 No, thanks, I don't trust "mycomputer".
310
311 >>> print_access_levels_for(
312 ... 'allow_permission=DESKTOP_INTEGRATION&allow_permission=UNAUTHORIZED')
313 Give all programs running on "mycomputer" access to my Launchpad account.
314 No, thanks, I don't trust "mycomputer".
315
316A desktop may not request a level of access other than
317DESKTOP_INTEGRATION, since the whole point is to have a permission
318level that specifically applies across the entire desktop.
319
320 >>> print_access_levels_for('allow_permission=WRITE_PRIVATE')
321 Traceback (most recent call last):
322 ...
323 Unauthorized: Desktop integration token requested a permission
324 ("Change Anything") not supported for desktop-wide use.
325
326 >>> print_access_levels_for(
327 ... 'allow_permission=WRITE_PUBLIC&allow_permission=DESKTOP_INTEGRATION')
328 Traceback (most recent call last):
329 ...
330 Unauthorized: Desktop integration token requested a permission
331 ("Change Non-Private Data") not supported for desktop-wide use.
332
333You can't specify a callback URL when authorizing a desktop-wide
334token, since callback URLs should only be used when integrating
335websites into Launchpad.
336
337 >>> params['oauth_callback'] = 'http://launchpad.dev/bzr'
338 >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
339 Traceback (most recent call last):
340 ...
341 Unauthorized: A desktop integration may not specify an OAuth
342 callback URL.
343
344This is true even if the desktop token isn't asking for the
345DESKTOP_INTEGRATION permission.
346
347 >>> print_access_levels_for('allow_permission=WRITE_PRIVATE')
348 Traceback (most recent call last):
349 ...
350 Unauthorized: A desktop integration may not specify an OAuth
351 callback URL.
266352
=== 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-14 17:12:48 +0000
@@ -21,28 +21,69 @@
21 <tal:token-not-reviewed condition="not:token/is_reviewed">21 <tal:token-not-reviewed condition="not:token/is_reviewed">
22 <div metal:use-macro="context/@@launchpad_form/form">22 <div metal:use-macro="context/@@launchpad_form/form">
23 <div metal:fill-slot="extra_top">23 <div metal:fill-slot="extra_top">
24 <p>The application identified as24
25 <strong tal:content="token/consumer/key">consumer</strong>25 <tal:desktop-integration-token condition="token/consumer/is_integrated_desktop">
26 wants to access26 <h1>Integrating
27 <tal:has-context condition="view/token_context">27 <tal:hostname replace="structure
28 things related to28 token/consumer/integrated_desktop_name" />
29 <strong tal:content="view/token_context/title">Context</strong>29 into your Launchpad account</h1>
30 in30 <p>The
31 </tal:has-context>31 <tal:desktop replace="structure
32 Launchpad on your behalf. What level of access32 token/consumer/integrated_desktop_type" />
33 do you want to grant?</p>33 called
3434 <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
35 <table>35 wants access to your Launchpad account. If you allow the
36 <tr tal:repeat="action view/visible_actions">36 integration, all applications running
37 <td style="text-align: right">37 on <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
38 <tal:action replace="structure action/render" />38 will have read-write access to your Launchpad account,
39 </td>39 including to your private data.</p>
40 <td>40
41 <span class="lesser"41 <p>If you're using a public computer, if
42 tal:content="action/permission/description" />42 <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
43 </td>43 is not the computer you're using right now, or if
44 </tr>44 something just doesn't feel right about this situation,
45 </table>45 you should choose "No, thanks, I don't trust
46 '<tal:hostname replace="structure
47 token/consumer/integrated_desktop_name" />'",
48 or close this window now. You can always try
49 again later.</p>
50
51 <p>Even if you decide to allow the integration, you can
52 change your mind later.</p>
53 </tal:desktop-integration-token>
54
55 <tal:web-integration-token condition="not:token/consumer/is_integrated_desktop">
56 <h1>Integrating
57 <tal:hostname replace="structure token/consumer/key" />
58 into your Launchpad account</h1>
59
60 <p>The application identified as
61 <strong tal:content="token/consumer/key">consumer</strong>
62 wants to access
63 <tal:has-context condition="view/token_context">
64 things related to
65 <strong tal:content="view/token_context/title">Context</strong>
66 in
67 </tal:has-context>
68 Launchpad on your behalf. What level of access
69 do you want to grant?</p>
70 </tal:web-integration-token>
71
72 <table>
73 <tr tal:repeat="action view/visible_actions">
74 <td style="text-align: right">
75 <tal:action replace="structure action/render" />
76 </td>
77
78 <tal:web-integration-token
79 condition="not:token/consumer/is_integrated_desktop">
80 <td>
81 <span class="lesser"
82 tal:content="action/permission/description" />
83 </td>
84 </tal:web-integration-token>
85 </tr>
86 </table>
46 </div>87 </div>
4788
48 <div metal:fill-slot="extra_bottom">89 <div metal:fill-slot="extra_bottom">
4990
=== 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-14 17:12:48 +0000
@@ -61,7 +61,8 @@
61 lp_permission = getUtility(ILaunchpadPermission, permission)61 lp_permission = getUtility(ILaunchpadPermission, permission)
62 if lp_permission.access_level == "write":62 if lp_permission.access_level == "write":
63 required_access_level = [63 required_access_level = [
64 AccessLevel.WRITE_PUBLIC, AccessLevel.WRITE_PRIVATE]64 AccessLevel.WRITE_PUBLIC, AccessLevel.WRITE_PRIVATE,
65 AccessLevel.DESKTOP_INTEGRATION]
65 if access_level not in required_access_level:66 if access_level not in required_access_level:
66 return False67 return False
67 elif lp_permission.access_level == "read":68 elif lp_permission.access_level == "read":
@@ -80,7 +81,8 @@
80 access to private objects, return False. Return True otherwise.81 access to private objects, return False. Return True otherwise.
81 """82 """
82 private_access_levels = [83 private_access_levels = [
83 AccessLevel.READ_PRIVATE, AccessLevel.WRITE_PRIVATE]84 AccessLevel.READ_PRIVATE, AccessLevel.WRITE_PRIVATE,
85 AccessLevel.DESKTOP_INTEGRATION]
84 if access_level in private_access_levels:86 if access_level in private_access_levels:
85 # The user has access to private objects. Return early,87 # The user has access to private objects. Return early,
86 # before checking whether the object is private, since88 # before checking whether the object is private, since
8789
=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
--- lib/canonical/launchpad/webapp/interfaces.py 2010-09-24 09:42:01 +0000
+++ lib/canonical/launchpad/webapp/interfaces.py 2010-10-14 17:12:48 +0000
@@ -531,14 +531,13 @@
531 for reading and changing anything, including private data.531 for reading and changing anything, including private data.
532 """)532 """)
533533
534 GRANT_PERMISSIONS = DBItem(60, """534 DESKTOP_INTEGRATION = DBItem(60, """
535 Grant Permissions535 Desktop Integration
536536
537 The application will be able to grant access to your Launchpad537 Every application running on your desktop will have read-write
538 account to any other application. This is a very powerful538 access to your Launchpad account, including to your private
539 level of access. You should not grant this level of access to539 data. You should not allow this unless you trust the computer
540 any application except the official Launchpad credential540 you're using right now.
541 manager.
542 """)541 """)
543542
544class AccessLevel(DBEnumeratedType):543class AccessLevel(DBEnumeratedType):