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