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

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

Tests/AttAuthorityIOtest.py: added testTrustedHosts3 to test results from livglue

Tests/security.py: returnURI input to init isn't needed - it can be picked up from as a form variable.

NDG/SecurityCGI.py: returnCreds renamed to returnCredsResponse. This method now checks the domain of the input
cookie. If this matches that of the returnURI, then there is no need to return the cookie information as they
can share the cookie.

NDG/Session.py: Created a metaclass _MetaUserSession for UserSession?. This enables UserSession? to have read-
only class variables - e.g. "cookieTags"

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