~fabricematrat/charmworld/redirect-charm

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