1 | """NDG Security Demonstration Rendering Interface for OpenIDProviderMiddleware |
---|
2 | |
---|
3 | NERC DataGrid Project |
---|
4 | |
---|
5 | Moved from ndg.security.server.wsgi.openid.provider 10/03/09 |
---|
6 | """ |
---|
7 | __author__ = "P J Kershaw" |
---|
8 | __date__ = "01/08/08" |
---|
9 | __copyright__ = "(C) 2009 Science and Technology Facilities Council" |
---|
10 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
11 | __revision__ = "$Id$" |
---|
12 | __license__ = "BSD - see LICENSE file in top-level directory" |
---|
13 | import logging |
---|
14 | log = logging.getLogger(__name__) |
---|
15 | import httplib |
---|
16 | import cgi |
---|
17 | |
---|
18 | from ndg.security.server.wsgi.openid.provider import RenderingInterface |
---|
19 | |
---|
20 | quoteattr = lambda s: '"%s"' % cgi.escape(s, 1) |
---|
21 | |
---|
22 | class DemoRenderingInterface(RenderingInterface): |
---|
23 | """Example rendering interface class for demonstration purposes""" |
---|
24 | |
---|
25 | def identityPage(self, environ, start_response): |
---|
26 | """Render the identity page. |
---|
27 | |
---|
28 | @type environ: dict |
---|
29 | @param environ: dictionary of environment variables |
---|
30 | @type start_response: callable |
---|
31 | @param start_response: WSGI start response function. Should be called |
---|
32 | from this method to set the response code and HTTP header content |
---|
33 | @rtype: basestring |
---|
34 | @return: WSGI response |
---|
35 | """ |
---|
36 | path = environ.get('PATH_INFO').rstrip('/') |
---|
37 | userIdentifier = path.split('/')[ - 1] |
---|
38 | |
---|
39 | link_tag = '<link rel="openid.server" href="%s">' % \ |
---|
40 | self.urls['url_openidserver'] |
---|
41 | |
---|
42 | yadis_loc_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \ |
---|
43 | (self.urls['url_yadis'] + '/' + userIdentifier) |
---|
44 | |
---|
45 | disco_tags = link_tag + yadis_loc_tag |
---|
46 | ident = self.base_url + path |
---|
47 | |
---|
48 | response = self._showPage(environ, |
---|
49 | 'Identity Page', |
---|
50 | head_extras=disco_tags, |
---|
51 | msg='<p>This is the identity page for %s.' |
---|
52 | '</p>' % ident) |
---|
53 | |
---|
54 | start_response("200 OK", |
---|
55 | [('Content-type', 'text/html' + self.charset), |
---|
56 | ('Content-length', str(len(response)))]) |
---|
57 | return response |
---|
58 | |
---|
59 | |
---|
60 | def login(self, environ, start_response, |
---|
61 | success_to=None, fail_to=None, msg=''): |
---|
62 | """Render the login form. |
---|
63 | |
---|
64 | @type environ: dict |
---|
65 | @param environ: dictionary of environment variables |
---|
66 | @type success_to: basestring |
---|
67 | @param success_to: URL put into hidden field telling |
---|
68 | OpenIDProviderMiddleware.do_loginsubmit() where to forward to on |
---|
69 | successful login |
---|
70 | @type fail_to: basestring |
---|
71 | @param fail_to: URL put into hidden field telling |
---|
72 | OpenIDProviderMiddleware.do_loginsubmit() where to forward to on |
---|
73 | login error |
---|
74 | @type msg: basestring |
---|
75 | @param msg: display (error) message below login form e.g. following |
---|
76 | previous failed login attempt. |
---|
77 | @rtype: basestring |
---|
78 | @return: WSGI response |
---|
79 | """ |
---|
80 | |
---|
81 | if success_to is None: |
---|
82 | success_to = self.urls['url_mainpage'] |
---|
83 | |
---|
84 | if fail_to is None: |
---|
85 | fail_to = self.urls['url_mainpage'] |
---|
86 | |
---|
87 | form = '''\ |
---|
88 | <h2>Login</h2> |
---|
89 | <form method="GET" action="%s"> |
---|
90 | <input type="hidden" name="success_to" value="%s" /> |
---|
91 | <input type="hidden" name="fail_to" value="%s" /> |
---|
92 | <table cellspacing="0" border="0" cellpadding="5"> |
---|
93 | <tr> |
---|
94 | <td>Username:</td> |
---|
95 | <td><input type="text" name="username" value=""/></td> |
---|
96 | </tr><tr> |
---|
97 | <td>Password:</td> |
---|
98 | <td><input type="password" name="password"/></td> |
---|
99 | </tr><tr> |
---|
100 | <td colspan="2" align="right"> |
---|
101 | <input type="submit" name="submit" value="Login"/> |
---|
102 | <input type="submit" name="cancel" value="Cancel"/> |
---|
103 | </td> |
---|
104 | </tr> |
---|
105 | </table> |
---|
106 | </form> |
---|
107 | %s |
---|
108 | ''' % (self.urls['url_loginsubmit'], success_to, fail_to, msg) |
---|
109 | |
---|
110 | response = self._showPage(environ, 'Login Page', form=form) |
---|
111 | start_response('200 OK', |
---|
112 | [('Content-type', 'text/html' + self.charset), |
---|
113 | ('Content-length', str(len(response)))]) |
---|
114 | return response |
---|
115 | |
---|
116 | |
---|
117 | def mainPage(self, environ, start_response): |
---|
118 | """Rendering the main page. |
---|
119 | |
---|
120 | @type environ: dict |
---|
121 | @param environ: dictionary of environment variables |
---|
122 | @type start_response: callable |
---|
123 | @param start_response: WSGI start response function. Should be called |
---|
124 | from this method to set the response code and HTTP header content |
---|
125 | @rtype: basestring |
---|
126 | @return: WSGI response |
---|
127 | """ |
---|
128 | |
---|
129 | yadis_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \ |
---|
130 | self.urls['url_serveryadis'] |
---|
131 | username = environ['beaker.session'].get('username') |
---|
132 | if username: |
---|
133 | openid_url = self.urls['url_id'] + '/' + username |
---|
134 | user_message = """\ |
---|
135 | <p>You are logged in as %s. Your OpenID identity URL is |
---|
136 | <tt><a href=%s>%s</a></tt>. Enter that URL at an OpenID |
---|
137 | consumer to test this server.</p> |
---|
138 | """ % (username, quoteattr(openid_url), openid_url) |
---|
139 | else: |
---|
140 | user_message = "<p>You are not <a href='%s'>logged in</a>.</p>" % \ |
---|
141 | self.urls['url_login'] |
---|
142 | |
---|
143 | msg = '''\ |
---|
144 | <p>OpenID server</p> |
---|
145 | |
---|
146 | %s |
---|
147 | |
---|
148 | <p>The URL for this server is <a href=%s><tt>%s</tt></a>.</p> |
---|
149 | ''' % (user_message, quoteattr(self.base_url), self.base_url) |
---|
150 | response = self._showPage(environ, |
---|
151 | 'Main Page', |
---|
152 | head_extras=yadis_tag, |
---|
153 | msg=msg) |
---|
154 | |
---|
155 | start_response('200 OK', |
---|
156 | [('Content-type', 'text/html' + self.charset), |
---|
157 | ('Content-length', str(len(response)))]) |
---|
158 | return response |
---|
159 | |
---|
160 | |
---|
161 | def decidePage(self, environ, start_response, oidRequest, oidResponse): |
---|
162 | """Show page giving the user the option to approve the return of their |
---|
163 | credentials to the Relying Party. This page is also displayed for |
---|
164 | ID select mode if the user is already logged in at the OpenID Provider. |
---|
165 | This enables them to confirm the OpenID to be sent back to the |
---|
166 | Relying Party |
---|
167 | |
---|
168 | @type environ: dict |
---|
169 | @param environ: dictionary of environment variables |
---|
170 | @type start_response: callable |
---|
171 | @param start_response: WSGI start response function. Should be called |
---|
172 | from this method to set the response code and HTTP header content |
---|
173 | @type oidRequest: openid.server.server.CheckIDRequest |
---|
174 | @param oidRequest: OpenID Check ID Request object |
---|
175 | @type oidResponse: openid.server.server.OpenIDResponse |
---|
176 | @param oidResponse: OpenID response object |
---|
177 | @rtype: basestring |
---|
178 | @return: WSGI response |
---|
179 | """ |
---|
180 | idURLBase = self.urls['url_id'] + '/' |
---|
181 | |
---|
182 | # XXX: This may break if there are any synonyms for idURLBase, |
---|
183 | # such as referring to it by IP address or a CNAME. |
---|
184 | |
---|
185 | # TODO: OpenID 2.0 Allows oidRequest.identity to be set to |
---|
186 | # http://specs.openid.net/auth/2.0/identifier_select. See, |
---|
187 | # http://openid.net/specs/openid-authentication-2_0.html. This code |
---|
188 | # implements this overriding the behaviour of the example code on |
---|
189 | # which this is based. - Check is the example code based on OpenID 1.0 |
---|
190 | # and therefore wrong for this behaviour? |
---|
191 | # assert oidRequest.identity.startswith(idURLBase), \ |
---|
192 | # repr((oidRequest.identity, idURLBase)) |
---|
193 | userIdentifier = oidRequest.identity[len(idURLBase):] |
---|
194 | username = environ['beaker.session']['username'] |
---|
195 | |
---|
196 | if oidRequest.idSelect(): # We are being asked to select an ID |
---|
197 | userIdentifier = self._authN.username2UserIdentifiers(environ, |
---|
198 | username)[0] |
---|
199 | identity = idURLBase + userIdentifier |
---|
200 | |
---|
201 | msg = '''\ |
---|
202 | <p>A site has asked for your identity. You may select an |
---|
203 | identifier by which you would like this site to know you. |
---|
204 | On a production site this would likely be a drop down list |
---|
205 | of pre-created accounts or have the facility to generate |
---|
206 | a random anonymous identifier. |
---|
207 | </p> |
---|
208 | ''' |
---|
209 | fdata = { |
---|
210 | 'pathAllow': self.urls['url_allow'], |
---|
211 | 'identity': identity, |
---|
212 | 'trust_root': oidRequest.trust_root, |
---|
213 | } |
---|
214 | form = '''\ |
---|
215 | <form method="POST" action="%(pathAllow)s"> |
---|
216 | <table> |
---|
217 | <tr><td>Identity:</td> |
---|
218 | <td>%(identity)s</td></tr> |
---|
219 | <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr> |
---|
220 | </table> |
---|
221 | <p>Allow this authentication to proceed?</p> |
---|
222 | <input type="checkbox" id="remember" name="remember" value="Yes" |
---|
223 | /><label for="remember">Remember this |
---|
224 | decision</label><br /> |
---|
225 | <input type="hidden" name="identity" value="%(identity)s" /> |
---|
226 | <input type="submit" name="Yes" value="Yes" /> |
---|
227 | <input type="submit" name="No" value="No" /> |
---|
228 | </form> |
---|
229 | ''' % fdata |
---|
230 | |
---|
231 | elif userIdentifier in self._authN.username2UserIdentifiers(environ, |
---|
232 | username): |
---|
233 | msg = '''\ |
---|
234 | <p>A new site has asked to confirm your identity. If you |
---|
235 | approve, the site represented by the trust root below will |
---|
236 | be told that you control identity URL listed below. (If |
---|
237 | you are using a delegated identity, the site will take |
---|
238 | care of reversing the delegation on its own.)</p>''' |
---|
239 | |
---|
240 | fdata = { |
---|
241 | 'pathAllow': self.urls['url_allow'], |
---|
242 | 'identity': oidRequest.identity, |
---|
243 | 'trust_root': oidRequest.trust_root, |
---|
244 | } |
---|
245 | form = '''\ |
---|
246 | <table> |
---|
247 | <tr><td>Identity:</td><td>%(identity)s</td></tr> |
---|
248 | <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr> |
---|
249 | </table> |
---|
250 | <p>Allow this authentication to proceed?</p> |
---|
251 | <form method="POST" action="%(pathAllow)s"> |
---|
252 | <input type="checkbox" id="remember" name="remember" value="Yes" |
---|
253 | /><label for="remember">Remember this |
---|
254 | decision</label><br /> |
---|
255 | <input type="submit" name="Yes" value="Yes" /> |
---|
256 | <input type="submit" name="No" value="No" /> |
---|
257 | </form>''' % fdata |
---|
258 | else: |
---|
259 | mdata = { |
---|
260 | 'userIdentifier': userIdentifier, |
---|
261 | 'username': username, |
---|
262 | } |
---|
263 | msg = '''\ |
---|
264 | <p>A site has asked for an identity belonging to |
---|
265 | %(userIdentifier)s, but you are logged in as %(username)s. To |
---|
266 | log in as %(userIdentifier)s and approve the login oidRequest, |
---|
267 | hit OK below. The "Remember this decision" checkbox |
---|
268 | applies only to the trust root decision.</p>''' % mdata |
---|
269 | |
---|
270 | fdata = { |
---|
271 | 'pathAllow': self.urls['url_allow'], |
---|
272 | 'identity': oidRequest.identity, |
---|
273 | 'trust_root': oidRequest.trust_root, |
---|
274 | 'username': username, |
---|
275 | } |
---|
276 | form = '''\ |
---|
277 | <table> |
---|
278 | <tr><td>Identity:</td><td>%(identity)s</td></tr> |
---|
279 | <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr> |
---|
280 | </table> |
---|
281 | <p>Allow this authentication to proceed?</p> |
---|
282 | <form method="POST" action="%(pathAllow)s"> |
---|
283 | <input type="checkbox" id="remember" name="remember" value="Yes" |
---|
284 | /><label for="remember">Remember this |
---|
285 | decision</label><br /> |
---|
286 | <input type="hidden" name="login_as" value="%(username)s"/> |
---|
287 | <input type="submit" name="Yes" value="Yes" /> |
---|
288 | <input type="submit" name="No" value="No" /> |
---|
289 | </form>''' % fdata |
---|
290 | |
---|
291 | response = self._showPage(environ, 'Approve OpenID request?', |
---|
292 | msg=msg, form=form) |
---|
293 | start_response('200 OK', |
---|
294 | [('Content-type', 'text/html' + self.charset), |
---|
295 | ('Content-length', str(len(response)))]) |
---|
296 | return response |
---|
297 | |
---|
298 | |
---|
299 | def _showPage(self, |
---|
300 | environ, |
---|
301 | title, |
---|
302 | head_extras='', |
---|
303 | msg=None, |
---|
304 | err=None, |
---|
305 | form=None): |
---|
306 | """Generic page rendering method. Derived classes may ignore this. |
---|
307 | |
---|
308 | @type environ: dict |
---|
309 | @param environ: dictionary of environment variables |
---|
310 | @type title: basestring |
---|
311 | @param title: page title |
---|
312 | @type head_extras: basestring |
---|
313 | @param head_extras: add extra HTML header elements |
---|
314 | @type msg: basestring |
---|
315 | @param msg: optional message for page body |
---|
316 | @type err: basestring |
---|
317 | @param err: optional error message for page body |
---|
318 | @type form: basestring |
---|
319 | @param form: optional form for page body |
---|
320 | @rtype: basestring |
---|
321 | @return: WSGI response |
---|
322 | """ |
---|
323 | |
---|
324 | username = environ['beaker.session'].get('username') |
---|
325 | if username is None: |
---|
326 | user_link = '<a href="/login">not logged in</a>.' |
---|
327 | else: |
---|
328 | user_link = 'logged in as <a href="%s/%s">%s</a>.<br />'\ |
---|
329 | '<a href="%s?submit=true&'\ |
---|
330 | 'success_to=%s">Log out</a>' % \ |
---|
331 | (self.urls['url_id'], username, username, |
---|
332 | self.urls['url_loginsubmit'], |
---|
333 | self.urls['url_login']) |
---|
334 | |
---|
335 | body = '' |
---|
336 | |
---|
337 | if err is not None: |
---|
338 | body += '''\ |
---|
339 | <div class="error"> |
---|
340 | %s |
---|
341 | </div> |
---|
342 | ''' % err |
---|
343 | |
---|
344 | if msg is not None: |
---|
345 | body += '''\ |
---|
346 | <div class="message"> |
---|
347 | %s |
---|
348 | </div> |
---|
349 | ''' % msg |
---|
350 | |
---|
351 | if form is not None: |
---|
352 | body += '''\ |
---|
353 | <div class="form"> |
---|
354 | %s |
---|
355 | </div> |
---|
356 | ''' % form |
---|
357 | |
---|
358 | contents = { |
---|
359 | 'title': 'Python OpenID Provider - ' + title, |
---|
360 | 'head_extras': head_extras, |
---|
361 | 'body': body, |
---|
362 | 'user_link': user_link, |
---|
363 | } |
---|
364 | |
---|
365 | response = '''<html> |
---|
366 | <head> |
---|
367 | <title>%(title)s</title> |
---|
368 | %(head_extras)s |
---|
369 | </head> |
---|
370 | <style type="text/css"> |
---|
371 | h1 a:link { |
---|
372 | color: black; |
---|
373 | text-decoration: none; |
---|
374 | } |
---|
375 | h1 a:visited { |
---|
376 | color: black; |
---|
377 | text-decoration: none; |
---|
378 | } |
---|
379 | h1 a:hover { |
---|
380 | text-decoration: underline; |
---|
381 | } |
---|
382 | body { |
---|
383 | font-family: verdana,sans-serif; |
---|
384 | width: 50em; |
---|
385 | margin: 1em; |
---|
386 | } |
---|
387 | div { |
---|
388 | padding: .5em; |
---|
389 | } |
---|
390 | table { |
---|
391 | margin: none; |
---|
392 | padding: none; |
---|
393 | } |
---|
394 | .banner { |
---|
395 | padding: none 1em 1em 1em; |
---|
396 | width: 100%%; |
---|
397 | } |
---|
398 | .leftbanner { |
---|
399 | text-align: left; |
---|
400 | } |
---|
401 | .rightbanner { |
---|
402 | text-align: right; |
---|
403 | font-size: smaller; |
---|
404 | } |
---|
405 | .error { |
---|
406 | border: 1px solid #ff0000; |
---|
407 | background: #ffaaaa; |
---|
408 | margin: .5em; |
---|
409 | } |
---|
410 | .message { |
---|
411 | border: 1px solid #2233ff; |
---|
412 | background: #eeeeff; |
---|
413 | margin: .5em; |
---|
414 | } |
---|
415 | .form { |
---|
416 | border: 1px solid #777777; |
---|
417 | background: #ddddcc; |
---|
418 | margin: .5em; |
---|
419 | margin-top: 1em; |
---|
420 | padding-bottom: 0em; |
---|
421 | } |
---|
422 | dd { |
---|
423 | margin-bottom: 0.5em; |
---|
424 | } |
---|
425 | </style> |
---|
426 | <body> |
---|
427 | <table class="banner"> |
---|
428 | <tr> |
---|
429 | <td class="leftbanner"> |
---|
430 | <h1><a href="/">Python OpenID Provider</a></h1> |
---|
431 | </td> |
---|
432 | <td class="rightbanner"> |
---|
433 | You are %(user_link)s |
---|
434 | </td> |
---|
435 | </tr> |
---|
436 | </table> |
---|
437 | %(body)s |
---|
438 | </body> |
---|
439 | </html> |
---|
440 | ''' % contents |
---|
441 | |
---|
442 | return response |
---|
443 | |
---|
444 | def errorPage(self, environ, start_response, msg, code=500): |
---|
445 | """Display error page |
---|
446 | |
---|
447 | @type environ: dict |
---|
448 | @param environ: dictionary of environment variables |
---|
449 | @type start_response: callable |
---|
450 | @param start_response: WSGI start response function. Should be called |
---|
451 | from this method to set the response code and HTTP header content |
---|
452 | @type msg: basestring |
---|
453 | @param msg: optional message for page body |
---|
454 | @rtype: basestring |
---|
455 | @return: WSGI response |
---|
456 | """ |
---|
457 | |
---|
458 | response = self._showPage(environ, 'Error Processing Request', err='''\ |
---|
459 | <p>%s</p> |
---|
460 | <!-- |
---|
461 | |
---|
462 | This is a large comment. It exists to make this page larger. |
---|
463 | That is unfortunately necessary because of the "smart" |
---|
464 | handling of pages returned with an error code in IE. |
---|
465 | |
---|
466 | ************************************************************* |
---|
467 | ************************************************************* |
---|
468 | ************************************************************* |
---|
469 | ************************************************************* |
---|
470 | ************************************************************* |
---|
471 | ************************************************************* |
---|
472 | ************************************************************* |
---|
473 | ************************************************************* |
---|
474 | ************************************************************* |
---|
475 | ************************************************************* |
---|
476 | ************************************************************* |
---|
477 | ************************************************************* |
---|
478 | ************************************************************* |
---|
479 | ************************************************************* |
---|
480 | ************************************************************* |
---|
481 | ************************************************************* |
---|
482 | ************************************************************* |
---|
483 | ************************************************************* |
---|
484 | ************************************************************* |
---|
485 | ************************************************************* |
---|
486 | ************************************************************* |
---|
487 | ************************************************************* |
---|
488 | ************************************************************* |
---|
489 | |
---|
490 | --> |
---|
491 | ''' % msg) |
---|
492 | |
---|
493 | start_response('%d %s' % (code, httplib.responses[code]), |
---|
494 | [('Content-type', 'text/html' + self.charset), |
---|
495 | ('Content-length', str(len(response)))]) |
---|
496 | return response |
---|