~launchpad-pqm/launchpad/devel

14538.1.3 by Curtis Hovey
Updated copyright.
1
# Copyright 2010-2011 Canonical Ltd.  All rights reserved.
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
2
3
"""Test OpenID server."""
4
5
__metaclass__ = type
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
6
__all__ = [
10212.5.4 by Guilherme Salgado
A few docstrings and style changes suggested by Gary
7
    'PersistentIdentityView',
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
8
    'TestOpenIDApplicationNavigation',
10556.4.8 by Guilherme Salgado
Shut up noisy OpenID library, as previously done by c-i-p
9
    'TestOpenIDIndexView',
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
10
    'TestOpenIDLoginView',
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
11
    'TestOpenIDRootUrlData',
12
    'TestOpenIDView',
13
    ]
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
14
15
from datetime import timedelta
16
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
17
from openid import oidutil
18
from openid.extensions.sreg import (
19
    SRegRequest,
20
    SRegResponse,
21
    )
22
from openid.server.server import (
23
    CheckIDRequest,
24
    ENCODE_HTML_FORM,
25
    Server,
26
    )
27
from openid.store.memstore import MemoryStore
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
28
from z3c.ptcompat import ViewPageTemplateFile
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
29
from zope.app.security.interfaces import IUnauthenticatedPrincipal
30
from zope.component import getUtility
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
31
from zope.interface import implements
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
32
from zope.security.proxy import isinstance as zisinstance
33
from zope.session.interfaces import ISession
34
14600.1.12 by Curtis Hovey
Move i18n to lp.
35
from lp import _
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
36
from lp.services.webapp import LaunchpadView
37
from lp.services.webapp.interfaces import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
    ICanonicalUrlData,
39
    IPlacelessLoginSource,
40
    )
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
41
from lp.services.webapp.login import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
42
    allowUnauthenticatedSession,
43
    logInPrincipal,
44
    logoutPerson,
45
    )
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
46
from lp.services.webapp.publisher import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
47
    Navigation,
48
    stepthrough,
49
    )
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
50
from lp.app.browser.launchpadform import (
51
    action,
52
    LaunchpadFormView,
53
    )
11270.1.4 by Tim Penhey
Update UnexpectedFormData imports.
54
from lp.app.errors import UnexpectedFormData
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
55
from lp.registry.interfaces.person import IPerson
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
56
from lp.services.identity.interfaces.account import (
57
    AccountStatus,
58
    IAccountSet,
59
    )
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
60
from lp.services.openid.browser.openiddiscovery import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
61
    XRDSContentNegotiationMixin,
62
    )
11382.6.34 by Gavin Panella
Reformat imports in all files touched so far.
63
from lp.services.propertycache import (
64
    cachedproperty,
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
65
    get_property_cache,
11382.6.34 by Gavin Panella
Reformat imports in all files touched so far.
66
    )
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
67
from lp.testopenid.interfaces.server import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
68
    get_server_url,
69
    ITestOpenIDApplication,
70
    ITestOpenIDLoginForm,
71
    ITestOpenIDPersistentIdentity,
72
    )
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
73
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
74
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
75
OPENID_REQUEST_SESSION_KEY = 'testopenid.request'
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
76
SESSION_PKG_KEY = 'TestOpenID'
10065.2.21 by Guilherme Salgado
Switch the test OpenID provider to use openid.store.MemoryStore instead of the store used by the SSO service
77
openid_store = MemoryStore()
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
78
79
10556.4.8 by Guilherme Salgado
Shut up noisy OpenID library, as previously done by c-i-p
80
# Shut up noisy OpenID library
81
oidutil.log = lambda message, level=0: None
82
83
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
84
class TestOpenIDRootUrlData:
85
    """`ICanonicalUrlData` for the test OpenID provider."""
86
87
    implements(ICanonicalUrlData)
88
89
    path = ''
90
    inside = None
91
    rootsite = 'testopenid'
92
93
    def __init__(self, context):
94
        self.context = context
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
95
96
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
97
class TestOpenIDApplicationNavigation(Navigation):
98
    """Navigation for `ITestOpenIDApplication`"""
99
    usedfor = ITestOpenIDApplication
100
101
    @stepthrough('+id')
102
    def traverse_id(self, name):
103
        """Traverse to persistent OpenID identity URLs."""
104
        try:
105
            account = getUtility(IAccountSet).getByOpenIDIdentifier(name)
106
        except LookupError:
107
            account = None
108
        if account is None or account.status != AccountStatus.ACTIVE:
109
            return None
110
        return ITestOpenIDPersistentIdentity(account)
111
112
113
class TestOpenIDXRDSContentNegotiationMixin(XRDSContentNegotiationMixin):
114
    """Custom XRDSContentNegotiationMixin that overrides openid_server_url."""
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
115
116
    @property
117
    def openid_server_url(self):
118
        """The OpenID Server endpoint URL for Launchpad."""
10382.1.1 by Guilherme Salgado
Turn the SERVER_URL constant (from lp.testopenid.browser) into a function so that the vhost.testopenid config section is not required
119
        return get_server_url()
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
120
121
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
122
class TestOpenIDIndexView(
123
        TestOpenIDXRDSContentNegotiationMixin, LaunchpadView):
124
    template = ViewPageTemplateFile("../templates/application-index.pt")
125
    xrds_template = ViewPageTemplateFile("../templates/application-xrds.pt")
126
127
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
128
class OpenIDMixin:
10212.5.4 by Guilherme Salgado
A few docstrings and style changes suggested by Gary
129
    """A mixin with OpenID helper methods."""
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
130
131
    openid_request = None
132
133
    def __init__(self, context, request):
134
        super(OpenIDMixin, self).__init__(context, request)
10382.1.1 by Guilherme Salgado
Turn the SERVER_URL constant (from lp.testopenid.browser) into a function so that the vhost.testopenid config section is not required
135
        self.server_url = get_server_url()
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
136
        self.openid_server = Server(openid_store, self.server_url)
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
137
138
    @property
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
139
    def user_identity_url(self):
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
140
        return ITestOpenIDPersistentIdentity(self.account).openid_identity_url
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
141
142
    def isIdentityOwner(self):
143
        """Return True if the user can authenticate as the given ID."""
144
        assert self.account is not None, "user should be logged in by now."
145
        return (self.openid_request.idSelect() or
146
                self.openid_request.identity == self.user_identity_url)
147
11382.6.30 by Gavin Panella
Convert lp.testopenid.browser.server to propertycache.
148
    @cachedproperty
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
149
    def openid_parameters(self):
150
        """A dictionary of OpenID query parameters from request."""
151
        query = {}
152
        for key, value in self.request.form.items():
153
            if key.startswith('openid.'):
10212.5.4 by Guilherme Salgado
A few docstrings and style changes suggested by Gary
154
                # All OpenID query args are supposed to be ASCII.
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
155
                query[key.encode('US-ASCII')] = value.encode('US-ASCII')
156
        return query
157
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
158
    def getSession(self):
10212.5.4 by Guilherme Salgado
A few docstrings and style changes suggested by Gary
159
        """Get the session data container that stores the OpenID request."""
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
160
        if IUnauthenticatedPrincipal.providedBy(self.request.principal):
161
            # A dance to assert that we want to break the rules about no
162
            # unauthenticated sessions. Only after this next line is it
163
            # safe to set session values.
164
            allowUnauthenticatedSession(
165
                self.request, duration=timedelta(minutes=60))
166
        return ISession(self.request)[SESSION_PKG_KEY]
167
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
168
    def restoreRequestFromSession(self):
169
        """Get the OpenIDRequest from our session."""
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
170
        session = self.getSession()
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
171
        cache = get_property_cache(self)
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
172
        try:
11382.6.30 by Gavin Panella
Convert lp.testopenid.browser.server to propertycache.
173
            cache.openid_parameters = session[OPENID_REQUEST_SESSION_KEY]
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
174
        except KeyError:
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
175
            raise UnexpectedFormData("No OpenID request in session")
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
176
177
        # Decode the request parameters and create the request object.
178
        self.openid_request = self.openid_server.decodeRequest(
179
            self.openid_parameters)
180
        assert zisinstance(self.openid_request, CheckIDRequest), (
181
            'Invalid OpenIDRequest in session')
182
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
183
    def saveRequestInSession(self):
184
        """Save the OpenIDRequest in our session."""
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
185
        query = self.openid_parameters
186
        assert query.get('openid.mode') == 'checkid_setup', (
187
            'Can only serialise checkid_setup OpenID requests')
188
189
        session = self.getSession()
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
190
        # If this was meant for use in production we'd have to use a nonce
191
        # as the key when storing the openid request in the session, but as
192
        # it's meant to run only on development instances we can simplify
193
        # things a bit by storing the openid request using a well known key.
194
        session[OPENID_REQUEST_SESSION_KEY] = query
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
195
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
196
    def renderOpenIDResponse(self, openid_response):
10212.5.4 by Guilherme Salgado
A few docstrings and style changes suggested by Gary
197
        """Return a web-suitable response constructed from openid_response."""
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
198
        webresponse = self.openid_server.encodeResponse(openid_response)
199
        response = self.request.response
200
        response.setStatus(webresponse.code)
11100.1.1 by Benji York
fix the three bugs that I've found thus far:
201
        # encodeResponse doesn't generate a content-type, help it out
11100.1.7 by Benji York
fix a small bug found during a full test run; there are non-html bodies
202
        if (webresponse.code == 200 and webresponse.body
203
                and openid_response.whichEncoding() == ENCODE_HTML_FORM):
11100.1.1 by Benji York
fix the three bugs that I've found thus far:
204
            response.setHeader('content-type', 'text/html')
10065.2.2 by Guilherme Salgado
Beginning of a fake openid provider which I'll use for automated tests.
205
        for header, value in webresponse.headers.items():
206
            response.setHeader(header, value)
207
        return webresponse.body
208
209
    def createPositiveResponse(self):
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
210
        """Create a positive assertion OpenIDResponse.
211
212
        This method should be called to create the response to
213
        successful checkid requests.
214
215
        If the trust root for the request is in openid_sreg_trustroots,
216
        then additional user information is included with the
217
        response.
218
        """
219
        assert self.account is not None, (
220
            'Must be logged in for positive OpenID response')
221
        assert self.openid_request is not None, (
222
            'No OpenID request to respond to.')
223
224
        if not self.isIdentityOwner():
225
            return self.createFailedResponse()
226
227
        if self.openid_request.idSelect():
228
            response = self.openid_request.answer(
229
                True, identity=self.user_identity_url)
230
        else:
231
            response = self.openid_request.answer(True)
232
7675.738.6 by Michael Nelson
Updated the testopenid server to always include email and full name.
233
        sreg_fields = dict(
234
            nickname=IPerson(self.account).name,
235
            email=self.account.preferredemail.email,
236
            fullname=self.account.displayname)
7675.671.2 by Brad Crittenden
Update testopenid to include sreg extension for nickname
237
        sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request)
238
        sreg_response = SRegResponse.extractResponse(
239
            sreg_request, sreg_fields)
240
        response.addExtension(sreg_response)
241
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
242
        return response
243
244
    def createFailedResponse(self):
245
        """Create a failed assertion OpenIDResponse.
246
247
        This method should be called to create the response to
248
        unsuccessful checkid requests.
249
        """
250
        assert self.openid_request is not None, (
251
            'No OpenID request to respond to.')
252
        response = self.openid_request.answer(False, self.server_url)
253
        return response
254
255
256
class TestOpenIDView(OpenIDMixin, LaunchpadView):
257
    """An OpenID Provider endpoint for Launchpad.
258
259
    This class implements an OpenID endpoint using the python-openid
260
    library.  In addition to the normal modes of operation, it also
261
    implements the OpenID 2.0 identifier select mode.
7675.738.6 by Michael Nelson
Updated the testopenid server to always include email and full name.
262
10065.2.14 by Guilherme Salgado
Change testopenid.lp.dev to always require authentication, thus providing a chance for users to login as somebody else
263
    Note that the checkid_immediate mode is not supported.
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
264
    """
265
266
    def render(self):
10065.2.14 by Guilherme Salgado
Change testopenid.lp.dev to always require authentication, thus providing a chance for users to login as somebody else
267
        """Handle all OpenID requests and form submissions."""
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
268
        # NB: Will be None if there are no parameters in the request.
269
        self.openid_request = self.openid_server.decodeRequest(
270
            self.openid_parameters)
271
272
        if self.openid_request.mode == 'checkid_setup':
273
            referer = self.request.get("HTTP_REFERER")
274
            if referer:
275
                self.request.response.setCookie("openid_referer", referer)
276
10065.2.14 by Guilherme Salgado
Change testopenid.lp.dev to always require authentication, thus providing a chance for users to login as somebody else
277
            # Log the user out and present the login page so that they can
278
            # authenticate as somebody else if they want.
279
            logoutPerson(self.request)
280
            return self.showLoginPage()
281
        elif self.openid_request.mode == 'checkid_immediate':
282
            raise UnexpectedFormData(
283
                'We do not handle checkid_immediate requests.')
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
284
        else:
10065.2.14 by Guilherme Salgado
Change testopenid.lp.dev to always require authentication, thus providing a chance for users to login as somebody else
285
            return self.renderOpenIDResponse(
286
                self.openid_server.handleRequest(self.openid_request))
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
287
288
    def showLoginPage(self):
289
        """Render the login dialog."""
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
290
        self.saveRequestInSession()
291
        return TestOpenIDLoginView(self.context, self.request)()
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
292
293
294
class TestOpenIDLoginView(OpenIDMixin, LaunchpadFormView):
10212.5.4 by Guilherme Salgado
A few docstrings and style changes suggested by Gary
295
    """A view for users to log into the OpenID provider."""
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
296
297
    page_title = "Login"
298
    schema = ITestOpenIDLoginForm
299
    action_url = '+auth'
10065.2.10 by Guilherme Salgado
Move all bits related to testopenid into its own package (lp.testopenid)
300
    template = ViewPageTemplateFile("../templates/auth.pt")
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
301
10065.2.24 by Guilherme Salgado
Simplify things in the test openid provider by storing the openid request in the session using a well known key rather than a nonce.
302
    def initialize(self):
303
        self.restoreRequestFromSession()
304
        super(TestOpenIDLoginView, self).initialize()
10065.2.9 by Guilherme Salgado
First working version of the test OpenID provider
305
306
    def validate(self, data):
307
        """Check that the email address and password are valid for login."""
308
        loginsource = getUtility(IPlacelessLoginSource)
309
        principal = loginsource.getPrincipalByLogin(data['email'])
310
        if principal is None or not principal.validate(data['password']):
311
            self.addError(
312
                _("Incorrect password for the provided email address."))
313
314
    @action('Continue', name='continue')
315
    def continue_action(self, action, data):
316
        email = data['email']
317
        principal = getUtility(IPlacelessLoginSource).getPrincipalByLogin(
318
            email)
319
        logInPrincipal(self.request, principal, email)
320
        # Update the attribute holding the cached user.
321
        self._account = principal.account
322
        return self.renderOpenIDResponse(self.createPositiveResponse())
10065.2.12 by Guilherme Salgado
Make the test openid work without the hack in c-i-p
323
324
325
class PersistentIdentityView(
326
        TestOpenIDXRDSContentNegotiationMixin, LaunchpadView):
327
    """Render the OpenID identity page."""
328
329
    xrds_template = ViewPageTemplateFile(
330
        "../templates/persistentidentity-xrds.pt")