source: TI12-security/trunk/python/NDG/Session.py @ 784

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

Fixes for CEH and PML installations...

conf/sessionMgrProperties.xml: added missing <simpleCASrvProp> tag.
NDG/SimpleCA.py: fixed bug in setProperties method - lTime var was ref'd but not set.
NDG/SessionClient.py: addUser method - move if 'errMsg' ... block out of exception handling
block for greater efficiency.
NDG/Session.py: addUser method of SessionMgr? class - fixed bug return AddUserResp? instance
instead of trying to raise an exception with it.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1"""NDG Session Management and security includes UserSession,
2SessionMgr, Credentials Repository classes.
3
4NERC Data Grid Project
5
6P J Kershaw 02/06/05
7
8Copyright (C) 2005 CCLRC & NERC
9
10This software may be distributed under the terms of the Q Public License,
11version 1.0 or later.
12"""
13
14cvsID = '$Id$'
15
16# SQLObject Database interface
17from sqlobject import *
18
19# MYSQL exceptions have no error message associated with them so include here
20# to allow an explicit trap around database calls
21import _mysql_exceptions
22
23# Placing of session ID on client
24from Cookie import SimpleCookie
25
26# Time module for use with cookie expiry
27from time import strftime
28from datetime import datetime
29
30# For parsing of properties files
31import cElementTree as ElementTree
32
33# Base 64 encode session IDs if returned in strings - urandom's output may
34# not be suitable for printing!
35import base64
36
37# Session Manager WSDL URI in cookie
38from Crypto.Cipher import AES
39
40# Check Session Mgr WSDL URI is encrypted
41from urllib import urlopen
42
43# Credential Wallet
44from NDG.CredWallet import *
45
46# MyProxy server interface
47from NDG.MyProxy import *
48
49# Tools for interfacing with SessionMgr WS
50from NDG.SessionMgrIO import *
51
52# SessionMgr.reqAuthorisation - getPubKey WS call.  Don't import
53# AttAuthorityIO's namespace as it would conflict with SessionMgrIO's
54from NDG import AttAuthorityIO
55
56# Use client package to allow reidrection of authorisation requests
57from NDG.SessionClient import *
58
59# Use to pipe output from ZSI ServiceProxy
60from cStringIO import StringIO
61
62# Use in SessionMgr __redirectAuthorisationReq to retrieve and store Public
63# key
64import tempfile
65import urllib
66
67#_____________________________________________________________________________
68class UserSessionError(Exception):   
69    """Exception handling for NDG User Session class."""
70   
71    def __init__(self, msg):
72        self.__msg = msg
73         
74    def __str__(self):
75        return self.__msg
76
77
78
79
80#_____________________________________________________________________________
81# Inheriting from 'object' allows Python 'new-style' class with Get/Set
82# access methods
83class UserSession(object):
84    """Session details - created when a user logs into NDG"""
85
86    # Session ID
87    __sessIDlen = 128
88
89    __cookieTags = ("NDG-ID1", "NDG-ID2")
90
91    # Follow standard format for cookie path and expiry attributes
92    __cookiePathTag = "path"
93    __cookiePath = "/"
94    __cookieDomainTag = 'domain'
95    __cookieExpiryTag = "expires"
96       
97    __sessCookieExpiryFmt = "%a, %d-%b-%Y %H:%M:%S GMT"
98
99
100    def __init__(self, *credWalletArgs, **credWalletKeys):
101        """Initialise UserSession with args and keywords to CredWallet"""
102
103        # Domain for cookie used by createCookie method - if not set, default
104        # is web server domain name
105        self.__cookieDomain = None
106               
107       
108        # Each User Session has one or more browser sessions associated with
109        # it.  These are stored in a list
110        self.__sessIDlist = []
111        self.__createSessID()
112        self.__credWallet = CredWallet(*credWalletArgs, **credWalletKeys)
113
114               
115#    def __repr__(self):
116#        "Represent User Session"       
117#        return "<UserSession instance>"
118
119    def __setCookieDomain(self, cookieDomain):
120        """Set domain for cookie - set to None to assume domain of web server
121        """
122
123        if not isinstance(cookieDomain, basestring) and \
124           cookieDomain is not None:
125            raise UserSessionError(\
126                "Expecting string or None type for \"cookieDomain\"")
127                       
128        self.__cookieDomain = cookieDomain
129
130    cookieDomain = property(fset=__setCookieDomain,
131                            doc="Set cookie domain")
132
133
134    #_________________________________________________________________________
135    # CredWallet access
136    def __getCredWallet(self):
137        """Get Credential Wallet instance"""
138        return self.__credWallet
139   
140    credWallet = property(fget=__getCredWallet,
141                          doc="Read-only access to CredWallet instance")
142
143
144    #_________________________________________________________________________
145    # CredWallet access
146    def __getSessIDlist(self):
147        """Get Session ID list - last item is latest allocated for this
148        session"""
149        return self.__sessIDlist
150   
151    sessIDlist = property(fget=__getSessIDlist,
152                          doc="Read-only access to Session ID list")
153
154
155    #_________________________________________________________________________       
156    def __latestSessID(self):
157        """Get the session ID most recently allocated"""
158        return self.__sessIDlist[-1]
159   
160    # Publish as an attribute
161    latestSessID = property(fget=__latestSessID,
162                            doc="Latest Session ID allocated")
163
164
165    #_________________________________________________________________________
166    def __createSessID(self):
167        """Add a new session ID to be associated with this UserSession
168        instance"""
169
170        # base 64 encode output from urandom - raw output from urandom is
171        # causes problems when passed over SOAP.  A consequence of this is
172        # that the string length of the session ID will almost certainly be
173        # longer than SessionMgr.__sessIDlen
174        sessID = base64.b64encode(os.urandom(self.__sessIDlen))
175        self.__sessIDlist.append(sessID)
176
177
178    #_________________________________________________________________________
179    def __getExpiryStr(self):
180        """Return session expiry date/time as would formatted for a cookie"""
181
182        try:
183            # Proxy certificate's not after time determines the expiry
184            dtNotAfter = self.credWallet.proxyCert.notAfter
185
186            return dtNotAfter.strftime(self.__sessCookieExpiryFmt)
187        except Exception, e:
188            UserSessionError("getExpiry: %s" % e)
189
190
191    #_________________________________________________________________________
192    @staticmethod
193    def encrypt(txt, encrKey):
194        """Encrypt the test of this Session Manager's WS URI / URI for its
195        public key to allow inclusion in a web browser session cookie
196       
197        The address is encrypted and then base 64 encoded"""
198       
199        # Text length must be a multiple of 16 for AES encryption
200        try:
201            mod = len(txt) % 16
202            if mod:
203                nPad = 16 - mod
204            else:
205                nPad = 0
206               
207            # Add padding
208            paddedURI = txt + ''.join([' ' for i in range(nPad)])
209        except Exception, e:
210            raise UserSessionError("Error padding text for encryption: " + \
211                                   str(e))
212       
213        # encrypt
214        try:
215            aes = AES.new(encrKey, AES.MODE_ECB)
216            return base64.b64encode(aes.encrypt(paddedURI))
217       
218        except Exception, e:
219            raise UserSessionError("Error encrypting text: %s" % str(e))
220                                       
221   
222    #_________________________________________________________________________
223    @staticmethod                                   
224    def decrypt(encrTxt, encrKey):
225        """Decrypt text from cookie set by another Session Manager.  This
226        is required when reading a session cookie to find out which
227        Session Manager holds the client's session
228       
229        encrTxt:    base 64 encoded encrypted text"""
230
231        try:
232            aes = AES.new(encrKey, AES.MODE_ECB)
233           
234            # Decode from base 64
235            b64DecodedEncrTxt=base64.b64decode(encrTxt)
236           
237            # Decrypt and strip trailing spaces
238            return aes.decrypt(b64DecodedEncrTxt).strip()
239       
240        except Exception, e:
241            raise SessionMgrError("Decrypting: %s" % str(e))           
242
243
244    #_________________________________________________________________________
245    def createCookie(self, 
246                     sessMgrWSDLuri,
247                     encrKey, 
248                     sessID=None,
249                     cookieDomain=None,
250                     asString=True):
251        """Create cookies for session ID Session Manager WSDL address
252
253        sessMgrWSDLuri:     WSDL address for Session Mananger
254        sessMgrPubKeyURI:   URI for public key of Session Manager
255        encrKey:               encryption key used to encrypted above URIs
256        sessID:                if no session ID is provided, use the latest
257                               one to be allocated.
258        cookieDomain:          domain set for cookie, if non set, web server
259                               domain name is used
260        asString:              Set to True to return the cookie as string
261                               text.  If False, it is returned as a
262                               SimpleCookie instance."""
263
264
265        # Nb. Implicit call to __setCookieDomain method
266        if cookieDomain:
267            self.cookieDomain = cookieDomain
268
269         
270        try:
271            if sessID is None:
272                # Use latest session ID allocated if none was input
273                sessID = self.__sessIDlist[-1]
274               
275            elif not isinstance(sessID, basestring):
276                raise UserSessionError(\
277                                    "Input session ID is not a valid string")
278                                   
279                if sessID not in self.__sessIDlist:
280                    raise UserSessionError(\
281                                        "Input session ID not found in list")
282 
283           
284            sessCookie = SimpleCookie()
285           
286            tagValues = (sessID, self.encrypt(sessMgrWSDLuri, encrKey))
287                         
288            expiryStr = self.__getExpiryStr()
289           
290            i=0
291            for tag in self.__cookieTags:
292               
293                sessCookie[tag] = tagValues[i]
294                i += 1
295               
296                # Use standard format for cookie path and expiry
297                sessCookie[tag][self.__cookiePathTag] = self.__cookiePath               
298                sessCookie[tag][self.__cookieExpiryTag]= expiryStr
299                                           
300                # Make cookie as generic as possible for domains - Nb. '.uk'
301                # alone won't work
302                if self.__cookieDomain:
303                    sessCookie[tag][self.__cookieDomainTag] = \
304                                                        self.__cookieDomain
305           
306           
307            # Caller should set the cookie e.g. in a CGI script
308            # print "Content-type: text/html"
309            # print cookie.output() + os.linesep
310            if asString:
311                return sessCookie.output()
312            else:
313                return sessCookie
314           
315        except Exception, e:
316            raise UserSessionError("Creating Session Cookie: %s" % e)
317
318
319#_____________________________________________________________________________
320class SessionMgrError(Exception):   
321    """Exception handling for NDG Session Manager class."""
322   
323    def __init__(self, msg):
324        self.__msg = msg
325         
326    def __str__(self):
327        return self.__msg
328
329
330#_____________________________________________________________________________
331class SessionMgr(dict):
332    """NDG authentication and session handling"""
333
334    # valid configuration property keywords
335    __validKeys = [    'caCertFile',
336                       'certFile',
337                       'keyFile',
338                       'keyPPhrase', 
339                       'sessMgrEncrKey', 
340                       'sessMgrWSDLuri',
341                       'cookieDomain', 
342                       'myProxyProp', 
343                       'credReposProp']
344
345   
346    #_________________________________________________________________________
347    def __init__(self, 
348                 propFilePath=None, 
349                 credReposPPhrase=None, 
350                 **prop):       
351        """Create a new session manager to manager NDG User Sessions
352       
353        propFilePath:        path to properties file
354        credReposPPhrase:    for credential repository if not set in
355                             properties file
356        **prop:              set any other properties corresponding to the
357                             tags in the properties file"""       
358
359        # Base class initialisation
360        dict.__init__(self)
361       
362
363        # MyProxy interface
364        try:
365            self.__myPx = MyProxy()
366           
367        except Exception, e:
368            raise SessionMgrError("Creating MyProxy interface: %s" % e)
369
370       
371        # Credentials repository - permanent stroe of user credentials
372        try:
373            self.__credRepos = SessionMgrCredRepos()
374           
375        except Exception, e:
376            raise SessionMgrError(\
377                    "Creating credential repository interface: %s" % e)
378
379        self.__sessList = []
380
381        # Dictionary to hold properties
382        self.__prop = {}
383       
384       
385        # Set properties from file
386        if propFilePath is not None:
387            self.readProperties(propFilePath,
388                                credReposPPhrase=credReposPPhrase)
389
390
391        # Set any properties that were provided by keyword input
392        #
393        # Nb. If any are duplicated with tags in the properties file they
394        # will overwrite the latter
395        self.setProperties(**prop)
396     
397       
398    #_________________________________________________________________________       
399    def __delitem__(self, key):
400        "Session Manager keys cannot be removed"       
401        raise KeyError('Keys cannot be deleted from '+self.__class__.__name__)
402
403
404    def __getitem__(self, key):
405        self.__class__.__name__ + """ behaves as data dictionary of Session
406        Manager properties
407        """
408        if key not in self.__prop:
409            raise KeyError("Invalid key " + key)
410       
411        return self.__prop[key]
412   
413   
414    def __setitem__(self, key, item):
415        self.__class__.__name__ + """ behaves as data dictionary of Session
416        Manager properties"""
417        self.setProperties(**{key: item})
418       
419
420    def clear(self):
421        raise KeyError("Data cannot be cleared from "+self.__class__.__name__)
422   
423    def keys(self):
424        return self.__prop.keys()
425
426    def items(self):
427        return self.__prop.items()
428
429    def values(self):
430        return self.__prop.values()
431
432    def has_key(self, key):
433        return self.__prop.has_key(key)
434
435    # 'in' operator
436    def __contains__(self, key):
437        return key in self.__prop
438           
439
440    #_________________________________________________________________________
441    def readProperties(self,
442                       propFilePath=None,
443                       propElem=None,
444                       credReposPPhrase=None):
445        """Read Session Manager properties from an XML file or cElementTree
446        node"""
447
448        if propFilePath is not None:
449
450            try:
451                tree = ElementTree.parse(propFilePath)
452                propElem = tree.getroot()
453
454            except IOError, e:
455                raise SessionMgrError(\
456                                "Error parsing properties file \"%s\": %s" % \
457                                (e.filename, e.strerror))
458               
459            except Exception, e:
460                raise SessionMgrError(\
461                    "Error parsing properties file: \"%s\": %s" % \
462                    (propFilePath, e))
463
464        if propElem is None:
465            raise SessionMgrError("Root element for parsing is not defined")
466
467        for elem in propElem:
468            if elem.tag == 'myProxyProp':
469                self.__myPx.readProperties(propElem=elem)
470
471            elif elem.tag == 'credReposProp':
472                self.__credRepos.readProperties(propElem=elem,
473                                                dbPPhrase=credReposPPhrase)
474            elif elem.tag in self.__validKeys:
475                try:
476                    # Check for environment variables in file paths
477                    tagCaps = elem.tag.upper()
478                    if 'FILE' in tagCaps or \
479                       'PATH' in tagCaps or \
480                       'DIR' in tagCaps:
481                        elem.text = os.path.expandvars(elem.text)
482                       
483                    self.__prop[elem.tag] = elem.text
484                   
485                    # Strip white space but not in the case of pass-phrase
486                    # field as pass-phrase might contain leading or trailing
487                    # white space
488                    if elem.tag != 'keyPPhrase' and \
489                       isinstance(self.__prop[elem.tag], basestring):
490                        self.__prop[elem.tag].strip()
491                       
492                except Exception, e:
493                    raise SessionMgrError(\
494                        "Error parsing properties file tag: \"%s\": %s" % \
495                        (elem.tag, e))
496               
497            else:
498                raise SessionMgrError(\
499                    "\"%s\" is not a valid properties file tag" % elem.tag)
500
501
502    #_________________________________________________________________________
503    def setProperties(self, **prop):
504        """Update existing properties from an input dictionary
505        Check input keys are valid names"""
506       
507        for key in prop.keys():
508            if key not in self.__validKeys:
509                raise SessionMgrError("Property name \"%s\" is invalid" % key)
510
511
512        for key, value in prop.items():
513                       
514            if key == 'myProxyProp':
515                self.__myPx.setProperties(prop[key])
516   
517            elif key == 'credReposProp':
518                self.__credRepos.setProperties(prop[key])
519
520            elif key in self.__validKeys:
521                # Only update other keys if they are not None or ""
522                if value:
523                    self.__prop[key] = value               
524            else:
525                raise SessionMgrError(\
526                    "Key \"%s\" is not a valid Session Manager property" %
527                    key)
528
529
530    #_________________________________________________________________________
531    def addUser(self, caConfigFilePath=None, caPassPhrase=None, **reqKeys):       
532        """Register a new user with NDG data centre
533       
534        addUser([caConfigFilePath, ]|[, caPassPhrase]
535                |[, userName=u, pPhrase=p])
536
537        returns XML formatted response message
538       
539        caConfigFilePath|caPassPhrase:  pass phrase for SimpleCA's
540                                        certificate.  Set via file or direct
541                                        string input respectively.  Set here
542                                        to override setting [if any] made at
543                                        object creation.
544       
545                                        Passphrase is only required if
546                                        SimpleCA is instantiated on the local
547                                        machine.  If SimpleCA WS is called no
548                                        passphrase is required.
549                                       
550        **reqKeys:                      use as alternative to
551                                        reqXMLtxt keyword - pass in
552                                        username and pass-phrase for new user
553                                        unencrypted as keywords username
554                                        and pPhrase respectively.  See
555                                        SessionMgrIO.AddUserRequest class for
556                                        reference."""
557             
558        try:
559            # Add new user certificate to MyProxy Repository
560            user = self.__myPx.addUser(reqKeys['userName'],
561                                       reqKeys['pPhrase'],
562                                       caConfigFilePath=caConfigFilePath,
563                                       caPassPhrase=caPassPhrase,
564                                       retDN=True)
565           
566            # Add to user database
567            self.__credRepos.addUser(reqKeys['userName'], user['dn'])
568           
569        except Exception, e:
570            return AddUserResp(errMsg=str(e))
571
572        return AddUserResp(errMsg='')
573   
574   
575    #_________________________________________________________________________       
576    def connect(self, **reqKeys):       
577        """Create a new user session or connect to an existing one:
578
579        connect([getCookie=True/False][createServerSess=Tue/False, ]
580                [, userName=u, pPhrase=p]|[, proxyCert=px]|[, sessID=id])
581
582        getCookie:              If True, allocate a user session with a
583                                wallet in the session manager and return a
584                                cookie containing the new session ID
585                                allocated.  If set False, return a proxy
586                                certificate only.  The client is then
587                                responsible for Credential Wallet management.
588        createServerSess:       If set to True, the SessionMgr will create
589                                and manage a session for the user.  Nb.
590                                this flag is ignored and set to True if
591                                getCookie is set.  For command line case,
592                                where getCookie is False, it's possible
593                                to choose to have a client or server side
594                                session using this keyword.
595        reqXMLtxt:              encrypted XML containing user credentials -
596                                user name, pass-phrase or proxy cert etc
597        reqKeys:                username and pass-phrase or the proxy"""
598       
599
600        if 'sessID' in reqKeys:
601           
602            # Connect to an existing session identified by a session ID and
603            # return equivalent proxy cert
604            userSess = self.__connect2UserSession(sessID=sessID)
605            return ConnectResp(proxyCert=userSess.credWallet.proxyCertTxt)
606       
607        elif 'proxyCert' in reqKeys:
608            # Connect to an existing session identified by a proxy
609            # certificate and return an equivalent session cookie
610            userSess = self.__connect2UserSession(proxyCert=proxyCert)
611            sessCookie = userSess.createCookie(self.__prop['sessMgrWSDLuri'],
612                                               self.__prop['sessMgrEncrKey'])
613            return ConnectResp(sessCookie=sessCookie)
614       
615        else:
616            # Create a fresh session
617            proxyCert = self.__delegateProxy(reqKeys['userName'], 
618                                             reqKeys['pPhrase'])
619
620            bGetCookie = 'getCookie' in reqKeys and reqKeys['getCookie']
621                                               
622            bCreateServerSess = 'createServerSess' in reqKeys and \
623                                            reqKeys['createServerSess']
624                                           
625            if bGetCookie or bCreateServerSess:
626                # Session Manager creates and manages user's session
627                userSess = self.__createUserSession(proxyCert)
628 
629                               
630            if bGetCookie:
631               
632                # Web browser client - Return session cookie
633                userSess.cookieDomain = self.__prop['cookieDomain']
634
635                sessCookie = userSess.createCookie(\
636                                            self.__prop['sessMgrWSDLuri'],
637                                            self.__prop['sessMgrEncrKey'])
638               
639                try:
640                    # Encrypt response if a client public key is available
641                    return ConnectResp(sessCookie=sessCookie)
642               
643                except Exception, e:
644                    raise SessionMgrError(\
645                        "Error formatting connect response: %s" % e)               
646            else:
647                # NDG Command line client - Return proxy certificate
648                return ConnectResp(proxyCert=proxyCert)
649           
650               
651    #_________________________________________________________________________       
652    def __delegateProxy(self, userName, passPhrase):
653        """Delegate a proxy certificate ID from input user credentials"""
654       
655        if not userName:
656            raise SessionMgrError(\
657                            "Getting proxy delegation: username is null")
658       
659        if not passPhrase:
660            raise SessionMgrError(\
661                            "Getting proxy delegation: pass-phrase is null")
662       
663        try:           
664            # Get a proxy certificate to represent users ID for the new
665            # session
666            return self.__myPx.getDelegation(userName, passPhrase)
667
668        except Exception, e:
669            raise SessionMgrError("Delegating from MyProxy: %s" % e)
670       
671       
672    #_________________________________________________________________________       
673    def __createUserSession(self, proxyCert):
674        """Create a new user session from input user credentials       
675        and return
676       
677        """
678       
679        try:   
680            # Search for an existing session for the same user
681            userSess = None
682            # PJK 16/12/05 - DON'T search for existing sessions make a new one
683            # even if the user has one already. 
684            # !! This allows users to have multiple sessions !!
685#            for u in self.__sessList:
686#                if u.credWallet['proxyCert'].dn['CN'] == userName:
687#
688#                    # Existing session found
689#                    userSess = u
690#
691#                    # Replace it's Proxy Certificate with a more up to date
692#                    # one
693#                    userSess.credWallet.proxyCert = proxyCert
694#                    break
695               
696
697            if userSess is None:
698                # Create a new user session using the new proxy certificate
699                # and session ID
700                #
701                # Nb. Client pub/pri key info to allow message level
702                # encryption for responses from Attribute Authority WS
703                userSess = UserSession(proxyCert, 
704                                caPubKeyFilePath=self.__prop['caCertFile'],
705                                clntPubKeyFilePath=self.__prop['certFile'],
706                                clntPriKeyFilePath=self.__prop['keyFile'],
707                                clntPriKeyPwd=self.__prop['keyPPhrase'],
708                                credRepos=self.__credRepos)               
709                newSessID = userSess.latestSessID
710               
711                # Check for unique session ID
712                for existingUserSess in self.__sessList:
713                    if newSessID in existingUserSess.sessIDlist:
714                        raise SessionMgrError(\
715                            "Session ID is not unique:\n\n %s" % newSessID)
716
717                # Add new session to list                 
718                self.__sessList.append(userSess)
719
720            # Return new session
721            return userSess
722       
723        except Exception, e:
724            raise SessionMgrError("Creating User Session: %s" % e)
725
726
727    #_________________________________________________________________________       
728    def __connect2UserSession(self, **idKeys):
729        """Connect to an existing session by providing a valid session ID
730
731        __connect2UserSession([proxyCert]|[sessID])
732       
733        proxyCert:    proxy certificate corresponding to an existing
734                      session to connect to.
735        sessID:       similiarly, a web browser session ID linking to an
736                      an existing session."""
737       
738           
739        # Look for a session corresponding to this ID
740        if 'sessID' in idKeys:
741            try:
742                for userSess in self.__sessList:
743                    if idKeys['sessID'] in userSess.sessIDlist:
744   
745                        # Check matched session has not expired
746                        userSess.credWallet.isValid(raiseExcep=True)
747                        return userSess
748                       
749            except Exception, e:
750                raise SessionMgrError(\
751                "Matching session ID to existing user session: %s" % e)
752               
753            # User session not found with given ID
754            raise SessionMgrError(\
755                "No user session found matching input session ID")
756       
757        elif 'proxyCert' in idKeys:
758            try:
759                for userSess in self.__sessList:
760                    if userSess.credWallet.proxyCertTxt==idKeys['proxyCert']:
761                       
762                        # Check matched session has not expired
763                        userSess.credWallet.isValid(raiseExcep=True)
764                        return userSess
765                                       
766            except Exception, e:
767                raise SessionMgrError(\
768                "Matching proxy certificate to existing user session: %s" % e)
769               
770            # User session not found with given proxy cert
771            raise SessionMgrError(\
772                    "No user session found matching input proxy certificate")
773        else:
774            raise SessionMgrError(\
775                                '"sessID" or "proxyCert" keyword must be set')
776
777
778
779    #_________________________________________________________________________
780    def reqAuthorisation(self, **reqKeys):
781        """For given sessID, request authorisation from an Attribute Authority
782        given by aaWSDL.  If sucessful, an attribute certificate is
783        returned.
784
785        **reqKeys:            pass equivalent to XML as keywords instead.
786                              See SessionMgrIO.AuthorisationReq class
787        """
788       
789        # Web browser client input will include the encrypted address of the
790        # Session Manager where the user's session is held.
791        if 'encrSessMgrWSDLuri' in reqKeys:
792           
793            # Decrypt the URI for where the user's session resides
794            userSessMgrWSDLuri = UserSession.decrypt(\
795                                                reqKeys['encrSessMgrWSDLuri'],
796                                                self.__prop['sessMgrEncrKey'])
797                                               
798            # Check the address against the address of THIS Session Manager 
799            if userSessMgrWSDLuri != self.__prop['sessMgrWSDLuri']:
800               
801                # Session is held on a remote Session  Manager
802                userSessMgrResp = self.__redirectAuthorisationReq(\
803                                                        userSessMgrWSDLuri,
804                                                        **reqKeys)
805
806                # Reset response by making a new AuthorisationResp object
807                # The response from the remote Session Manager will still
808                # contain the encrypted XML sent by it.  This should be
809                # discarded
810                return userSessMgrResp
811
812           
813        # User's session resides with THIS Session Manager / no encrypted
814        # WSDL address passed in (as in command line context for security) ...
815
816           
817        # Retrieve session corresponding to user's session ID using relevant
818        # input credential
819        idKeys = {}
820        if 'sessID' in reqKeys:
821            idKeys['sessID'] = reqKeys['sessID']
822           
823        elif 'proxyCert' in reqKeys:
824            idKeys['proxyCert'] = reqKeys['proxyCert']           
825        else:
826            raise SessionMgrError(\
827                                'Expecting "sessID" or "proxyCert" keywords')
828                               
829        userSess = self.__connect2UserSession(**idKeys)
830
831
832        # Copy keywords to be passed onto the request to the attribute
833        # authority
834        #
835        # Nb. the following keys aren't required
836        delKeys = ('proxyCert',
837                   'sessID',
838                   'encrCert',
839                   'encrSessMgrWSDLuri', 
840                   'aaPubKey')
841                   
842        aaKeys = dict([i for i in reqKeys.items() if i[0] not in delKeys])
843
844
845        if 'aaPubKey' not in reqKeys:
846            # Get public key using WS
847            try:
848                aaSrv = ServiceProxy(reqKeys['aaWSDL'], use_wsdl=True)
849               
850                pubKeyReq = AttAuthorityIO.PubKeyReq()
851                resp = aaSrv.getPubKey(pubKeyReq=pubKeyReq())
852               
853                pubKeyResp = AttAuthorityIO.PubKeyResp(\
854                                                   xmlTxt=resp['pubKeyResp'])
855       
856                if 'errMsg' in pubKeyResp and pubKeyResp['errMsg']:
857                    raise Exception(pubKeyResp['errMsg'])
858               
859                reqKeys['aaPubKey'] = pubKeyResp['pubKey']
860               
861            except Exception, e:
862                raise SessionMgrError(\
863                    "Retrieving Attribute Authority public key: "+ str(e))
864                               
865                                                       
866        # Make a temporary file to hold Attribute Authority Public Key. 
867        # The Credential Wallet needs this to encrypt requests to the
868        # Attribute Authority
869        try:
870            aaPubKeyTmpFile = tempfile.NamedTemporaryFile()
871            open(aaPubKeyTmpFile.name, "w").write(reqKeys['aaPubKey'])
872            aaKeys['aaPubKeyFilePath'] = aaPubKeyTmpFile.name
873           
874        except IOError, (errNo, errMsg):
875            raise SessionMgrError("Making temporary file for Attribute " + \
876                                  "Authority public key: %s" % errMsg)
877               
878        except Exception, e:
879            raise SessionMgrError("Making temporary file for Attribute " + \
880                                  "Authority public key: %s" % str(e))
881
882                                             
883        # User's Credential Wallet carries out authorisation request to the
884        # Attribute Authority
885        try:
886            attCert = userSess.credWallet.reqAuthorisation(**aaKeys)
887           
888            # AuthorisationResp class formats a response message in XML and
889            # allow dictionary-like access to XML tags
890            resp = AuthorisationResp(attCert=attCert, 
891                                     statCode=AuthorisationResp.accessGranted)
892           
893        except CredWalletAuthorisationDenied, e:
894            # Exception object containa a list of attribute certificates
895            # which could be used to re-try to get authorisation via a mapped
896            # certificate
897            resp = AuthorisationResp(extAttCertList=e.extAttCertList,
898                                     statCode=AuthorisationResp.accessDenied,
899                                     errMsg=str(e))
900       
901        except Exception, e:
902            # Some other error occured - create an error Authorisation
903            # response
904            resp = AuthorisationResp(statCode=AuthorisationResp.accessError,
905                                     errMsg=str(e))
906   
907        return resp
908
909
910    #_________________________________________________________________________
911    def __redirectAuthorisationReq(self, userSessMgrWSDLuri, **reqKeys):
912        """Handle case where User session resides on another Session Manager -
913        forward the request"""
914       
915        # Instantiate WS proxy for remote session manager
916        try:
917            sessClnt = SessionClient(smWSDL=userSessMgrWSDLuri,
918                                 clntPubKeyFilePath=self.__prop['certFile'],
919                                 clntPriKeyFilePath=self.__prop['keyFile'])           
920        except Exception, e:
921            raise SessionMgrError(\
922                        "Re-directing authorisation request to \"%s\": %s" % \
923                        (userSessMgrWSDLuri, str(e)))
924
925           
926        # Call remote session manager's authorisation request method
927        # and return result to caller
928        try:
929            # encrCert key not needed - it gets set above via
930            # 'clntPubKeyFilePath'
931            if 'encrCert' in reqKeys:
932                del reqKeys['encrCert']
933               
934            # Call remote SessionMgr where users session lies
935            redirectAuthResp = sessClnt.reqAuthorisation(\
936                                    clntPriKeyPwd=self.__prop['keyPPhrase'],
937                                    **reqKeys)
938         
939            return redirectAuthResp
940       
941        except Exception, e:
942            raise SessionMgrError(\
943        "Forwarding Authorisation request for Session Manager \"%s\": %s" %\
944                (userSessMgrWSDLuri, e))
945
946
947    #_________________________________________________________________________
948    def auditCredRepos(self):
949        """Remove expired Attribute Certificates from the Credential
950        Repository"""
951        self.__credRepos.auditCredentials()
952
953
954
955       
956def reqAuthorisationTest(userName, passPhrase=None, passPhraseFilePath='tmp'):
957
958    import pdb
959    pdb.set_trace()
960
961    try:
962        if passPhrase is None:
963            passPhrase = open(passPhraseFilePath).read().strip()
964           
965        # Start session manager
966        sessMgr = SessionMgr("./sessionMgrProperties.xml")
967
968        # Create a new session
969        userSess = sessMgr.connect(userName, passPhrase)
970
971        # Request authorisation from a data centre
972        return sessMgr.reqAuthorisation(\
973                            aaWSDL='./attAuthority.wsdl', 
974                            #aaPropFilePath='./attAuthorityProperties.xml',
975                            sessID=userSess['sessID'][0])
976
977    except Exception, e:
978        print str(e)
979       
980
981
982
983def addUserTest(userName,
984                userPassPhrase,
985                caConfigFilePath="tmp.txt",
986                caPassPhrase=None):
987
988    import pdb
989    pdb.set_trace()
990
991    try:
992        # Add a new user using the session manager
993        sessMgr = SessionMgr("./sessionMgrProperties.xml")
994        sessMgr.addUser(userName,
995                        userPassPhrase,
996                        caConfigFilePath=caConfigFilePath)
997       
998    except Exception, e:
999        print str(e)
1000   
1001
1002
1003
1004#_____________________________________________________________________________
1005class SessionMgrCredRepos(CredRepos):
1006    """Interface to Credential Repository Database
1007   
1008    Nb. inherits from CredWallet.CredRepos to ensure correct interface
1009    to the wallet"""
1010
1011    # valid configuration property keywords
1012    __validKeys = ['dbURI']
1013   
1014
1015    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
1016        """Initialise Credentials Repository Database object.
1017
1018        If the connection string or properties file is set a connection
1019        will be made
1020
1021        dbURI:              <db type>://<username>:<passwd>@<hostname>/dbname
1022        propFilePath: file path to properties file
1023
1024        Nb. propFilePath setting overrides input dbURI
1025        """
1026           
1027        self.__con = None
1028        self.__prop = {}
1029       
1030        if propFilePath is not None:
1031           
1032            # Read database URI set in file
1033            self.readProperties(propFilePath, dbPPhrase=dbPPhrase)
1034           
1035        elif prop != {}:
1036           
1037            # Database URI may have been set as an input keyword argument
1038            self.setProperties(dbPPhrase=dbPPhrase, **prop)
1039
1040
1041
1042
1043    def __setConnection(self,
1044                        dbType=None,
1045                        dbUserName=None,
1046                        dbPPhrase=None,
1047                        dbHostname=None,
1048                        dbName=None,
1049                        dbURI=None,
1050                        chkConnection=True):
1051        """Establish a database connection from a database URI
1052
1053        pass a URI OR the parameters to construct the URI
1054           
1055        dbURI: "<db type>://<username>:<passwd>:<hostname>/dbname"
1056
1057        or
1058
1059        dbURI: "<db type>://<username>:%PPHRASE%:<hostname>/dbname"
1060        + passPhrase
1061
1062        - %PPHRASE% is substituted with the input passPhrase keyword
1063       
1064        or
1065       
1066        dbType:         database type e.g. 'mysql'
1067        dbUserName:     username
1068        dbPPhrase:      pass-phrase
1069        dbHostname:     name of host where database resides
1070        dbName:         name of the database
1071
1072
1073        chkConnection:  check that the URI is able to connect to the
1074        """
1075
1076        try:
1077            if dbURI:
1078                # Check for pass-phrase variable set in URI '%PPHRASE%'
1079                dbURIspl = dbURI.split('%')
1080                if len(dbURIspl) == 3:
1081                   
1082                    if dbPPhrase is None:
1083                        raise CredReposError("No database pass-phrase set")
1084                   
1085                    dbURI = dbURIspl[0] + dbPPhrase + dbURIspl[2]
1086               
1087            else:
1088                # Construct URI from individual inputs
1089                dbURI = dbType + '://' + dbUserName + ':' + dbPPhrase + \
1090                        ':' + dbHostname + '/' + dbName
1091        except Exception, e:
1092            # Checking form missing keywords
1093            raise CredReposError("Error creating database URI: %s" % e)
1094
1095        try:
1096            self.__con = connectionForURI(dbURI)
1097        except Exception, e:
1098            raise CredReposError("Error creating database connection: %s" % e)
1099
1100        if chkConnection:
1101            try:
1102                self.__con.makeConnection()
1103            except _mysql_exceptions.OperationalError, (errNum, errMsg):
1104                raise CredReposError(\
1105                    "Error connecting to Credential Repository: %s" % errMsg)
1106               
1107            except Exception, e:
1108                raise CredReposError(\
1109                    "Error connecting to Credential Repository: %s" % e)
1110
1111           
1112        # Copy the connection object into the table classes
1113        SessionMgrCredRepos.User._connection = self.__con
1114        SessionMgrCredRepos.UserCredential._connection = self.__con
1115         
1116
1117
1118
1119    def setProperties(self, dbPPhrase=None, **prop):
1120        """Update existing properties from an input dictionary
1121        Check input keys are valid names"""
1122       
1123        for key in prop.keys():
1124            if key not in self.__validKeys:
1125                raise CredReposError("Property name \"%s\" is invalid" % key)
1126               
1127        self.__prop.update(prop)
1128
1129
1130        # Update connection setting
1131        if 'dbURI' in prop:
1132            self.__setConnection(dbURI=prop['dbURI'],
1133                                 dbPPhrase=dbPPhrase)
1134               
1135
1136
1137       
1138    def readProperties(self,
1139                       propFilePath=None,
1140                       propElem=None,
1141                       dbPPhrase=None):
1142        """Read the configuration properties for the CredentialRepository
1143
1144        propFilePath|propElem
1145
1146        propFilePath: set to read from the specified file
1147        propElem:     set to read beginning from a cElementTree node"""
1148
1149        if propFilePath is not None:
1150
1151            try:
1152                tree = ElementTree.parse(propFilePath)
1153                propElem = tree.getroot()
1154               
1155            except IOError, e:
1156                raise CredReposError(\
1157                                "Error parsing properties file \"%s\": %s" % \
1158                                (e.filename, e.strerror))
1159
1160            except Exception, e:
1161                raise CredReposError("Error parsing properties file: %s" % \
1162                                    str(e))
1163
1164        if propElem is None:
1165            raise CredReposError("Root element for parsing is not defined")
1166
1167
1168        # Read properties into a dictionary
1169        prop = {}
1170        for elem in propElem:
1171                   
1172            # Check for environment variables in file paths
1173            tagCaps = elem.tag.upper()
1174            if 'FILE' in tagCaps or 'PATH' in tagCaps or 'DIR' in tagCaps:
1175                elem.text = os.path.expandvars(elem.text)
1176
1177            prop[elem.tag] = elem.text
1178           
1179        self.setProperties(dbPPhrase=dbPPhrase, **prop)
1180
1181           
1182
1183    def addUser(self, userName, dn):
1184        """A new user to Credentials Repository"""
1185        try:
1186            self.User(userName=userName, dn=dn)
1187
1188        except Exception, e:
1189            raise CredReposError("Error adding new user '%s': %s" % \
1190                                                        (userName, e))
1191
1192
1193
1194                           
1195    def auditCredentials(self, **attCertValidKeys):
1196        """Check the attribute certificates held in the repository and delete
1197        any that have expired
1198
1199        attCertValidKeys:  keywords which set how to check the Attribute
1200                            Certificate e.g. check validity time, XML
1201                            signature, version etc.  Default is check
1202                            validity time only"""
1203
1204        if attCertValidKeys == {}:
1205            # Default to check only the validity time
1206            attCertValidKeys = {    'chkTime':          True,
1207                                    'chkVersion':       False,
1208                                    'chkProvenance':    False,
1209                                    'chkSig':           False }
1210           
1211        try:
1212            credList = self.UserCredential.select()
1213           
1214        except Exception, e:
1215            raise CredReposError("Selecting credentials from repository: %s",\
1216                                 e)
1217
1218        # Iterate through list of credentials deleting records where the
1219        # certificate is invalid
1220        try:
1221            for cred in credList:
1222                attCert = AttCertParse(cred.attCert)
1223               
1224                if not attCert.isValid(**attCertValidKeys):
1225                    self.UserCredential.delete(cred.id)
1226                   
1227        except Exception, e:
1228            try:
1229                raise CredReposError("Deleting credentials for '%s': %s",
1230                                                       (cred.dn, e))
1231            except:
1232                raise CredReposError("Deleting credentials: %s", e)
1233
1234
1235
1236
1237    def getCredentials(self, dn):
1238        """Get the list of credentials for a given user's DN"""
1239
1240        try:
1241            return self.UserCredential.selectBy(dn=dn)
1242           
1243        except Exception, e:
1244            raise CredReposError("Selecting credentials for %s: %s" % (dn, e))
1245
1246
1247
1248       
1249    def addCredentials(self, dn, attCertList):
1250        """Add new attribute certificates for a user.  The user must have
1251        been previously registered in the repository
1252
1253        dn:             users Distinguished name
1254        attCertList:   list of attribute certificates"""
1255       
1256        try:
1257            userCred = self.User.selectBy(dn=dn)
1258           
1259            if userCred.count() == 0:
1260                raise CredReposError("User \"%s\" is not registered" % dn)
1261
1262        # Make explicit trap for MySQL interface error since it has no error
1263        # message associated with it
1264        except _mysql_exceptions.InterfaceError, e:
1265            raise CredReposError("Checking for user \"%s\": %s" % \
1266                                 (dn, "MySQL interface error"))
1267       
1268        except Exception, e:
1269            raise CredReposError("Checking for user \"%s\":" % (dn, e))
1270
1271       
1272        # Carry out check? - filter out certs in db where a new cert
1273        # supercedes it - i.e. expires later and has the same roles
1274        # assigned - May be too complicated to implement
1275        #uniqAttCertList = [attCert for attCert in attCertList \
1276        #    if min([attCert == cred.attCert for cred in userCred])]
1277       
1278               
1279        # Update database with new entries
1280        try:
1281            for attCert in attCertList:
1282                self.UserCredential(dn=dn, attCert=attCert.asString())
1283
1284        except _mysql_exceptions.InterfaceError, e:
1285            raise CredReposError("Adding new user credentials for " + \
1286                                 "user %s: %s" % (dn,"MySQL interface error"))
1287        except Exception, e:
1288            raise CredReposError("Adding new user credentials for " + \
1289                                 "user %s: %s" % (dn, e))
1290
1291
1292    def _initTables(self, prompt=True):
1293        """Use with EXTREME caution - this method will initialise the database
1294        tables removing any previous records entered"""
1295 
1296        if prompt:
1297            resp = raw_input(\
1298        "Are you sure you want to initialise the database tables? (yes/no)")
1299   
1300            if resp.upper() != "YES":
1301                print "Tables unchanged"
1302                return
1303       
1304        self.User.createTable()
1305        self.UserCredential.createTable()
1306        print "Tables created"
1307
1308           
1309    #_________________________________________________________________________
1310    # Database tables defined using SQLObject derived classes
1311    # Nb. These are class variables of the SessionMgrCredRepos class
1312    class User(SQLObject):
1313        """SQLObject derived class to define Credentials Repository db table
1314        to store user information"""
1315
1316        # to be assigned to connectionForURI(<db URI>)
1317        _connection = None
1318
1319        # Force table name
1320        _table = "User"
1321
1322        userName = StringCol(dbName='userName', length=30)
1323        dn = StringCol(dbName='dn', length=128)
1324
1325
1326    class UserCredential(SQLObject):
1327        """SQLObject derived class to define Credentials Repository db table
1328        to store user credentials information"""
1329
1330        # to be assigned to connectionForURI(<db URI>)
1331        _connection = None
1332
1333        # Force table name
1334        _table = "UserCredential"
1335
1336       
1337        # User name field binds with UserCredential table
1338        dn = StringCol(dbName='dn', length=128)
1339
1340        # Store complete attribute certificate text
1341        attCert = StringCol(dbName='attCert')
Note: See TracBrowser for help on using the repository browser.