91.2.18
by Aaron Bentley
Updates from review. |
1 |
# Copied from 92:lp:django-openid-auth on Jan 2 2013
|
91.2.2
by Aaron Bentley
Get initial teams support working. |
2 |
# Launchpad OpenID Teams Extension support for python-openid
|
3 |
#
|
|
4 |
# Copyright (C) 2008-2010 Canonical Ltd.
|
|
5 |
#
|
|
6 |
# Redistribution and use in source and binary forms, with or without
|
|
7 |
# modification, are permitted provided that the following conditions
|
|
8 |
# are met:
|
|
9 |
#
|
|
10 |
# * Redistributions of source code must retain the above copyright
|
|
11 |
# notice, this list of conditions and the following disclaimer.
|
|
12 |
#
|
|
13 |
# * Redistributions in binary form must reproduce the above copyright
|
|
14 |
# notice, this list of conditions and the following disclaimer in the
|
|
15 |
# documentation and/or other materials provided with the distribution.
|
|
16 |
#
|
|
17 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
18 |
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
19 |
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
20 |
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
21 |
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
22 |
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
23 |
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24 |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
26 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
27 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28 |
# POSSIBILITY OF SUCH DAMAGE.
|
|
29 |
||
30 |
"""Team membership support for Launchpad.
|
|
31 |
||
32 |
The primary form of communication between the RP and Launchpad is an
|
|
33 |
OpenID authentication request. Our solution is to piggyback a team
|
|
34 |
membership test onto this interaction.
|
|
35 |
||
36 |
As part of an OpenID authentication request, the RP includes the
|
|
37 |
following fields:
|
|
38 |
||
39 |
openid.ns.lp:
|
|
40 |
An OpenID 2.0 namespace URI for the extension. It is not strictly
|
|
41 |
required for 1.1 requests, but including it is good for forward
|
|
42 |
compatibility.
|
|
43 |
||
44 |
It must be set to: http://ns.launchpad.net/2007/openid-teams
|
|
45 |
||
46 |
openid.lp.query_membership:
|
|
47 |
A comma separated list of Launchpad team names that the RP is
|
|
48 |
interested in.
|
|
49 |
||
50 |
As part of the positive assertion OpenID response, the following field
|
|
51 |
will be provided:
|
|
52 |
||
53 |
openid.ns.lp:
|
|
54 |
(as above)
|
|
55 |
||
56 |
openid.lp.is_member:
|
|
57 |
A comma separated list of teams that the user is actually a member
|
|
58 |
of. The list may be limited to those teams mentioned in the
|
|
59 |
request.
|
|
60 |
||
61 |
This field must be included in the response signature in order to
|
|
62 |
be considered valid (as the response is bounced through the user's
|
|
63 |
web browser, an unsigned value could be modified).
|
|
64 |
||
65 |
@since: 2.1.1
|
|
66 |
"""
|
|
67 |
||
68 |
from openid.message import registerNamespaceAlias, \ |
|
69 |
NamespaceAliasRegistrationError
|
|
70 |
from openid.extension import Extension |
|
71 |
from openid import oidutil |
|
72 |
||
73 |
try: |
|
74 |
basestring #pylint:disable-msg=W0104 |
|
75 |
except NameError: |
|
76 |
# For Python 2.2
|
|
77 |
basestring = (str, unicode) #pylint:disable-msg=W0622 |
|
78 |
||
79 |
__all__ = [ |
|
80 |
'TeamsRequest', |
|
81 |
'TeamsResponse', |
|
82 |
'ns_uri', |
|
83 |
'supportsTeams', |
|
84 |
]
|
|
85 |
||
86 |
ns_uri = 'http://ns.launchpad.net/2007/openid-teams' |
|
87 |
||
88 |
try: |
|
89 |
registerNamespaceAlias(ns_uri, 'lp') |
|
90 |
except NamespaceAliasRegistrationError, e: |
|
91 |
oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri, |
|
92 |
'lp', str(e),)) |
|
93 |
||
94 |
def supportsTeams(endpoint): |
|
95 |
"""Does the given endpoint advertise support for Launchpad Teams?
|
|
96 |
||
97 |
@param endpoint: The endpoint object as returned by OpenID discovery
|
|
98 |
@type endpoint: openid.consumer.discover.OpenIDEndpoint
|
|
99 |
||
100 |
@returns: Whether an lp type was advertised by the endpoint
|
|
101 |
@rtype: bool
|
|
102 |
"""
|
|
103 |
return endpoint.usesExtension(ns_uri) |
|
104 |
||
105 |
class TeamsNamespaceError(ValueError): |
|
106 |
"""The Launchpad teams namespace was not found and could not
|
|
107 |
be created using the expected name (there's another extension
|
|
108 |
using the name 'lp')
|
|
109 |
||
110 |
This is not I{illegal}, for OpenID 2, although it probably
|
|
111 |
indicates a problem, since it's not expected that other extensions
|
|
112 |
will re-use the alias that is in use for OpenID 1.
|
|
113 |
||
114 |
If this is an OpenID 1 request, then there is no recourse. This
|
|
115 |
should not happen unless some code has modified the namespaces for
|
|
116 |
the message that is being processed.
|
|
117 |
"""
|
|
118 |
||
119 |
def getTeamsNS(message): |
|
120 |
"""Extract the Launchpad teams namespace URI from the given
|
|
121 |
OpenID message.
|
|
122 |
||
123 |
@param message: The OpenID message from which to parse Launchpad
|
|
124 |
teams. This may be a request or response message.
|
|
125 |
@type message: C{L{openid.message.Message}}
|
|
126 |
||
127 |
@returns: the lp namespace URI for the supplied message. The
|
|
128 |
message may be modified to define a Launchpad teams
|
|
129 |
namespace.
|
|
130 |
@rtype: C{str}
|
|
131 |
||
132 |
@raise ValueError: when using OpenID 1 if the message defines
|
|
133 |
the 'lp' alias to be something other than a Launchpad
|
|
134 |
teams type.
|
|
135 |
"""
|
|
136 |
# See if there exists an alias for the Launchpad teams type.
|
|
137 |
alias = message.namespaces.getAlias(ns_uri) |
|
138 |
if alias is None: |
|
139 |
# There is no alias, so try to add one. (OpenID version 1)
|
|
140 |
try: |
|
141 |
message.namespaces.addAlias(ns_uri, 'lp') |
|
142 |
except KeyError, why: |
|
143 |
# An alias for the string 'lp' already exists, but it's
|
|
144 |
# defined for something other than Launchpad teams
|
|
145 |
raise TeamsNamespaceError(why[0]) |
|
146 |
||
147 |
# we know that ns_uri defined, because it's defined in the
|
|
148 |
# else clause of the loop as well, so disable the warning
|
|
149 |
return ns_uri #pylint:disable-msg=W0631 |
|
150 |
||
151 |
class TeamsRequest(Extension): |
|
152 |
"""An object to hold the state of a Launchpad teams request.
|
|
153 |
||
154 |
@ivar query_membership: A comma separated list of Launchpad team
|
|
155 |
names that the RP is interested in.
|
|
156 |
@type required: [str]
|
|
157 |
||
158 |
@group Consumer: requestField, requestTeams, getExtensionArgs, addToOpenIDRequest
|
|
159 |
@group Server: fromOpenIDRequest, parseExtensionArgs
|
|
160 |
"""
|
|
161 |
||
162 |
ns_alias = 'lp' |
|
163 |
||
164 |
def __init__(self, query_membership=None, lp_ns_uri=ns_uri): |
|
165 |
"""Initialize an empty Launchpad teams request"""
|
|
166 |
Extension.__init__(self) |
|
167 |
self.query_membership = [] |
|
168 |
self.ns_uri = lp_ns_uri |
|
169 |
||
170 |
if query_membership: |
|
171 |
self.requestTeams(query_membership) |
|
172 |
||
173 |
# Assign getTeamsNS to a static method so that it can be
|
|
174 |
# overridden for testing.
|
|
175 |
_getTeamsNS = staticmethod(getTeamsNS) |
|
176 |
||
177 |
def fromOpenIDRequest(cls, request): |
|
178 |
"""Create a Launchpad teams request that contains the
|
|
179 |
fields that were requested in the OpenID request with the
|
|
180 |
given arguments
|
|
181 |
||
182 |
@param request: The OpenID request
|
|
183 |
@type request: openid.server.CheckIDRequest
|
|
184 |
||
185 |
@returns: The newly created Launchpad teams request
|
|
186 |
@rtype: C{L{TeamsRequest}}
|
|
187 |
"""
|
|
188 |
self = cls() |
|
189 |
||
190 |
# Since we're going to mess with namespace URI mapping, don't
|
|
191 |
# mutate the object that was passed in.
|
|
192 |
message = request.message.copy() |
|
193 |
||
194 |
self.ns_uri = self._getTeamsNS(message) |
|
195 |
args = message.getArgs(self.ns_uri) |
|
196 |
self.parseExtensionArgs(args) |
|
197 |
||
198 |
return self |
|
199 |
||
200 |
fromOpenIDRequest = classmethod(fromOpenIDRequest) |
|
201 |
||
202 |
def parseExtensionArgs(self, args, strict=False): |
|
203 |
"""Parse the unqualified Launchpad teams request
|
|
204 |
parameters and add them to this object.
|
|
205 |
||
206 |
This method is essentially the inverse of
|
|
207 |
C{L{getExtensionArgs}}. This method restores the serialized
|
|
208 |
Launchpad teams request fields.
|
|
209 |
||
210 |
If you are extracting arguments from a standard OpenID
|
|
211 |
checkid_* request, you probably want to use C{L{fromOpenIDRequest}},
|
|
212 |
which will extract the lp namespace and arguments from the
|
|
213 |
OpenID request. This method is intended for cases where the
|
|
214 |
OpenID server needs more control over how the arguments are
|
|
215 |
parsed than that method provides.
|
|
216 |
||
217 |
>>> args = message.getArgs(ns_uri)
|
|
218 |
>>> request.parseExtensionArgs(args)
|
|
219 |
||
220 |
@param args: The unqualified Launchpad teams arguments
|
|
221 |
@type args: {str:str}
|
|
222 |
||
223 |
@param strict: Whether requests with fields that are not
|
|
224 |
defined in the Launchpad teams specification should be
|
|
225 |
tolerated (and ignored)
|
|
226 |
@type strict: bool
|
|
227 |
||
228 |
@returns: None; updates this object
|
|
229 |
"""
|
|
230 |
items = args.get('query_membership') |
|
231 |
if items: |
|
232 |
for team_name in items.split(','): |
|
233 |
try: |
|
234 |
self.requestTeam(team_name, strict) |
|
235 |
except ValueError: |
|
236 |
if strict: |
|
237 |
raise
|
|
238 |
||
239 |
def allRequestedTeams(self): |
|
240 |
"""A list of all of the Launchpad teams that were
|
|
241 |
requested.
|
|
242 |
||
243 |
@rtype: [str]
|
|
244 |
"""
|
|
245 |
return self.query_membership |
|
246 |
||
247 |
def wereTeamsRequested(self): |
|
248 |
"""Have any Launchpad teams been requested?
|
|
249 |
||
250 |
@rtype: bool
|
|
251 |
"""
|
|
252 |
return bool(self.allRequestedTeams()) |
|
253 |
||
254 |
def __contains__(self, team_name): |
|
255 |
"""Was this team in the request?"""
|
|
256 |
return team_name in self.query_membership |
|
257 |
||
258 |
def requestTeam(self, team_name, strict=False): |
|
259 |
"""Request the specified team from the OpenID user
|
|
260 |
||
261 |
@param team_name: the unqualified Launchpad team name
|
|
262 |
@type team_name: str
|
|
263 |
||
264 |
@param strict: whether to raise an exception when a team is
|
|
265 |
added to a request more than once
|
|
266 |
||
267 |
@raise ValueError: when strict is set and the team was
|
|
268 |
requested more than once
|
|
269 |
"""
|
|
270 |
if strict: |
|
271 |
if team_name in self.query_membership: |
|
272 |
raise ValueError('That team has already been requested') |
|
273 |
else: |
|
274 |
if team_name in self.query_membership: |
|
275 |
return
|
|
276 |
||
277 |
self.query_membership.append(team_name) |
|
278 |
||
279 |
def requestTeams(self, query_membership, strict=False): |
|
280 |
"""Add the given list of teams to the request
|
|
281 |
||
282 |
@param query_membership: The Launchpad teams request
|
|
283 |
@type query_membership: [str]
|
|
284 |
||
285 |
@raise ValueError: when a team requested is not a string
|
|
286 |
or strict is set and a team was requested more than once
|
|
287 |
"""
|
|
288 |
if isinstance(query_membership, basestring): |
|
289 |
raise TypeError('Teams should be passed as a list of ' |
|
290 |
'strings (not %r)' % (type(query_membership),)) |
|
291 |
||
292 |
for team_name in query_membership: |
|
293 |
self.requestTeam(team_name, strict=strict) |
|
294 |
||
295 |
def getExtensionArgs(self): |
|
296 |
"""Get a dictionary of unqualified Launchpad teams
|
|
297 |
arguments representing this request.
|
|
298 |
||
299 |
This method is essentially the inverse of
|
|
300 |
C{L{parseExtensionArgs}}. This method serializes the Launchpad
|
|
301 |
teams request fields.
|
|
302 |
||
303 |
@rtype: {str:str}
|
|
304 |
"""
|
|
305 |
args = {} |
|
306 |
||
307 |
if self.query_membership: |
|
308 |
args['query_membership'] = ','.join(self.query_membership) |
|
309 |
||
310 |
return args |
|
311 |
||
312 |
class TeamsResponse(Extension): |
|
313 |
"""Represents the data returned in a Launchpad teams response
|
|
314 |
inside of an OpenID C{id_res} response. This object will be
|
|
315 |
created by the OpenID server, added to the C{id_res} response
|
|
316 |
object, and then extracted from the C{id_res} message by the
|
|
317 |
Consumer.
|
|
318 |
||
319 |
@ivar data: The Launchpad teams data, an array.
|
|
320 |
||
321 |
@ivar ns_uri: The URI under which the Launchpad teams data was
|
|
322 |
stored in the response message.
|
|
323 |
||
324 |
@group Server: extractResponse
|
|
325 |
@group Consumer: fromSuccessResponse
|
|
326 |
@group Read-only dictionary interface: keys, iterkeys, items, iteritems,
|
|
327 |
__iter__, get, __getitem__, keys, has_key
|
|
328 |
"""
|
|
329 |
||
330 |
ns_alias = 'lp' |
|
331 |
||
332 |
def __init__(self, is_member=None, lp_ns_uri=ns_uri): |
|
333 |
Extension.__init__(self) |
|
334 |
if is_member is None: |
|
335 |
self.is_member = [] |
|
336 |
else: |
|
337 |
self.is_member = is_member |
|
338 |
||
339 |
self.ns_uri = lp_ns_uri |
|
340 |
||
341 |
def addTeam(self, team_name): |
|
342 |
if team_name not in self.is_member: |
|
343 |
self.is_member.append(team_name) |
|
344 |
||
345 |
def extractResponse(cls, request, is_member_str): |
|
346 |
"""Take a C{L{TeamsRequest}} and a list of Launchpad
|
|
347 |
team values and create a C{L{TeamsResponse}}
|
|
348 |
object containing that data.
|
|
349 |
||
350 |
@param request: The Launchpad teams request object
|
|
351 |
@type request: TeamsRequest
|
|
352 |
||
353 |
@param is_member: The Launchpad teams data for this
|
|
354 |
response, as a list of strings.
|
|
355 |
@type is_member: {str:str}
|
|
356 |
||
357 |
@returns: a Launchpad teams response object
|
|
358 |
@rtype: TeamsResponse
|
|
359 |
"""
|
|
360 |
self = cls() |
|
361 |
self.ns_uri = request.ns_uri |
|
362 |
self.is_member = is_member_str.split(',') |
|
363 |
return self |
|
364 |
||
365 |
extractResponse = classmethod(extractResponse) |
|
366 |
||
367 |
# Assign getTeamsNS to a static method so that it can be
|
|
368 |
# overridden for testing
|
|
369 |
_getTeamsNS = staticmethod(getTeamsNS) |
|
370 |
||
371 |
def fromSuccessResponse(cls, success_response, signed_only=True): |
|
372 |
"""Create a C{L{TeamsResponse}} object from a successful OpenID
|
|
373 |
library response
|
|
374 |
(C{L{openid.consumer.consumer.SuccessResponse}}) response
|
|
375 |
message
|
|
376 |
||
377 |
@param success_response: A SuccessResponse from consumer.complete()
|
|
378 |
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
|
|
379 |
||
380 |
@param signed_only: Whether to process only data that was
|
|
381 |
signed in the id_res message from the server.
|
|
382 |
@type signed_only: bool
|
|
383 |
||
384 |
@rtype: TeamsResponse
|
|
385 |
@returns: A Launchpad teams response containing the data
|
|
386 |
that was supplied with the C{id_res} response.
|
|
387 |
"""
|
|
388 |
self = cls() |
|
389 |
self.ns_uri = self._getTeamsNS(success_response.message) |
|
390 |
if signed_only: |
|
391 |
args = success_response.getSignedNS(self.ns_uri) |
|
392 |
else: |
|
393 |
args = success_response.message.getArgs(self.ns_uri) |
|
394 |
||
395 |
if "is_member" in args: |
|
396 |
is_member_str = args["is_member"] |
|
397 |
self.is_member = is_member_str.split(',') |
|
398 |
#self.is_member = args["is_member"]
|
|
399 |
||
400 |
return self |
|
401 |
||
402 |
fromSuccessResponse = classmethod(fromSuccessResponse) |
|
403 |
||
404 |
def getExtensionArgs(self): |
|
405 |
"""Get the fields to put in the Launchpad teams namespace
|
|
406 |
when adding them to an id_res message.
|
|
407 |
||
408 |
@see: openid.extension
|
|
409 |
"""
|
|
410 |
ns_args = {'is_member': ','.join(self.is_member),} |
|
411 |
return ns_args |
|
412 |