source: TI12-security/trunk/python/NDG/SecurityCGI.py @ 1018

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/NDG/SecurityCGI.py@1018
Revision 1018, 19.4 KB checked in by pjkersha, 13 years ago (diff)

Tests/SecurityClientTest?.py: mods to run on gabriel.

Tests/security.py: added functionality to get trusted host info.

dist/NDG-Security-0.66.tar.gz: new distro for testing on gabriel.

conf/mapConfig.xml: added loginURI tag for each trusted host - indicate URI for user login useful for forwarding of
login page from remote site.

NDG/AttAuthorityIO.py: include loginURI tag in trusted host info response message.

NDG/SecurityCGI.py: include functionality to get trusted host info from an AttAuthority?

NDG/AttAuthority.py: added loginURI tag for getTrustedHostInfo call.

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