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

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

NDG/Session.py:

  • UserSession? createSessID -> addNewSessID public method to enable more than one session ID

to be tied to one session.

  • deleteUserSession - fix to input keyword args

NDG/CredWallet.py: fix to reqAuthorisation authzResp check.

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