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

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

NDG/SecurityCGI.py: rationalised from original code taken from xDomainCredTransfer.py. Needs
testing.

NDG/Session.py: added class methods to UserSession? for NDG security sessino cookie handling.

createSecurityCookie - makes a new cookie
isValidSecuirtyCookie - checks an existing cookie to see if it has the write tags for an NDG
security cookie.

These have been made class methods so that they can by called without making a UserSession?
instance. This is useful for creating a new cookie from an existing one as is needed when
cookie information is communicated across domains.

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