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

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

NDG/Session.py: SessionMgr?.readProperties - fixed bug key pass-phrase not set.

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