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

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

Updates to Tests/security.py and NDG/SecurityCGI.py: call to AA to get login hosts works but no further.

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