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

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

NDG/Session.py: SessionMgr? class to make user session access more efficient dnDict and sessDict dictionaries
enable a user session to be indexed by proxy cert. DN and session ID respectively.

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