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

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

setup.py, README: altered version info to post Alpha development
NDG/LogClient.py, SimpleCAClient.py and GatekeeperClient?.py: catch HTTPResponse error from ZSI ServiceProxy?. This
allows more error info for file not found type problems.

NDG/Session.py, NDG/CredWallet.py: use AttAuthorityClient? wrapper rather than direct calls to ZSI.

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