1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
|
************
Introduction
************
Some standard behavior is defined by the web service itself, not by
the individual resources.
Multiple versions
=================
The Launchpad web service defines three versions: 'beta', '1.0', and
'devel'.
>>> def me_link_for_version(version):
... response = webservice.get("/", api_version=version)
... print response.jsonBody()['me_link']
>>> me_link_for_version('beta')
http://api.launchpad.dev/beta/people/+me
>>> me_link_for_version('1.0')
http://api.launchpad.dev/1.0/people/+me
>>> me_link_for_version('devel')
http://api.launchpad.dev/devel/people/+me
No other versions are available.
>>> print webservice.get("/", api_version="nosuchversion")
HTTP/1.1 404 Not Found
...
Anonymous requests
==================
A properly signed web service request whose OAuth token key is empty
is treated as an anonymous request.
>>> root = 'http://api.launchpad.dev/beta'
>>> body = anon_webservice.get(root).jsonBody()
>>> print body['projects_collection_link']
http://api.launchpad.dev/beta/projects
>>> print body['me_link']
http://api.launchpad.dev/beta/people/+me
Normally, Launchpad will reject any call made with an unrecognized
consumer key, because access tokens are registered with specific
consumer keys.
>>> from lp.testing.pages import (
... LaunchpadWebServiceCaller)
>>> from lp.services.oauth.interfaces import IOAuthConsumerSet
>>> caller = LaunchpadWebServiceCaller('new-consumer', 'access-key')
>>> response = caller.get(root)
>>> print response.getheader('status')
401 Unauthorized
>>> print response.body
Unknown consumer (new-consumer).
But with anonymous access there is no registration step. The first
time Launchpad sees a consumer key might be during an
anonymous request, and it can't reject that request just because it
doesn't recognize the client.
>>> login(ANONYMOUS)
>>> from zope.component import getUtility
>>> consumer_set = getUtility(IOAuthConsumerSet)
>>> print consumer_set.getByKey('another-new-consumer')
None
>>> logout()
>>> caller = LaunchpadWebServiceCaller('another-new-consumer', '')
>>> response = caller.get(root)
>>> print response.getheader('status')
200 Ok
Launchpad automatically adds new consumer keys it sees to its database.
>>> login(ANONYMOUS)
>>> print consumer_set.getByKey('another-new-consumer').key
another-new-consumer
>>> logout()
Anonymous requests can't access certain data.
>>> response = anon_webservice.get(body['me_link'])
>>> print response.getheader('status')
401 Unauthorized
>>> print response.body
You need to be logged in to view this URL.
Anonymous requests can't change the dataset.
>>> import simplejson
>>> data = simplejson.dumps({'display_name' : "This won't work"})
>>> response = anon_webservice.patch(root + "/~salgado",
... 'application/json', data)
>>> print response.getheader('status')
401 Unauthorized
>>> print response.body
(<Person at...>, 'displayname', 'launchpad.Edit')
A completely unsigned web service request is treated as an anonymous
request, with the OAuth consumer name being equal to the User-Agent.
>>> agent = "unsigned-user-agent"
>>> login(ANONYMOUS)
>>> print consumer_set.getByKey(agent)
None
>>> logout()
>>> from zope.app.testing.functional import HTTPCaller
>>> def request_with_user_agent(agent, url="/devel"):
... if agent is None:
... agent_string = ''
... else:
... agent_string = '\nUser-Agent: %s' % agent
... http = HTTPCaller()
... request = ("GET %s HTTP/1.1\n"
... "Host: api.launchpad.dev"
... "%s\n\n") % (url, agent_string)
... return http(request)
>>> response = request_with_user_agent(agent)
>>> print response.getOutput()
HTTP/1.1 200 Ok
...
{...}
Here, too, the OAuth consumer name is automatically registered if it
doesn't exist.
>>> login(ANONYMOUS)
>>> print consumer_set.getByKey(agent).key
unsigned-user-agent
>>> logout()
Here's another request now that the User-Agent has been registered as
a consumer name.
>>> response = request_with_user_agent(agent)
>>> print response.getOutput()
HTTP/1.1 200 Ok
...
{...}
An unsigned request, like a request signed with the empty string,
isn't logged in as any particular user:
>>> response = request_with_user_agent(agent, "/devel/people/+me")
>>> print response.getOutput()
HTTP/1.1 401 Unauthorized
...
You need to be logged in to view this URL.
API Requests to other hosts
===========================
JavaScript working with the API must deal with the browser's Same Origin
Policy - requests may only be made to the host that the page was loaded
from. For example, we can not visit a page on http://bugs.launchpad.net
and make a request to http://api.launchpad.net.
Instead of directing the request to api.launchpad.net, we may direct it
at the /api subpath of the current virtual host, such as
http://bugs.launchpad.net/api. Such requests are handled as if they
were directed at the api.launchpad.net subdomain.
The URLs in the returned representations point to the current host,
rather than the api host. The canonical_url() function also returns
links to the current host.
The ServiceRoot for http://bugs.launchpad.dev/api/devel/ is the same as a
request to http://api.launchpad.net/beta/, but with the links pointing
to a different host.
>>> webservice.domain = 'bugs.launchpad.dev'
>>> root = webservice.get(
... 'http://bugs.launchpad.dev/api/devel/').jsonBody()
>>> print root['people_collection_link']
http://bugs.launchpad.dev/api/devel/people
Requests on these hosts also honor the standard Launchpad authorization
scheme (and don't require OAuth).
>>> from lp.testing.pages import (
... LaunchpadWebServiceCaller)
>>> noauth_webservice = LaunchpadWebServiceCaller(
... domain='bugs.launchpad.dev')
>>> sample_auth = 'Basic %s' % 'test@canonical.com:test'.encode('base64')
>>> print noauth_webservice.get(
... 'http://bugs.launchpad.dev/api/devel/people/+me',
... headers={'Authorization': sample_auth})
HTTP/1.1 303 See Other
...
Location: http://bugs.launchpad.dev/api/devel/~name12...
...
But the regular authentication still doesn't work on the normal API
virtual host: an attempt to do HTTP Basic Auth will be treated as an
anonymous request.
>>> noauth_webservice.domain = 'api.launchpad.dev'
>>> print noauth_webservice.get(
... 'http://api.launchpad.dev/beta/people/+me',
... headers={'Authorization': sample_auth})
HTTP/1.1 401 Unauthorized
...
You need to be logged in to view this URL.
The 'Vary' Header
=================
Launchpad's web service sets the Vary header differently from other
parts of Launchpad.
>>> browser.open("http://launchpad.dev/")
>>> print browser.headers['Vary']
Cookie, Authorization
>>> response = webservice.get(
... 'http://bugs.launchpad.dev/api/devel/')
>>> print response.getheader('Vary')
Accept
The web service's Vary header does not mention the 'Cookie' header,
because the web service doesn't use cookies. It doesn't mention the
'Authorization' header, because every web service request has a
distinct 'Authorization' header. It does mention the 'Accept' header,
because the web service does use content negotiation.
|