1 | #!/usr/bin/env python |
---|
2 | |
---|
3 | """NDG Security CGI services |
---|
4 | |
---|
5 | NERC Data Grid Project |
---|
6 | |
---|
7 | P J Kershaw 23/05/06 |
---|
8 | |
---|
9 | Copyright (C) 2006 CCLRC & NERC |
---|
10 | |
---|
11 | This software may be distributed under the terms of the Q Public License, |
---|
12 | version 1.0 or later. |
---|
13 | """ |
---|
14 | from Cookie import SimpleCookie |
---|
15 | |
---|
16 | import sys |
---|
17 | import cgi |
---|
18 | import os |
---|
19 | |
---|
20 | from NDG.SecurityClient import * |
---|
21 | from NDG.Session import UserSession |
---|
22 | from NDG.Session import UserSessionError |
---|
23 | |
---|
24 | |
---|
25 | class SecurityCGIError(Exception): |
---|
26 | """Exception handling for NDG Security CGI class.""" |
---|
27 | |
---|
28 | def __init__(self, msg): |
---|
29 | self.__msg = msg |
---|
30 | |
---|
31 | def __str__(self): |
---|
32 | return self.__msg |
---|
33 | |
---|
34 | |
---|
35 | class SecurityCGI(cgi.FieldStorage): |
---|
36 | """CGI interface class for NDG Security |
---|
37 | |
---|
38 | Terms used throughout: |
---|
39 | remote site - where user is accessing resources |
---|
40 | home site - where user's credentials are held or they login""" |
---|
41 | |
---|
42 | #_________________________________________________________________________ |
---|
43 | def __init__(self, |
---|
44 | smWSDL, |
---|
45 | userName=None, |
---|
46 | passPhrase=None, |
---|
47 | smPubKeyFilePath=None, |
---|
48 | clntPubKeyFilePath=None, |
---|
49 | clntPriKeyFilePath=None, |
---|
50 | clntPriKeyPwd=None, |
---|
51 | aaPubKey=None, |
---|
52 | scriptName=None, |
---|
53 | returnURI=None, |
---|
54 | trustedHosts=None, |
---|
55 | wsDebug=False, |
---|
56 | **cgiFieldStorageKwArgs): |
---|
57 | """scriptName: name of script to call in forms - defaults to |
---|
58 | this file. Modify if you inherit from this |
---|
59 | class. |
---|
60 | smWSDL: URI For Session Manager WSDL used for user |
---|
61 | authentication |
---|
62 | returnURI: the address to redirect back to following a |
---|
63 | redirect |
---|
64 | to the user's home site to obtain their |
---|
65 | credentials |
---|
66 | trustedHosts: dictionary of URIs for trusted hosts indexed by |
---|
67 | hostname |
---|
68 | wsDebug: print output from WS transactions to stderr""" |
---|
69 | |
---|
70 | self.smWSDL = smWSDL |
---|
71 | self.userName = userName |
---|
72 | self.passPhrase = passPhrase |
---|
73 | self.smPubKeyFilePath = smPubKeyFilePath |
---|
74 | self.clntPubKeyFilePath = clntPubKeyFilePath |
---|
75 | self.clntPriKeyFilePath = clntPriKeyFilePath |
---|
76 | self.clntPriKeyPwd = clntPriKeyPwd |
---|
77 | self.__aaPubKey = aaPubKey |
---|
78 | |
---|
79 | if scriptName: |
---|
80 | self.scriptName = scriptName |
---|
81 | else: |
---|
82 | self.scriptName = __file__ |
---|
83 | |
---|
84 | self.returnURI = returnURI |
---|
85 | self.trustedHosts = trustedHosts |
---|
86 | self.__wsDebug = False |
---|
87 | self.__authorisationMethod = None |
---|
88 | |
---|
89 | cgi.FieldStorage.__init__(self, **cgiFieldStorageKwArgs) |
---|
90 | |
---|
91 | |
---|
92 | #_________________________________________________________________________ |
---|
93 | def processFields(self, **kwargs): |
---|
94 | """Call appropriate actions according to the fields set""" |
---|
95 | |
---|
96 | if 'requestURI' in self: |
---|
97 | # Request credentials from user's home site |
---|
98 | self.requestCreds(**kwargs) |
---|
99 | |
---|
100 | elif 'NDG-ID1' in self and 'NDG-ID2' in self and 'expires' in self: |
---|
101 | # Receive credentials back from home site and set a new cookie at |
---|
102 | # remote site |
---|
103 | self.receiveCredsResponse(self['NDG-ID1'].value, |
---|
104 | self['NDG-ID2'].value, |
---|
105 | self['NDG-ID2']['expires'], |
---|
106 | **kwargs) |
---|
107 | |
---|
108 | elif 'authenticate' in self: |
---|
109 | # User has logged on at home site and a cookie is now set - |
---|
110 | # next step is processCredsRequest() below |
---|
111 | sessCookie = self.authenticate() |
---|
112 | |
---|
113 | if 'returnURI' in self: |
---|
114 | # Authentication is required following a redirect from |
---|
115 | # another site - redirect back to the remote site returning |
---|
116 | # the cookie information |
---|
117 | self.processCredsRequest(sessCookie=sessCookie, |
---|
118 | setCookie=True, |
---|
119 | **kwargs) |
---|
120 | |
---|
121 | elif 'returnURI' in self: |
---|
122 | # Home site receives request from remote site for credentials and |
---|
123 | # returns them |
---|
124 | self.processCredsRequest(**kwargs) |
---|
125 | else: |
---|
126 | # Remote site presents possible sites for user to get their |
---|
127 | # credentials from |
---|
128 | self.showHomeSiteSelect(**kwargs) |
---|
129 | |
---|
130 | |
---|
131 | #_________________________________________________________________________ |
---|
132 | # Use instance name as an alias to processFields method |
---|
133 | __call__ = processFields |
---|
134 | |
---|
135 | |
---|
136 | #_________________________________________________________________________ |
---|
137 | def requestCreds(self, |
---|
138 | requestURI=None, |
---|
139 | pageTitle='', |
---|
140 | headTags='', |
---|
141 | delayTime=0, |
---|
142 | redirectMsg=''): |
---|
143 | """Request credentials from a user's home site |
---|
144 | |
---|
145 | requestURI: site to request credentials from - default is |
---|
146 | 'requestURI' CGI form value |
---|
147 | pageTitle: Give the redirect page a title |
---|
148 | headTags: Optionally add additional tags in <head/> section |
---|
149 | delayTime: time in seconds before carrying out redirect - redirect |
---|
150 | page will be displayed in this interval |
---|
151 | redirectMsg: Message to put on redirect page. Can be plain text or |
---|
152 | formatted HTML""" |
---|
153 | |
---|
154 | if requestURI is None: |
---|
155 | requestURI = self['requestURI'].value |
---|
156 | |
---|
157 | print """Content-type: text/html |
---|
158 | |
---|
159 | <html> |
---|
160 | <head> |
---|
161 | <title>%s</title> |
---|
162 | <meta http-equiv="REFRESH" content="%d; url=%s?returnURI=%s"> |
---|
163 | %s |
---|
164 | </head> |
---|
165 | <body> |
---|
166 | %s |
---|
167 | </body> |
---|
168 | </html>""" % \ |
---|
169 | (pageTitle, delayTime, requestURI, self.returnURI, headTags, redirectMsg) |
---|
170 | |
---|
171 | |
---|
172 | #_________________________________________________________________________ |
---|
173 | def receiveCredsResponse(self, |
---|
174 | sessID, |
---|
175 | sessMgrURI, |
---|
176 | expiry, |
---|
177 | pageTitle='', |
---|
178 | hdrTxt='', |
---|
179 | bodyTxt=''): |
---|
180 | """Remote site receives returned credentials and creates a new cookie |
---|
181 | for its domain""" |
---|
182 | |
---|
183 | sessCookie = UserSession.createSecurityCookie(sessID, |
---|
184 | sessMgrURI, |
---|
185 | expiryStr=expiry) |
---|
186 | |
---|
187 | print """Content-type: text/html" |
---|
188 | %s |
---|
189 | |
---|
190 | <html> |
---|
191 | <head> |
---|
192 | <title>%s</title> |
---|
193 | %s |
---|
194 | </head> |
---|
195 | <body> |
---|
196 | %s |
---|
197 | </body> |
---|
198 | </html>""" % (sessCookie.output(), pageTitle, hdrTxt, bodyTxt) |
---|
199 | |
---|
200 | |
---|
201 | #_________________________________________________________________________ |
---|
202 | def processCredsRequest(self, |
---|
203 | returnURI=None, |
---|
204 | sessCookie=None, |
---|
205 | **returnCredsKwArgs): |
---|
206 | """Receive request from remote site for credentials. Process and |
---|
207 | return via a redirect""" |
---|
208 | |
---|
209 | if returnURI is None: |
---|
210 | returnURI = self['returnURI'].value |
---|
211 | |
---|
212 | # Check for cookie in environment |
---|
213 | if 'HTTP_COOKIE' in os.environ: |
---|
214 | |
---|
215 | # Get session ID from existing cookie |
---|
216 | sessCookie = SimpleCookie(os.environ['HTTP_COOKIE']) |
---|
217 | |
---|
218 | |
---|
219 | if sessCookie: |
---|
220 | # Cookie is set - check for NDG cookie |
---|
221 | try: |
---|
222 | UserSession.isValidSecurityCookie(sessCookie, raiseExcep=True) |
---|
223 | |
---|
224 | except UserSessionError, e: |
---|
225 | raise SecurityCGIError, \ |
---|
226 | 'Checking existing session cookie: %s' % e |
---|
227 | |
---|
228 | # Return cookie to requestor |
---|
229 | self.returnCreds(sessCookie, returnURI, **returnCredsKwArgs) |
---|
230 | |
---|
231 | else: |
---|
232 | # No cookie present - display login. Submit must redirect back to |
---|
233 | # this script with '?authenticate=1&returnURI=<...>' |
---|
234 | self.showLogin(returnURI=returnURI, |
---|
235 | setCookie=True, |
---|
236 | contentTypeHdr=True, |
---|
237 | pageTitle="NDG Login", |
---|
238 | htmlTag=True, |
---|
239 | bodyTag=True) |
---|
240 | |
---|
241 | |
---|
242 | #_________________________________________________________________________ |
---|
243 | def returnCreds(self, |
---|
244 | sessCookie, |
---|
245 | returnURI=None, |
---|
246 | pageTitle='', |
---|
247 | hdrTxt='', |
---|
248 | delayTime=0, |
---|
249 | redirectMsg='', |
---|
250 | setCookie=False): |
---|
251 | """User's home site returns credentials to requestor via a HTTP |
---|
252 | redirect |
---|
253 | |
---|
254 | sessCookie: NDG Session cookie |
---|
255 | pageTitle: Give the redirect page a title |
---|
256 | headTags: Optionally add additional tags in <head/> section |
---|
257 | delayTime: time in seconds before carrying out redirect - redirect |
---|
258 | page will be displayed in this interval |
---|
259 | redirectMsg: Message to put on redirect page. Can be plain text or |
---|
260 | formatted HTML""" |
---|
261 | |
---|
262 | if returnURI is None: |
---|
263 | returnURI = self['returnURI'].value |
---|
264 | |
---|
265 | if setCookie: |
---|
266 | cookieTxt = sessCookie.output() + os.line_sep() |
---|
267 | else: |
---|
268 | cookieTxt = '' |
---|
269 | |
---|
270 | print """Content-type: text/html |
---|
271 | %s |
---|
272 | <html> |
---|
273 | <head> |
---|
274 | <title>%s</title> |
---|
275 | <meta http-equiv="REFRESH" |
---|
276 | content="%d; url=%s?NDG-ID1=%s&NDG-ID2=%s&expires=%s"> |
---|
277 | %s |
---|
278 | </head> |
---|
279 | <body> |
---|
280 | %s |
---|
281 | </body> |
---|
282 | </html>""" % ( cookieTxt, |
---|
283 | pageTitle, |
---|
284 | delayTime, |
---|
285 | returnURI, |
---|
286 | sessCookie['NDG-ID1'].value, |
---|
287 | sessCookie['NDG-ID2'].value, |
---|
288 | base64.b64encode(sessCookie['NDG-ID1']['expires']), |
---|
289 | redirectMsg) |
---|
290 | |
---|
291 | |
---|
292 | #_________________________________________________________________________ |
---|
293 | def authenticate(self, bAuthorise=False): |
---|
294 | """Authenticate username and passphrase input from preceeding login |
---|
295 | form |
---|
296 | |
---|
297 | bAuthorise: set to True so that if an error occurs, login will be |
---|
298 | recalled followed by authorisation""" |
---|
299 | |
---|
300 | if self.__wsDebug: |
---|
301 | traceFile = sys.stderr |
---|
302 | else: |
---|
303 | traceFile = None |
---|
304 | |
---|
305 | |
---|
306 | if 'userName' in self: |
---|
307 | self.userName = self['userName'].value |
---|
308 | |
---|
309 | if 'passPhrase' in self: |
---|
310 | self.passPhrase = self['passPhrase'].value |
---|
311 | |
---|
312 | |
---|
313 | if self.userName is None: |
---|
314 | raise SecurityCGIError("no username set for authentication") |
---|
315 | |
---|
316 | if self.passPhrase is None: |
---|
317 | raise SecurityCGIError("no pass-phrase set for authentication") |
---|
318 | |
---|
319 | |
---|
320 | # Instantiate WS proxy and request connection |
---|
321 | try: |
---|
322 | smClient = SessionClient( |
---|
323 | smWSDL=self.smWSDL, |
---|
324 | smPubKeyFilePath=self.smPubKeyFilePath, |
---|
325 | clntPubKeyFilePath=self.clntPubKeyFilePath, |
---|
326 | clntPriKeyFilePath=self.clntPriKeyFilePath, |
---|
327 | traceFile=traceFile) |
---|
328 | |
---|
329 | return smClient.connect(userName=self.userName, |
---|
330 | pPhrase=self.passPhrase, |
---|
331 | clntPriKeyPwd=self.clntPriKeyPwd) |
---|
332 | except Exception, e: |
---|
333 | raise SecurityCGIError("Session client: " + str(e)) |
---|
334 | |
---|
335 | |
---|
336 | #_________________________________________________________________________ |
---|
337 | def showLogin(self, |
---|
338 | returnURI=None, |
---|
339 | contentTypeHdr=False, |
---|
340 | htmlTag=False, |
---|
341 | pageTitle='', |
---|
342 | hdrTxt='', |
---|
343 | headTag=False, |
---|
344 | bodyTag=False, |
---|
345 | bAuthorise=False): |
---|
346 | """Display initial NDG login form""" |
---|
347 | |
---|
348 | if contentTypeHdr: print "Content-type: text/html\n\n" |
---|
349 | |
---|
350 | if htmlTag: print "<html>" |
---|
351 | |
---|
352 | if headTag: |
---|
353 | if not hdrTxt: |
---|
354 | hdrTxt = """ <style type=\"text/css\"> |
---|
355 | <!-- |
---|
356 | .al { |
---|
357 | text-align: justify |
---|
358 | } |
---|
359 | a{ |
---|
360 | text-decoration:none; |
---|
361 | } |
---|
362 | a:hover{ |
---|
363 | color:#0000FF; |
---|
364 | } |
---|
365 | body { font-family: Verdana, sans-serif; font-size: 11} |
---|
366 | table { font-family: Verdana, sans-serif; font-size: 11} |
---|
367 | --> |
---|
368 | </style>""" |
---|
369 | |
---|
370 | print """<head> |
---|
371 | <title>%s</title> |
---|
372 | %s |
---|
373 | </head>""" % (pageTitle, hdrTxt) |
---|
374 | |
---|
375 | |
---|
376 | if bodyTag: print "<body>" |
---|
377 | |
---|
378 | if returnURI: |
---|
379 | returnURIfield = \ |
---|
380 | "<input type=hidden name=returnURI value=\"%s\">" % returnURI |
---|
381 | else: |
---|
382 | returnURIfield = '' |
---|
383 | |
---|
384 | |
---|
385 | if bAuthorise: |
---|
386 | authoriseArg = "<input type=hidden name=authorise value=\"1\">" |
---|
387 | else: |
---|
388 | authoriseArg = "" |
---|
389 | |
---|
390 | |
---|
391 | # Set authorisation method default |
---|
392 | authorisationMethodChk = { "allowMapping": '', |
---|
393 | "allowMappingWithPrompt" : '', |
---|
394 | "noMapping": ''} |
---|
395 | |
---|
396 | if self.__authorisationMethod is None: |
---|
397 | # Default to safest option for user |
---|
398 | authorisationMethodChk["allowMappingWithPrompt"] = ' checked' |
---|
399 | else: |
---|
400 | authorisationMethodChk[self.__authorisationMethod] = ' checked' |
---|
401 | |
---|
402 | print \ |
---|
403 | """<script language="javascript"> |
---|
404 | <!-- |
---|
405 | function toggleLayer(layerId) |
---|
406 | { |
---|
407 | if (document.getElementById) |
---|
408 | { |
---|
409 | // Standard |
---|
410 | var style = document.getElementById(layerId).style; |
---|
411 | } |
---|
412 | else if (document.all) |
---|
413 | { |
---|
414 | // Old msie versions |
---|
415 | var style = document.all[whichLayer].style; |
---|
416 | } |
---|
417 | else if (document.layers) |
---|
418 | { |
---|
419 | // nn4 |
---|
420 | var style = document.layers[whichLayer].style; |
---|
421 | } |
---|
422 | style.visibility = style.visibility == "visible" ? "hidden":"visible"; |
---|
423 | } |
---|
424 | //--> |
---|
425 | </script> |
---|
426 | <h3>NERC Data Grid Site Login (Test)<BR clear=all></h3> |
---|
427 | <hr> |
---|
428 | |
---|
429 | <form action="%s" method="POST"> |
---|
430 | |
---|
431 | <table bgcolor=#ADD8E6 cellspacing=0 border=0 cellpadding=5> |
---|
432 | <tbody> |
---|
433 | <tr><td>User Name:</td> <td><input type=text name=userName value=""> |
---|
434 | </td></tr> |
---|
435 | <tr> |
---|
436 | <td>Password:</td> |
---|
437 | <td><input type=password name=passPhrase></td> |
---|
438 | </tr> |
---|
439 | <tr> |
---|
440 | <td colspan="2" align="right"> |
---|
441 | <a href="javascript:toggleLayer('advSettings');">Advanced Settings</a> |
---|
442 | <input type=submit value="Login"> |
---|
443 | </td> |
---|
444 | </tr> |
---|
445 | <input type=hidden name=authenticate value="1"> |
---|
446 | %s""" % (self.scriptName, returnURIfield) |
---|
447 | |
---|
448 | print \ |
---|
449 | """</tbody></table> |
---|
450 | <br> |
---|
451 | <div id="advSettings" style="position: relative; visibility: hidden;"> |
---|
452 | <h4>Role Mapping for access to other trusted sites</h4> |
---|
453 | <p>Your account has roles or <i>privileges</i> which determine what data you have access to. If you access data at another NDG trusted site, these roles can be mapped to local roles at that site to help you gain access: |
---|
454 | </p> |
---|
455 | <table bgcolor=#ADD8E6 cellspacing=0 border=0 cellpadding=5> |
---|
456 | <tbody> |
---|
457 | <tr> |
---|
458 | <td> |
---|
459 | <input type="radio" name="authorisationMethod" value="allowMapping"%s> |
---|
460 | </td> |
---|
461 | <td> |
---|
462 | Allow my roles to be mapped to local roles at other NDG trusted sites. |
---|
463 | </td> |
---|
464 | </tr> |
---|
465 | <tr> |
---|
466 | <td> |
---|
467 | <input type="radio" name="authorisationMethod" value="allowMappingWithPrompt"%s> |
---|
468 | </td> |
---|
469 | <td> |
---|
470 | Allow my roles to be mapped, but prompt me so that I may choose which roles to map before gaining access. |
---|
471 | </td> |
---|
472 | <tr> |
---|
473 | <td> |
---|
474 | <input type="radio" name="authorisationMethod" value="noMapping"%s> |
---|
475 | </td> |
---|
476 | <td> |
---|
477 | Don't allow mapping of my roles. |
---|
478 | </td> |
---|
479 | </tr> |
---|
480 | </tbody> |
---|
481 | </table> |
---|
482 | </div> |
---|
483 | </form> |
---|
484 | """ % (authorisationMethodChk['allowMapping'], \ |
---|
485 | authorisationMethodChk['allowMappingWithPrompt'], \ |
---|
486 | authorisationMethodChk['noMapping']) |
---|
487 | |
---|
488 | if bodyTag: print "</body>" |
---|
489 | if htmlTag: print "</html>" |
---|
490 | |
---|
491 | # end of showLogin() |
---|
492 | |
---|
493 | |
---|
494 | def showHomeSiteSelect(self, |
---|
495 | trustedHosts=None, |
---|
496 | scriptName=None, |
---|
497 | contentTypeHdr=False, |
---|
498 | htmlTag=False, |
---|
499 | hdrTag=False, |
---|
500 | hdrTxt='', |
---|
501 | bodyTag=False, |
---|
502 | pageTitle=""): |
---|
503 | |
---|
504 | if trustedHosts: |
---|
505 | self.trustedHosts = trustedHosts |
---|
506 | |
---|
507 | if scriptName: |
---|
508 | self.scriptName = scriptName |
---|
509 | |
---|
510 | |
---|
511 | if contentTypeHdr: |
---|
512 | print "Content-type: text/html\n\n" |
---|
513 | |
---|
514 | if htmlTag: |
---|
515 | print "<html>\n" |
---|
516 | |
---|
517 | if hdrTag: |
---|
518 | if not hdrTxt: |
---|
519 | hdrTxt = """ <style type=\"text/css\"> |
---|
520 | <!-- |
---|
521 | .al { |
---|
522 | text-align: justify |
---|
523 | } |
---|
524 | a{ |
---|
525 | text-decoration:none; |
---|
526 | } |
---|
527 | a:hover{ |
---|
528 | color:#0000FF; |
---|
529 | } |
---|
530 | body { font-family: Verdana, sans-serif; font-size: 11} |
---|
531 | table { font-family: Verdana, sans-serif; font-size: 11} |
---|
532 | --> |
---|
533 | </style>""" |
---|
534 | |
---|
535 | print """<head> |
---|
536 | |
---|
537 | <title>%s</title> |
---|
538 | %s |
---|
539 | </head>""" % (pageTitle, hdrTxt) |
---|
540 | |
---|
541 | |
---|
542 | if bodyTag: |
---|
543 | print "<body>\n" |
---|
544 | |
---|
545 | print """ |
---|
546 | <form action="%s" method="POST"> |
---|
547 | <table bgcolor=#ADD8E6 cellspacing=0 border=0 cellpadding=5> |
---|
548 | <tbody> |
---|
549 | <tr> |
---|
550 | <td> |
---|
551 | <select name="requestURI"> |
---|
552 | <option value="">Select your home site...""" % self.scriptName |
---|
553 | |
---|
554 | for hostname, uri in trustedHosts.items(): |
---|
555 | print "<option value=\"%s\">%s" % (uri, hostname) |
---|
556 | |
---|
557 | print \ |
---|
558 | """ </select> |
---|
559 | </td> |
---|
560 | <td align="right"> |
---|
561 | <input type=submit value="Go"> |
---|
562 | </td> |
---|
563 | </tr> |
---|
564 | </tbody> |
---|
565 | </table> |
---|
566 | </form>""" |
---|
567 | |
---|
568 | if bodyTag: |
---|
569 | print "</body>\n" |
---|
570 | |
---|
571 | if htmlTag: |
---|
572 | print "</html>\n" |
---|
573 | |
---|
574 | # end of showHomeSiteSelect() |
---|
575 | |
---|
576 | |
---|
577 | if __name__ == "__main__": |
---|
578 | clntPubKeyFilePath = "../certs/GabrielCGI-cert.pem" |
---|
579 | clntPriKeyFilePath = "../certs/GabrielCGI-key.pem" |
---|
580 | |
---|
581 | securityCGI = SecurityCGI("http://gabriel.bnsc.rl.ac.uk/sessionMgr.wsdl", |
---|
582 | smPubKeyFilePath="/usr/local/NDG/conf/certs/gabriel-sm-cert.pem, |
---|
583 | clntPubKeyFilePath=clntPubKeyFilePath, |
---|
584 | clntPriKeyFilePath=clntPriKeyFilePath, |
---|
585 | returnURI="https://gabriel.bnsc.rl.ac.uk/cgi-bin/security.py", |
---|
586 | trustedHosts=None) |
---|
587 | securityCGI() |
---|