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

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

ndgSessionClient.py: added option for authorisation requests and also the ability to
combine --connect and --req-autho calls.

sessionMgrProperties.xml: new property cookieDomain enables a specific domain to be set for
session cookie.

Session.py: changes to implement the above - UserSession? has a cookieDomain attribute and
cookieDomain property.

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