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

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

NDG/SessionClient.py: ! Will be moved to SecurityClient?.py !
NDG/Session.py:

  • SessionMgr?.addUser now DOES NOT add a record into the Credential Repository for a new user.

This is to keep the CredentialRepository? and MyProxy? separate. MyProxy? add user method may
be removed from SessionMgr? WS or moved to its own MyProxy? WS for extra security.

table used by Postgres. Mike Grant came across this problem with the PML installation.

UserID entry, add one. Previously a UserID entry was made when the user first registers but
see above. Note, userName field is probably now superfluous.

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