1 | """NDG Security Genshi based Rendering Interface for |
---|
2 | OpenIDProviderMiddleware |
---|
3 | |
---|
4 | NERC Data Grid Project |
---|
5 | """ |
---|
6 | __author__ = "P J Kershaw" |
---|
7 | __date__ = "14/08/08" |
---|
8 | __copyright__ = "(C) 2009 Science and Technology Facilities Council" |
---|
9 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
10 | __revision__ = "$Id: $" |
---|
11 | __license__ = "BSD - see LICENSE file in top-level directory" |
---|
12 | import logging |
---|
13 | log = logging.getLogger(__name__) |
---|
14 | |
---|
15 | import httplib |
---|
16 | from os import path |
---|
17 | |
---|
18 | from genshi.template import TemplateLoader |
---|
19 | from openid.consumer import discover |
---|
20 | from openid.server.server import CheckIDRequest, OpenIDResponse |
---|
21 | from openid.extensions import ax |
---|
22 | |
---|
23 | # Rendering classes for OpenID Provider must derive from generic render |
---|
24 | # interface |
---|
25 | from ndg.security.server.wsgi.openid.provider import (RenderingInterface, |
---|
26 | RenderingInterfaceConfigError) |
---|
27 | |
---|
28 | from ndg.security.server.wsgi.openid.provider import OpenIDProviderMiddleware |
---|
29 | |
---|
30 | |
---|
31 | class GenshiRendering(RenderingInterface): |
---|
32 | """Provide Templating for OpenID Provider Middleware using Genshi templating |
---|
33 | """ |
---|
34 | PROPERTY_NAMES = ( |
---|
35 | 'templateRootDir', |
---|
36 | 'baseURL', |
---|
37 | 'leftLogo', |
---|
38 | 'leftAlt', |
---|
39 | 'leftLink', |
---|
40 | 'leftImage', |
---|
41 | 'rightLink', |
---|
42 | 'rightImage', |
---|
43 | 'rightAlt', |
---|
44 | 'footerText', |
---|
45 | 'helpIcon' |
---|
46 | ) |
---|
47 | ATTR_NAMES = ( |
---|
48 | 'title', |
---|
49 | 'heading', |
---|
50 | 'xml', |
---|
51 | 'headExtras', |
---|
52 | 'loginStatus', |
---|
53 | 'loader', |
---|
54 | 'session', |
---|
55 | 'success_to', |
---|
56 | 'fail_to', |
---|
57 | 'trust_root', |
---|
58 | 'environ', |
---|
59 | 'identityURI', |
---|
60 | 'oidRequest', |
---|
61 | 'oidResponse' |
---|
62 | ) |
---|
63 | __slots__ = tuple(["__%s" % name for name in ATTR_NAMES]) |
---|
64 | del name |
---|
65 | __slots__ += PROPERTY_NAMES |
---|
66 | |
---|
67 | LOGIN_TMPL_NAME = 'login.html' |
---|
68 | DECIDE_PAGE_TMPL_NAME = 'decide.html' |
---|
69 | MAIN_PAGE_TMPL_NAME = 'main.html' |
---|
70 | ERROR_PAGE_TMPL_NAME = 'error.html' |
---|
71 | |
---|
72 | # Approve and reject submit HTML input types for the Relying Party Approval |
---|
73 | # page |
---|
74 | APPROVE_RP_SUBMIT = OpenIDProviderMiddleware.APPROVE_RP_SUBMIT |
---|
75 | REJECT_RP_SUBMIT = OpenIDProviderMiddleware.REJECT_RP_SUBMIT |
---|
76 | |
---|
77 | DEFAULT_TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates') |
---|
78 | |
---|
79 | def __init__(self, *arg, **opt): |
---|
80 | '''Extend RenderingInterface to include config and set-up for Genshi |
---|
81 | templating |
---|
82 | |
---|
83 | @type *arg: tuple |
---|
84 | @param *arg: RenderingInterface parent class arguments |
---|
85 | @type **opt: dict |
---|
86 | @param **opt: additional keywords to set-up Genshi rendering''' |
---|
87 | super(GenshiRendering, self).__init__(*arg, **opt) |
---|
88 | |
---|
89 | # Initialise attributes |
---|
90 | for i in GenshiRendering.PROPERTY_NAMES: |
---|
91 | setattr(self, i, '') |
---|
92 | |
---|
93 | # Update from keywords |
---|
94 | for i in opt: |
---|
95 | setattr(self, i, opt[i]) |
---|
96 | |
---|
97 | if not self.templateRootDir: |
---|
98 | self.templateRootDir = GenshiRendering.DEFAULT_TEMPLATES_DIR |
---|
99 | |
---|
100 | self.__loader = TemplateLoader(self.templateRootDir, auto_reload=True) |
---|
101 | |
---|
102 | self.title = '' |
---|
103 | self.heading = '' |
---|
104 | self.xml = '' |
---|
105 | self.headExtras = '' |
---|
106 | self.loginStatus = True |
---|
107 | self.session = '' |
---|
108 | self.success_to = '' |
---|
109 | self.fail_to = '' |
---|
110 | |
---|
111 | self.__oidRequest = None |
---|
112 | self.__oidResponse = None |
---|
113 | self.__identityURI = None |
---|
114 | self.__environ = None |
---|
115 | self.__trust_root = None |
---|
116 | |
---|
117 | def getEnviron(self): |
---|
118 | return self.__environ |
---|
119 | |
---|
120 | def getIdentityURI(self): |
---|
121 | return self.__identityURI |
---|
122 | |
---|
123 | def setEnviron(self, value): |
---|
124 | self.__environ = value |
---|
125 | |
---|
126 | def setIdentityURI(self, value): |
---|
127 | self.__identityURI = value |
---|
128 | |
---|
129 | def getTrust_root(self): |
---|
130 | return self.__trust_root |
---|
131 | |
---|
132 | def getOidRequest(self): |
---|
133 | return self.__oidRequest |
---|
134 | |
---|
135 | def getOidResponse(self): |
---|
136 | return self.__oidResponse |
---|
137 | |
---|
138 | def setTrust_root(self, value): |
---|
139 | if not isinstance(value, basestring): |
---|
140 | raise TypeError('Expecting string type for trust_root attribute; ' |
---|
141 | 'got %r' % type(value)) |
---|
142 | self.__trust_root = value |
---|
143 | |
---|
144 | def setOidRequest(self, value): |
---|
145 | if not isinstance(value, CheckIDRequest): |
---|
146 | raise TypeError('Expecting %r type for oidRequest attribute; ' |
---|
147 | 'got %r' % (CheckIDRequest, type(value))) |
---|
148 | self.__oidRequest = value |
---|
149 | |
---|
150 | def setOidResponse(self, value): |
---|
151 | if not isinstance(value, OpenIDResponse): |
---|
152 | raise TypeError('Expecting %r type for oidResponse attribute; ' |
---|
153 | 'got %r' % (OpenIDResponse, type(value))) |
---|
154 | self.__oidResponse = value |
---|
155 | |
---|
156 | def getSuccess_to(self): |
---|
157 | return self.__success_to |
---|
158 | |
---|
159 | def getFail_to(self): |
---|
160 | return self.__fail_to |
---|
161 | |
---|
162 | def setSuccess_to(self, value): |
---|
163 | if not isinstance(value, basestring): |
---|
164 | raise TypeError('Expecting string type for success_to attribute; ' |
---|
165 | 'got %r' % type(value)) |
---|
166 | self.__success_to = value |
---|
167 | |
---|
168 | def setFail_to(self, value): |
---|
169 | if not isinstance(value, basestring): |
---|
170 | raise TypeError('Expecting string type for fail_to attribute; ' |
---|
171 | 'got %r' % type(value)) |
---|
172 | self.__fail_to = value |
---|
173 | |
---|
174 | def getTitle(self): |
---|
175 | return self.__title |
---|
176 | |
---|
177 | def getHeading(self): |
---|
178 | return self.__heading |
---|
179 | |
---|
180 | def getXml(self): |
---|
181 | return self.__xml |
---|
182 | |
---|
183 | def getHeadExtras(self): |
---|
184 | return self.__headExtras |
---|
185 | |
---|
186 | def getLoginStatus(self): |
---|
187 | return self.__loginStatus |
---|
188 | |
---|
189 | def getSession(self): |
---|
190 | return self.__session |
---|
191 | |
---|
192 | def setTitle(self, value): |
---|
193 | if not isinstance(value, basestring): |
---|
194 | raise TypeError('Expecting string type for title attribute; ' |
---|
195 | 'got %r' % type(value)) |
---|
196 | self.__title = value |
---|
197 | |
---|
198 | def setHeading(self, value): |
---|
199 | if not isinstance(value, basestring): |
---|
200 | raise TypeError('Expecting string type for heading attribute; ' |
---|
201 | 'got %r' % type(value)) |
---|
202 | self.__heading = value |
---|
203 | |
---|
204 | def setXml(self, value): |
---|
205 | if not isinstance(value, basestring): |
---|
206 | raise TypeError('Expecting string type for xml attribute; ' |
---|
207 | 'got %r' % type(value)) |
---|
208 | self.__xml = value |
---|
209 | |
---|
210 | def setHeadExtras(self, value): |
---|
211 | if not isinstance(value, basestring): |
---|
212 | raise TypeError('Expecting string type for headExtras attribute; ' |
---|
213 | 'got %r' % type(value)) |
---|
214 | self.__headExtras = value |
---|
215 | |
---|
216 | def setLoginStatus(self, value): |
---|
217 | if not isinstance(value, bool): |
---|
218 | raise TypeError('Expecting bool type for loginStatus attribute; ' |
---|
219 | 'got %r' % type(value)) |
---|
220 | self.__loginStatus = value |
---|
221 | |
---|
222 | def setSession(self, value): |
---|
223 | self.__session = value |
---|
224 | |
---|
225 | title = property(getTitle, setTitle, None, "Template title") |
---|
226 | |
---|
227 | heading = property(getHeading, setHeading, None, "Template heading") |
---|
228 | |
---|
229 | xml = property(getXml, setXml, None, "Additional XML for template") |
---|
230 | |
---|
231 | headExtras = property(getHeadExtras, setHeadExtras, None, |
---|
232 | "additional head info for template") |
---|
233 | |
---|
234 | loginStatus = property(getLoginStatus, setLoginStatus, None, |
---|
235 | "Login Status boolean") |
---|
236 | |
---|
237 | session = property(getSession, setSession, None, |
---|
238 | "Beaker session") |
---|
239 | |
---|
240 | success_to = property(getSuccess_to, setSuccess_to, None, |
---|
241 | "URL following successful login") |
---|
242 | |
---|
243 | fail_to = property(getFail_to, setFail_to, None, |
---|
244 | "URL following an error with login") |
---|
245 | |
---|
246 | def __setattr__(self, name, value): |
---|
247 | """Apply some generic type checking""" |
---|
248 | if name in GenshiRendering.PROPERTY_NAMES: |
---|
249 | if not isinstance(value, basestring): |
---|
250 | raise TypeError('Expecting string type for %r attribute; got ' |
---|
251 | '%r' % (name, type(value))) |
---|
252 | |
---|
253 | super(GenshiRendering, self).__setattr__(name, value) |
---|
254 | |
---|
255 | def _getLoader(self): |
---|
256 | return self.__loader |
---|
257 | |
---|
258 | def _setLoader(self, value): |
---|
259 | if not isinstance(value, TemplateLoader): |
---|
260 | raise TypeError('Expecting %r type for "loader"; got %r' % |
---|
261 | (TemplateLoader, type(value))) |
---|
262 | self.__loader = value |
---|
263 | |
---|
264 | loader = property(_getLoader, _setLoader, |
---|
265 | doc="Genshi TemplateLoader instance") |
---|
266 | |
---|
267 | def _render(self, templateName, c=None, **kw): |
---|
268 | '''Wrapper for Genshi template rendering |
---|
269 | @type templateName: basestring |
---|
270 | @param templateName: name of template file to load |
---|
271 | @type c: None/object |
---|
272 | @param c: reference to object to pass into template - defaults to self |
---|
273 | @type kw: dict |
---|
274 | @param kw: keywords to pass to template |
---|
275 | @rtype: string |
---|
276 | @return: rendered template |
---|
277 | ''' |
---|
278 | if c is None: |
---|
279 | c = self |
---|
280 | |
---|
281 | kw['c'] = c |
---|
282 | |
---|
283 | tmpl = self.loader.load(templateName) |
---|
284 | rendering = tmpl.generate(**kw).render('html', doctype='html') |
---|
285 | |
---|
286 | return rendering |
---|
287 | |
---|
288 | def yadis(self, environ, start_response): |
---|
289 | """Render Yadis document containing user URL - override base |
---|
290 | implementation to specify Yadis based discovery for user URL |
---|
291 | |
---|
292 | @type environ: dict |
---|
293 | @param environ: dictionary of environment variables |
---|
294 | @type start_response: callable |
---|
295 | @param start_response: WSGI start response function. Should be called |
---|
296 | from this method to set the response code and HTTP header content |
---|
297 | @rtype: basestring |
---|
298 | @return: WSGI response |
---|
299 | """ |
---|
300 | userIdentifier = OpenIDProviderMiddleware.parseIdentityURI( |
---|
301 | environ['PATH_INFO'])[-1] |
---|
302 | |
---|
303 | # This is where this implementation differs from the base class one |
---|
304 | user_url = OpenIDProviderMiddleware.createIdentityURI( |
---|
305 | self.urls['url_yadis'], |
---|
306 | userIdentifier) |
---|
307 | |
---|
308 | yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE, |
---|
309 | openid10type=discover.OPENID_1_0_TYPE, |
---|
310 | endpoint_url=self.urls['url_openidserver'], |
---|
311 | user_url=user_url) |
---|
312 | |
---|
313 | response = RenderingInterface.tmplYadis % yadisDict |
---|
314 | |
---|
315 | start_response('200 OK', |
---|
316 | [('Content-type', 'application/xrds+xml'+self.charset), |
---|
317 | ('Content-length', str(len(response)))]) |
---|
318 | return response |
---|
319 | |
---|
320 | def login(self, environ, start_response, success_to=None, fail_to=None, |
---|
321 | msg=''): |
---|
322 | """Set-up template for OpenID Provider Login""" |
---|
323 | self.title = "OpenID Login" |
---|
324 | self.heading = "Login" |
---|
325 | self.success_to = success_to or self.urls['url_mainpage'] |
---|
326 | self.fail_to = fail_to or self.urls['url_mainpage'] |
---|
327 | self.xml = msg |
---|
328 | |
---|
329 | response = self._render(GenshiRendering.LOGIN_TMPL_NAME) |
---|
330 | start_response('200 OK', |
---|
331 | [('Content-type', 'text/html'+self.charset), |
---|
332 | ('Content-length', str(len(response)))]) |
---|
333 | self.xml = '' |
---|
334 | return response |
---|
335 | |
---|
336 | def mainPage(self, environ, start_response): |
---|
337 | """Set-up template for OpenID Provider Login""" |
---|
338 | self.title = "OpenID Provider" |
---|
339 | self.heading = "OpenID Provider" |
---|
340 | self.headExtras = '<meta http-equiv="x-xrds-location" content="%s"/>'%\ |
---|
341 | self.urls['url_serveryadis'] |
---|
342 | |
---|
343 | response = self._render(GenshiRendering.MAIN_PAGE_TMPL_NAME) |
---|
344 | start_response('200 OK', |
---|
345 | [('Content-type', 'text/html'+self.charset), |
---|
346 | ('Content-length', str(len(response)))]) |
---|
347 | return response |
---|
348 | |
---|
349 | def identityPage(self, environ, start_response): |
---|
350 | """This page would normally render the user's Identity page but it's |
---|
351 | not needed for Yadis only based discovery""" |
---|
352 | self.title = 'OpenID Provider - Error' |
---|
353 | self.heading = 'OpenID Provider - Invalid Page Requested' |
---|
354 | self.xml = 'Invalid page requested for OpenID Provider' |
---|
355 | response = self._render(GenshiRendering.ERROR_PAGE_TMPL_NAME) |
---|
356 | self.xml = '' |
---|
357 | start_response("404 Not Found", |
---|
358 | [('Content-type', 'text/html'+self.charset), |
---|
359 | ('Content-length', str(len(response)))]) |
---|
360 | return response |
---|
361 | |
---|
362 | def decidePage(self, environ, start_response, oidRequest, oidResponse): |
---|
363 | """Handle user interaction required before final submit back to Relying |
---|
364 | Party""" |
---|
365 | self.title = 'Approve OpenID Request?' |
---|
366 | self.heading = 'Approve OpenID Request?' |
---|
367 | self.trust_root = oidRequest.trust_root |
---|
368 | self.oidRequest = oidRequest |
---|
369 | |
---|
370 | # Get all the content namespaced as AX type |
---|
371 | axArgs = oidResponse.fields.getArgs(ax.AXMessage.ns_uri) |
---|
372 | |
---|
373 | # Add to access object for convenient access based on type URI |
---|
374 | axFetchResponse = ax.FetchResponse() |
---|
375 | axFetchResponse.parseExtensionArgs(axArgs) |
---|
376 | |
---|
377 | ax_req = ax.FetchRequest.fromOpenIDRequest(oidRequest) |
---|
378 | axRequestedAttr = ax_req.requested_attributes |
---|
379 | self.environ = environ |
---|
380 | |
---|
381 | if oidRequest.idSelect(): |
---|
382 | if 'username' not in self.session: |
---|
383 | log.error("No 'username' key set in session object for " |
---|
384 | "idselect mode do decide page") |
---|
385 | msg = ('An internal error has occurred. Please contact ' |
---|
386 | 'your system administrator') |
---|
387 | response = self.errorPage(environ, start_response, msg) |
---|
388 | return response |
---|
389 | |
---|
390 | userIdentifier = self._authN.username2UserIdentifiers( |
---|
391 | environ, |
---|
392 | self.session['username'])[0] |
---|
393 | |
---|
394 | # Use the Yadis path because we want to use Yadis only |
---|
395 | # based discovery |
---|
396 | self.identityURI = OpenIDProviderMiddleware.createIdentityURI( |
---|
397 | self.urls['url_yadis'], |
---|
398 | userIdentifier) |
---|
399 | else: |
---|
400 | self.identityURI = oidRequest.identity |
---|
401 | |
---|
402 | response = self._render(GenshiRendering.DECIDE_PAGE_TMPL_NAME, |
---|
403 | axRequestedAttr=axRequestedAttr, |
---|
404 | axFetchResponse=axFetchResponse) |
---|
405 | self.identityURI = '' |
---|
406 | |
---|
407 | start_response("200 OK", |
---|
408 | [('Content-type', 'text/html'+self.charset), |
---|
409 | ('Content-length', str(len(response)))]) |
---|
410 | return response |
---|
411 | |
---|
412 | def errorPage(self, environ, start_response, msg, code=500): |
---|
413 | '''Display error information''' |
---|
414 | self.title = 'Error with OpenID Provider' |
---|
415 | self.heading = 'Error' |
---|
416 | self.xml = msg |
---|
417 | response = self._render(GenshiRendering.ERROR_PAGE_TMPL_NAME) |
---|
418 | start_response('%d %s' % (code, httplib.responses[code]), |
---|
419 | [('Content-type', 'text/html'+self.charset), |
---|
420 | ('Content-length', str(len(response)))]) |
---|
421 | self.xml = '' |
---|
422 | return response |
---|
423 | |
---|
424 | trust_root = property(getTrust_root, setTrust_root, |
---|
425 | doc="trust_root - dict of user trusted RPs") |
---|
426 | |
---|
427 | oidRequest = property(getOidRequest, setOidRequest, |
---|
428 | doc="oidRequest - OpenID Request object") |
---|
429 | |
---|
430 | oidResponse = property(getOidResponse, setOidResponse, |
---|
431 | doc="oidRequest - OpenID Response object") |
---|
432 | |
---|
433 | environ = property(getEnviron, setEnviron, None, |
---|
434 | "WSGI environ dict") |
---|
435 | |
---|
436 | identityURI = property(getIdentityURI, setIdentityURI, |
---|
437 | doc="User OpenID URI") |
---|