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

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

ndgSetup.sh: fixed slight typo.

mapConfig.xml: added pubKey tag to allow client to Attribute Authority to use it to encrypt
outbound messages to it.

ndgSessionClient.py:

  • include code to set public key of Attribute Authority so that Session Manager can encrypt

messages to it.

  • -r/--req-autho option now requires the AA WSDL URI. -a is now used to set the AA pub key
  • see previous point.

AttAuthorityIO.py:

  • Changed tag 'clntCert' to 'encrCert' so as to be consistent with SessionMgrIO.py code.

attAuthority_services_server.py:

  • Moved encrypt/decrypt code here from AttAuthority? class to be consistent with

sessionMgr_services_server.py.

AttAuthority?.py:

  • Now inherits from dict to allow convenient access to properties file parameters as dictionary

items.

  • Added code to include pubKey tag from mapConfig file in trustedHostInfo returned from

getTrustedHostInfo.

SessionMgrIO.py:

output XML.

  • Shifted test code into separate file in Tests/

SessionClient?.py:

  • Added aaPubKey to reqAuthorisation method - see above re. passing AA public key for

encryption of messages.

sessionMgr_services_server.py:

  • Changes to comments.

Session.py:

private key info of client to allow encrypt of responses from other WSs that SessionMgr? calls.
These are actually passed into CredWallet? instance of UserSession?.

  • AA Public key is passed into reqAuthorisation. This is written to a temp file for use by

XMLSec encryption code.

CredWallet?.py:

  • CredWalletAuthorisationDenied? - make sure extAttCertList gets set to []
  • Added pub/private functionality for encryption of messages to and from Attribute Authorities.
  • reqAuthorisation and getAATrustedHostInfo methods - read in client public key using

straight open/read: using X509Cert.asString() misses out the actual MIME encoded cert text(!)

  • Changed reqAuthorisation() - a required role is now optional with mapFromTrustedHosts flag set.

It does help though with finding a suitable AttCert? for mapping.

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