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

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

NDG/Session.py: fixed bug in SessionMgr?.readProperties - not stripping whitespace correctly for field values.

  • 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                    missingKeys.append(elem.tag)
588               
589        except Exception, e:
590            raise SessionMgrError, \
591                "Error parsing tag \"%s\" in properties file \"%s\": %s" % \
592                (elem.tag, propFilePath, e)
593
594
595        if missingKeys != []:
596            raise SessionMgrError, "The following properties are " + \
597                                   "missing from the properties file: " + \
598                                   ', '.join(missingKeys)
599
600
601    #_________________________________________________________________________
602    def setProperties(self, **prop):
603        """Update existing properties from an input dictionary
604        Check input keys are valid names"""
605       
606        for key in prop.keys():
607            if key not in self.__validKeys:
608                raise SessionMgrError("Property name \"%s\" is invalid" % key)
609
610
611        for key, value in prop.items():
612                       
613            if key == 'myProxyProp':
614                self.__myPx.setProperties(prop[key])
615   
616            elif key == 'credReposProp':
617                self.__credRepos.setProperties(prop[key])
618
619            elif key in self.__validKeys:
620                # Only update other keys if they are not None or ""
621                if value:
622                    self.__prop[key] = value               
623            else:
624                raise SessionMgrError(\
625                    "Key \"%s\" is not a valid Session Manager property" %
626                    key)
627
628
629    #_________________________________________________________________________
630    def addUser(self, caConfigFilePath=None, caPassPhrase=None, **reqKeys):       
631        """Register a new user with NDG data centre
632       
633        addUser([caConfigFilePath, ]|[, caPassPhrase]
634                |[, userName=u, pPhrase=p])
635
636        returns XML formatted response message
637       
638        caConfigFilePath|caPassPhrase:  pass phrase for SimpleCA's
639                                        certificate.  Set via file or direct
640                                        string input respectively.  Set here
641                                        to override setting [if any] made at
642                                        object creation.
643       
644                                        Passphrase is only required if
645                                        SimpleCA is instantiated on the local
646                                        machine.  If SimpleCA WS is called no
647                                        passphrase is required.
648                                       
649        **reqKeys:                      use as alternative to
650                                        reqXMLtxt keyword - pass in
651                                        username and pass-phrase for new user
652                                        unencrypted as keywords username
653                                        and pPhrase respectively.  See
654                                        SessionMgrIO.AddUserRequest class for
655                                        reference."""
656             
657        try:
658            # Add new user certificate to MyProxy Repository
659            #
660            # Changed so that record is NOT added to UserID table of
661            # CredentialRepository.  Instead, a check can be made when a new
662            # Attribute Certificate credential is added: if no entry is
663            # present for the user, add them into the UserID table at this
664            # point.
665            #
666            # By removing the add record call, MyProxy and Credential
667            # Repository can be independent of one another
668            user = self.__myPx.addUser(reqKeys['userName'],
669                                       reqKeys['pPhrase'],
670                                       caConfigFilePath=caConfigFilePath,
671                                       caPassPhrase=caPassPhrase,
672                                       retDN=True)           
673        except Exception, e:
674            return AddUserResp(errMsg=str(e))
675
676        return AddUserResp(errMsg='')
677   
678   
679    #_________________________________________________________________________       
680    def connect(self, **reqKeys):       
681        """Create a new user session or connect to an existing one:
682
683        connect([getCookie=True/False][createServerSess=Tue/False, ]
684                [, userName=u, pPhrase=p]|[, proxyCert=px]|[, sessID=id])
685
686        getCookie:              If True, allocate a user session with a
687                                wallet in the session manager and return a
688                                cookie containing the new session ID
689                                allocated.  If set False, return a proxy
690                                certificate only.  The client is then
691                                responsible for Credential Wallet management.
692        createServerSess:       If set to True, the SessionMgr will create
693                                and manage a session for the user.  Nb.
694                                this flag is ignored and set to True if
695                                getCookie is set.  For command line case,
696                                where getCookie is False, it's possible
697                                to choose to have a client or server side
698                                session using this keyword.
699        reqXMLtxt:              encrypted XML containing user credentials -
700                                user name, pass-phrase or proxy cert etc
701        reqKeys:                username and pass-phrase or the proxy"""
702       
703
704        if 'sessID' in reqKeys:
705           
706            # Connect to an existing session identified by a session ID and
707            # return equivalent proxy cert
708            userSess = self.__connect2UserSession(sessID=sessID)
709            return ConnectResp(proxyCert=userSess.credWallet.proxyCertTxt)
710       
711        elif 'proxyCert' in reqKeys:
712            # Connect to an existing session identified by a proxy
713            # certificate and return an equivalent session cookie
714            userSess = self.__connect2UserSession(proxyCert=proxyCert)
715            sessCookie = userSess.createCookie(self.__prop['sessMgrWSDLuri'],
716                                               self.__prop['sessMgrEncrKey'])
717            return ConnectResp(sessCookie=sessCookie)
718       
719        else:
720            # Create a fresh session
721            try:           
722                # Get a proxy certificate to represent users ID for the new
723                # session
724                proxyCert = self.__myPx.getDelegation(reqKeys['userName'], 
725                                                      reqKeys['pPhrase'])   
726            except Exception, e:
727                raise SessionMgrError, "Delegating from MyProxy: %s" % e
728
729            bGetCookie = 'getCookie' in reqKeys and reqKeys['getCookie']
730                                               
731            bCreateServerSess = 'createServerSess' in reqKeys and \
732                                            reqKeys['createServerSess']
733                                           
734            if bGetCookie or bCreateServerSess:
735                # Session Manager creates and manages user's session
736                userSess = self.__createUserSession(proxyCert)
737 
738                               
739            if bGetCookie:
740               
741                # Web browser client - Return session cookie
742                userSess.cookieDomain = self.__prop['cookieDomain']
743
744                sessCookie = userSess.createCookie(\
745                                            self.__prop['sessMgrWSDLuri'],
746                                            self.__prop['sessMgrEncrKey'])
747               
748                try:
749                    # Encrypt response if a client public key is available
750                    return ConnectResp(sessCookie=sessCookie)
751               
752                except Exception, e:
753                    raise SessionMgrError, \
754                                "Error formatting connect response: %s" % e             
755            else:
756                # NDG Command line client - Return proxy certificate
757                return ConnectResp(proxyCert=proxyCert)       
758       
759       
760    #_________________________________________________________________________       
761    def __createUserSession(self, proxyCert):
762        """Create a new user session from input user credentials       
763        and return
764       
765        """
766       
767        try:   
768            # Check for an existing session for the same user
769            try:
770                userDN = str(X509CertParse(proxyCert).dn)
771               
772            except Exception, e:
773                raise SessionMgrError, \
774                "Parsing input proxy certificate DN for session create: %s"%\
775                                                                        str(e)
776
777            if userDN in self.__dnDict:
778                # Update existing session with proxy cert and add a new
779                # session ID to access it - a single session can be accessed
780                # via multiple session IDs e.g. a user may wish to access the
781                # same session from the their desktop PC and their laptop.
782                # Different session IDs are allocated in each case.
783                userSess = self.__dnDict[userDN]
784                userSess.addNewSessID()
785               
786            else:
787                # Create a new user session using the new proxy certificate
788                # and session ID
789                #
790                # Nb. Client pub/pri key info to allow message level
791                # encryption for responses from Attribute Authority WS
792                userSess = UserSession(proxyCert, 
793                                   caPubKeyFilePath=self.__prop['caCertFile'],
794                                   clntPubKeyFilePath=self.__prop['certFile'],
795                                   clntPriKeyFilePath=self.__prop['keyFile'],
796                                   clntPriKeyPwd=self.__prop['keyPPhrase'],
797                                   credRepos=self.__credRepos)
798
799                # Also allow access by user DN
800                self.__dnDict[userDN] = userSess
801
802                               
803            newSessID = userSess.latestSessID
804           
805            # Check for unique session ID
806            if newSessID in self.__sessDict:
807                raise SessionMgrError, \
808                    "New Session ID is already in use:\n\n %s" % newSessID
809
810            # Add new session to list                 
811            self.__sessDict[newSessID] = userSess
812                   
813            # Return new session
814            return userSess
815       
816        except Exception, e:
817            raise SessionMgrError, "Creating User Session: %s" % str(e)
818
819
820    #_________________________________________________________________________       
821    def __connect2UserSession(self, **idKeys):
822        """Connect to an existing session by providing a valid session ID or
823        proxy certificate
824
825        __connect2UserSession([proxyCert]|[sessID])
826       
827        proxyCert:    proxy certificate string corresponding to an existing
828                      session to connect to.
829        sessID:       similiarly, a web browser session ID linking to an
830                      an existing session."""
831       
832           
833        # Look for a session corresponding to this ID
834        if 'sessID' in idKeys:
835            try:
836                # Check matched session has not expired
837                userSess = self.__sessDict[idKeys['sessID']]
838               
839            except KeyError:
840                # User session not found with given ID
841                raise SessionMgrError, \
842                        "No user session found matching input session ID"
843                       
844            try:
845                userSess.credWallet.isValid(raiseExcep=True)
846                return userSess
847                       
848            except Exception, e:
849                raise SessionMgrError, \
850                        "Matching session ID to existing user session: %s" % e
851               
852       
853        elif 'proxyCert' in idKeys:
854            try:
855                userDN = str(X509CertParse(idKeys['proxyCert']).dn)
856               
857            except Exception, e:
858                raise SessionMgrError, \
859                "Parsing input proxy certificate DN for session connect: %s"%\
860                                                                        str(e)
861            try:
862                userSess = self.__dnDict[userDN]
863                       
864            except KeyError:
865                # User session not found with given proxy cert
866                raise SessionMgrError, \
867                    "No user session found matching input proxy certificate"
868                   
869            try:
870                # Check matched session has not expired
871                userSess.credWallet.isValid(raiseExcep=True)
872                return userSess
873                                       
874            except Exception, e:
875                raise SessionMgrError(\
876                "Matching proxy certificate to existing user session: %s" % e)
877        else:
878            raise SessionMgrError,\
879                                '"sessID" or "proxyCert" keywords must be set'
880
881
882    #_________________________________________________________________________       
883    def deleteUserSession(self, sessID=None, proxyCert=None):
884        """Delete an existing session by providing a valid session ID or
885        proxy certificate - use for user logout
886
887        __deleteUserSession([proxyCert]|[sessID])
888       
889        proxyCert:    proxy certificate corresponding to an existing
890                      session to connect to.
891        sessID:       similiarly, a web browser session ID linking to an
892                      an existing session."""
893       
894           
895        # Look for a session corresponding to the session ID/proxy cert.
896        if sessID:
897            try:
898                userSess = self.__sessDict[sessID]
899               
900            except KeyError:
901                raise SessionMgrError, \
902                    "Deleting user session - no matching session ID exists"
903
904            # Get associated user Distinguished Name
905            userDN = userSess.credWallet.proxyCert.dn
906           
907        elif proxyCert:
908            try:
909                userDN = str(X509CertParse(idKeys['proxyCert']).dn)
910               
911            except Exception, e:
912                raise SessionMgrError, \
913                "Parsing input proxy certificate DN for session connect: %s"%\
914                                                                        str(e)
915            try:
916                userSess = self.__dnDict[userDN]
917                       
918            except KeyError:
919                # User session not found with given proxy cert
920                raise SessionMgrError, \
921                    "No user session found matching input proxy certificate"
922        else:
923            # User session not found with given ID
924            raise SessionMgrError, \
925                                '"sessID" or "proxyCert" keywords must be set'
926 
927        # Delete associated sessions
928        try:
929            # Each session may have a number of session IDs allocated to
930            # it
931            for userSessID in userSess.sessIDlist:
932                del self.__sessDict[userSessID]
933
934            del self.__dnDict[userDN]
935           
936        except Exception, e:
937            raise SessionMgrError, "Deleting user session: %s" % e       
938
939
940    #_________________________________________________________________________
941    def reqAuthorisation(self, **reqKeys):
942        """For given sessID, request authorisation from an Attribute Authority
943        given by aaWSDL.  If sucessful, an attribute certificate is
944        returned.
945
946        **reqKeys:            pass equivalent to XML as keywords instead.
947                              See SessionMgrIO.AuthorisationReq class
948        """
949       
950        # Web browser client input will include the encrypted address of the
951        # Session Manager where the user's session is held.
952        if 'encrSessMgrWSDLuri' in reqKeys:
953           
954            # Decrypt the URI for where the user's session resides
955            userSessMgrWSDLuri = UserSession.decrypt(\
956                                                reqKeys['encrSessMgrWSDLuri'],
957                                                self.__prop['sessMgrEncrKey'])
958                                               
959            # Check the address against the address of THIS Session Manager 
960            if userSessMgrWSDLuri != self.__prop['sessMgrWSDLuri']:
961               
962                # Session is held on a remote Session  Manager
963                userSessMgrResp = self.__redirectAuthorisationReq(\
964                                                        userSessMgrWSDLuri,
965                                                        **reqKeys)
966
967                # Reset response by making a new AuthorisationResp object
968                # The response from the remote Session Manager will still
969                # contain the encrypted XML sent by it.  This should be
970                # discarded
971                return userSessMgrResp
972
973           
974        # User's session resides with THIS Session Manager / no encrypted
975        # WSDL address passed in (as in command line context for security) ...
976
977           
978        # Retrieve session corresponding to user's session ID using relevant
979        # input credential
980        idKeys = {}
981        if 'sessID' in reqKeys:
982            idKeys['sessID'] = reqKeys['sessID']
983           
984        elif 'proxyCert' in reqKeys:
985            idKeys['proxyCert'] = reqKeys['proxyCert']           
986        else:
987            raise SessionMgrError,'Expecting "sessID" or "proxyCert" keywords'
988                               
989        userSess = self.__connect2UserSession(**idKeys)
990
991
992        # Copy keywords to be passed onto the request to the attribute
993        # authority
994        #
995        # Nb. the following keys aren't required
996        delKeys = ('proxyCert',
997                   'sessID',
998                   'encrCert',
999                   'encrSessMgrWSDLuri', 
1000                   'aaPubKey')
1001                   
1002        aaKeys = dict([i for i in reqKeys.items() if i[0] not in delKeys])
1003
1004
1005        if 'aaPubKey' not in reqKeys:
1006            # Get public key using WS
1007            try:
1008                aaClnt = AttAuthorityClient(aaWSDL=reqKeys['aaWSDL'])               
1009                reqKeys['aaPubKey'] = aaClnt.getPubKey()
1010
1011            except Exception, e:
1012                raise SessionMgrError, \
1013                    "Retrieving Attribute Authority public key: "+ str(e)
1014                               
1015                                                       
1016        # Make a temporary file to hold Attribute Authority Public Key. 
1017        # The Credential Wallet needs this to encrypt requests to the
1018        # Attribute Authority
1019        try:
1020            aaPubKeyTmpFile = tempfile.NamedTemporaryFile()
1021            open(aaPubKeyTmpFile.name, "w").write(reqKeys['aaPubKey'])
1022            aaKeys['aaPubKeyFilePath'] = aaPubKeyTmpFile.name
1023           
1024        except IOError, (errNo, errMsg):
1025            raise SessionMgrError, "Making temporary file for Attribute " + \
1026                                  "Authority public key: %s" % errMsg
1027               
1028        except Exception, e:
1029            raise SessionMgrError, "Making temporary file for Attribute " + \
1030                                  "Authority public key: %s" % str(e)
1031
1032                                             
1033        # User's Credential Wallet carries out authorisation request to the
1034        # Attribute Authority
1035        try:
1036            attCert = userSess.credWallet.reqAuthorisation(**aaKeys)
1037           
1038            # AuthorisationResp class formats a response message in XML and
1039            # allow dictionary-like access to XML tags
1040            resp = AuthorisationResp(attCert=attCert, 
1041                                     statCode=AuthorisationResp.accessGranted)
1042           
1043        except CredWalletAuthorisationDenied, e:
1044            # Exception object containa a list of attribute certificates
1045            # which could be used to re-try to get authorisation via a mapped
1046            # certificate
1047            resp = AuthorisationResp(extAttCertList=e.extAttCertList,
1048                                     statCode=AuthorisationResp.accessDenied,
1049                                     errMsg=str(e))
1050       
1051        except Exception, e:
1052            # Some other error occured - create an error Authorisation
1053            # response
1054            resp = AuthorisationResp(statCode=AuthorisationResp.accessError,
1055                                     errMsg=str(e))
1056   
1057        return resp
1058
1059
1060    #_________________________________________________________________________
1061    def __redirectAuthorisationReq(self, userSessMgrWSDLuri, **reqKeys):
1062        """Handle case where User session resides on another Session Manager -
1063        forward the request"""
1064       
1065        # Instantiate WS proxy for remote session manager
1066        try:
1067            sessClnt = SessionClient(smWSDL=userSessMgrWSDLuri,
1068                                 clntPubKeyFilePath=self.__prop['certFile'],
1069                                 clntPriKeyFilePath=self.__prop['keyFile'])           
1070        except Exception, e:
1071            raise SessionMgrError(\
1072                        "Re-directing authorisation request to \"%s\": %s" % \
1073                        (userSessMgrWSDLuri, str(e)))
1074
1075           
1076        # Call remote session manager's authorisation request method
1077        # and return result to caller
1078        try:
1079            # encrCert key not needed - it gets set above via
1080            # 'clntPubKeyFilePath'
1081            if 'encrCert' in reqKeys:
1082                del reqKeys['encrCert']
1083               
1084            # Call remote SessionMgr where users session lies
1085            redirectAuthResp = sessClnt.reqAuthorisation(\
1086                                    clntPriKeyPwd=self.__prop['keyPPhrase'],
1087                                    **reqKeys)
1088         
1089            return redirectAuthResp
1090       
1091        except Exception, e:
1092            raise SessionMgrError(\
1093        "Forwarding Authorisation request for Session Manager \"%s\": %s" %\
1094                (userSessMgrWSDLuri, e))
1095
1096
1097    #_________________________________________________________________________
1098    def auditCredRepos(self):
1099        """Remove expired Attribute Certificates from the Credential
1100        Repository"""
1101        self.__credRepos.auditCredentials()
1102
1103
1104#_____________________________________________________________________________
1105class SessionMgrCredRepos(CredRepos):
1106    """Interface to Credential Repository Database
1107   
1108    Nb. inherits from CredWallet.CredRepos to ensure correct interface
1109    to the wallet"""
1110
1111    # valid configuration property keywords
1112    __validKeys = ['dbURI']
1113   
1114
1115    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
1116        """Initialise Credentials Repository Database object.
1117
1118        If the connection string or properties file is set a connection
1119        will be made
1120
1121        dbURI:              <db type>://<username>:<passwd>@<hostname>/dbname
1122        propFilePath: file path to properties file
1123
1124        Nb. propFilePath setting overrides input dbURI
1125        """
1126           
1127        self.__con = None
1128        self.__prop = {}
1129       
1130        if propFilePath is not None:
1131           
1132            # Read database URI set in file
1133            self.readProperties(propFilePath, dbPPhrase=dbPPhrase)
1134           
1135        elif prop != {}:
1136           
1137            # Database URI may have been set as an input keyword argument
1138            self.setProperties(dbPPhrase=dbPPhrase, **prop)
1139
1140
1141
1142
1143    def __setConnection(self,
1144                        dbType=None,
1145                        dbUserName=None,
1146                        dbPPhrase=None,
1147                        dbHostname=None,
1148                        dbName=None,
1149                        dbURI=None,
1150                        chkConnection=True):
1151        """Establish a database connection from a database URI
1152
1153        pass a URI OR the parameters to construct the URI
1154           
1155        dbURI: "<db type>://<username>:<passwd>:<hostname>/dbname"
1156
1157        or
1158
1159        dbURI: "<db type>://<username>:%PPHRASE%:<hostname>/dbname"
1160        + passPhrase
1161
1162        - %PPHRASE% is substituted with the input passPhrase keyword
1163       
1164        or
1165       
1166        dbType:         database type e.g. 'mysql'
1167        dbUserName:     username
1168        dbPPhrase:      pass-phrase
1169        dbHostname:     name of host where database resides
1170        dbName:         name of the database
1171
1172
1173        chkConnection:  check that the URI is able to connect to the
1174        """
1175
1176        try:
1177            if dbURI:
1178                # Check for pass-phrase variable set in URI '%PPHRASE%'
1179                dbURIspl = dbURI.split('%')
1180                if len(dbURIspl) == 3:
1181                   
1182                    if dbPPhrase is None:
1183                        raise CredReposError("No database pass-phrase set")
1184                   
1185                    dbURI = dbURIspl[0] + dbPPhrase + dbURIspl[2]
1186               
1187            else:
1188                # Construct URI from individual inputs
1189                dbURI = dbType + '://' + dbUserName + ':' + dbPPhrase + \
1190                        ':' + dbHostname + '/' + dbName
1191        except Exception, e:
1192            # Checking form missing keywords
1193            raise CredReposError("Error creating database URI: %s" % e)
1194
1195        try:
1196            self.__con = connectionForURI(dbURI)
1197        except Exception, e:
1198            raise CredReposError("Error creating database connection: %s" % e)
1199
1200        if chkConnection:
1201            try:
1202                self.__con.makeConnection()
1203               
1204            except Exception, e:
1205                raise CredReposError(\
1206                    "Error connecting to Credential Repository: %s" % e)
1207
1208           
1209        # Copy the connection object into the table classes
1210        SessionMgrCredRepos.UserID._connection = self.__con
1211        SessionMgrCredRepos.UserCredential._connection = self.__con
1212         
1213
1214
1215
1216    def setProperties(self, dbPPhrase=None, **prop):
1217        """Update existing properties from an input dictionary
1218        Check input keys are valid names"""
1219       
1220        for key in prop.keys():
1221            if key not in self.__validKeys:
1222                raise CredReposError("Property name \"%s\" is invalid" % key)
1223               
1224        self.__prop.update(prop)
1225
1226
1227        # Update connection setting
1228        if 'dbURI' in prop:
1229            self.__setConnection(dbURI=prop['dbURI'],
1230                                 dbPPhrase=dbPPhrase)
1231               
1232
1233
1234       
1235    def readProperties(self,
1236                       propFilePath=None,
1237                       propElem=None,
1238                       dbPPhrase=None):
1239        """Read the configuration properties for the CredentialRepository
1240
1241        propFilePath|propElem
1242
1243        propFilePath: set to read from the specified file
1244        propElem:     set to read beginning from a cElementTree node"""
1245
1246        if propFilePath is not None:
1247
1248            try:
1249                tree = ElementTree.parse(propFilePath)
1250                propElem = tree.getroot()
1251               
1252            except IOError, e:
1253                raise CredReposError(\
1254                                "Error parsing properties file \"%s\": %s" % \
1255                                (e.filename, e.strerror))
1256
1257            except Exception, e:
1258                raise CredReposError("Error parsing properties file: %s" % \
1259                                    str(e))
1260
1261        if propElem is None:
1262            raise CredReposError("Root element for parsing is not defined")
1263
1264
1265        # Read properties into a dictionary
1266        prop = {}
1267        for elem in propElem:
1268                   
1269            # Check for environment variables in file paths
1270            tagCaps = elem.tag.upper()
1271            if 'FILE' in tagCaps or 'PATH' in tagCaps or 'DIR' in tagCaps:
1272                elem.text = os.path.expandvars(elem.text)
1273
1274            prop[elem.tag] = elem.text
1275           
1276        self.setProperties(dbPPhrase=dbPPhrase, **prop)
1277
1278           
1279
1280    def addUser(self, userName, dn):
1281        """A new user to Credentials Repository"""
1282        try:
1283            self.UserID(userName=userName, dn=dn)
1284
1285        except Exception, e:
1286            raise CredReposError("Error adding new user '%s': %s" % \
1287                                                        (userName, e))
1288
1289
1290
1291                           
1292    def auditCredentials(self, dn=None, **attCertValidKeys):
1293        """Check the attribute certificates held in the repository and delete
1294        any that have expired
1295
1296        dn:                Only audit for the given user distinguished Name.
1297                           if not set, all records are audited
1298        attCertValidKeys:  keywords which set how to check the Attribute
1299                           Certificate e.g. check validity time, XML
1300                           signature, version etc.  Default is check
1301                           validity time only"""
1302
1303        if attCertValidKeys == {}:
1304            # Default to check only the validity time
1305            attCertValidKeys = {    'chkTime':          True,
1306                                    'chkVersion':       False,
1307                                    'chkProvenance':    False,
1308                                    'chkSig':           False }
1309           
1310        try:
1311            if dn:
1312                # Only audit for the given user distinguished Name
1313                credList = self.UserCredential.selectBy(dn=dn)
1314            else:
1315                # Audit all credentials
1316                credList = self.UserCredential.select()
1317           
1318        except Exception, e:
1319            raise CredReposError("Selecting credentials from repository: %s",\
1320                                 e)
1321
1322        # Iterate through list of credentials deleting records where the
1323        # certificate is invalid
1324        try:
1325            for cred in credList:
1326                attCert = AttCertParse(cred.attCert)
1327               
1328                if not attCert.isValid(**attCertValidKeys):
1329                    self.UserCredential.delete(cred.id)
1330                   
1331        except Exception, e:
1332            try:
1333                raise CredReposError("Deleting credentials for '%s': %s",
1334                                                       (cred.dn, e))
1335            except:
1336                raise CredReposError("Deleting credentials: %s", e)
1337
1338
1339
1340
1341    def getCredentials(self, dn):
1342        """Get the list of credentials for a given user's DN"""
1343
1344        try:
1345            return self.UserCredential.selectBy(dn=dn)
1346           
1347        except Exception, e:
1348            raise CredReposError("Selecting credentials for %s: %s" % (dn, e))
1349
1350
1351
1352       
1353    def addCredentials(self, dn, attCertList):
1354        """Add new attribute certificates for a user.  The user must have
1355        been previously registered in the repository
1356
1357        dn:             users Distinguished name
1358        attCertList:   list of attribute certificates"""
1359       
1360        try:
1361            userCred = self.UserID.selectBy(dn=dn)
1362           
1363            if userCred.count() == 0:
1364                # Add a new user record HERE instead of at user registration
1365                # time.  This decouples CredentialRepository from MyProxy and
1366                # user registration process. Previously, a user not recognised
1367                # exception would have been raised here.  'userName' field
1368                # of UserID table is now perhaps superfluous.
1369                #
1370                # P J Kershaw 26/04/06
1371                self.addUser(X500DN(dn)['CN'], dn)
1372
1373        except Exception, e:
1374            raise CredReposError("Checking for user \"%s\": %s"%(dn, str(e)))
1375
1376       
1377        # Carry out check? - filter out certs in db where a new cert
1378        # supercedes it - i.e. expires later and has the same roles
1379        # assigned - May be too complicated to implement
1380        #uniqAttCertList = [attCert for attCert in attCertList \
1381        #    if min([attCert == cred.attCert for cred in userCred])]
1382       
1383               
1384        # Update database with new entries
1385        try:
1386            for attCert in attCertList:
1387                self.UserCredential(dn=dn, attCert=attCert.asString())
1388
1389        except Exception, e:
1390            raise CredReposError("Adding new user credentials for " + \
1391                                 "user %s: %s" % (dn, str(e)))
1392
1393
1394    def _initTables(self, prompt=True):
1395        """Use with EXTREME caution - this method will initialise the database
1396        tables removing any previous records entered"""
1397 
1398        if prompt:
1399            resp = raw_input(\
1400        "Are you sure you want to initialise the database tables? (yes/no) ")
1401   
1402            if resp.upper() != "YES":
1403                print "Tables unchanged"
1404                return
1405       
1406        self.UserID.createTable()
1407        self.UserCredential.createTable()
1408        print "Tables created"
1409
1410           
1411    #_________________________________________________________________________
1412    # Database tables defined using SQLObject derived classes
1413    # Nb. These are class variables of the SessionMgrCredRepos class
1414    class UserID(SQLObject):
1415        """SQLObject derived class to define Credentials Repository db table
1416        to store user information"""
1417
1418        # to be assigned to connectionForURI(<db URI>)
1419        _connection = None
1420
1421        # Force table name
1422        _table = "UserID"
1423
1424        userName = StringCol(dbName='userName', length=30)
1425        dn = StringCol(dbName='dn', length=128)
1426
1427
1428    class UserCredential(SQLObject):
1429        """SQLObject derived class to define Credentials Repository db table
1430        to store user credentials information"""
1431
1432        # to be assigned to connectionForURI(<db URI>)
1433        _connection = None
1434
1435        # Force table name
1436        _table = "UserCredential"
1437
1438       
1439        # User name field binds with UserCredential table
1440        dn = StringCol(dbName='dn', length=128)
1441
1442        # Store complete attribute certificate text
1443        attCert = StringCol(dbName='attCert')
Note: See TracBrowser for help on using the repository browser.