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

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

Fixes for CEH and PML installations...

conf/sessionMgrProperties.xml: added missing <simpleCASrvProp> tag.
NDG/SimpleCA.py: fixed bug in setProperties method - lTime var was ref'd but not set.
NDG/SessionClient.py: addUser method - move if 'errMsg' ... block out of exception handling
block for greater efficiency.
NDG/Session.py: addUser method of SessionMgr? class - fixed bug return AddUserResp? instance
instead of trying to raise an exception with it.

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