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

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

* Working version for mapped certificates *

ndgSessionClient.py:

  • renamed dict to argDict to avoid clash with existing arg var.
  • added code for handling ext attCert list and trusted host list files.
  • Changed so that --connect and --req-autho can be specified together so that a connect call is

concatenated with a call to request authorisation.

attAuthorityIOtest.py: unit tests for AttAuthorityIO classes.
attCertTest.py: unit tests for AttCert?.

AttAuthorityIO.py:

attAuthority_services_server.py: update and fixes to GetTrustedHostInfo? WS stub.

AttAuthority?.py:

AttCert?.py: !! important fix - added nonzero method so that test on AttCert? instance
yields True e.g. if attCert: ...

Session.py:

  • SessionMgr?.readProperties - only strip white space from XML data if it's string type -

elementtree sets content to None if an empty tag is present in the XML it's reading.

CredWallet?.py: changes to WS calls - use AttAuthorityIO classes - AuthorisationReq/?
AuthorisationResp? + TrustedHostInfoReq/TrustedHostInfoResp?.

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