source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/sessionmanager.py @ 4368

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/sessionmanager.py@4368
Revision 4368, 44.1 KB checked in by pjkersha, 12 years ago (diff)

Move ndg.security.server Session Manager and Attribute Authority back into top-level modules from their sub-package placings

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""NDG Security server side session management and security includes
2UserSession and SessionMgr classes.
3
4NERC Data Grid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "02/06/05"
8__copyright__ = "(C) 2007 STFC & NERC"
9__license__ = \
10"""This software may be distributed under the terms of the Q Public
11License, version 1.0 or later."""
12__contact__ = "P.J.Kershaw@rl.ac.uk"
13__revision__ = '$Id:sessionmanager.py 4367 2008-10-29 09:27:59Z pjkersha $'
14import logging
15log = logging.getLogger(__name__)
16
17# Modify sys.path when carrying out dynamic import for Credential Repository
18import sys, os
19
20# Base 64 encode session IDs if returned in strings - urandom's output may
21# not be suitable for printing!
22import base64
23
24# Time module for use with cookie expiry
25from time import strftime
26from datetime import datetime
27
28# For parsing of properties file
29try: # python 2.5
30    from xml.etree import cElementTree as ElementTree
31except ImportError:
32    # if you've installed it yourself it comes this way
33    import cElementTree as ElementTree
34
35# Session Manager URI in cookie
36from Crypto.Cipher import AES
37
38# Credential Wallet
39from ndg.security.common.CredWallet import CredWallet, CredentialRepository, \
40    CredWalletError, CredWalletAttributeRequestDenied
41   
42from ndg.security.common.wssecurity import WSSecurityConfig
43from ndg.security.common.X509 import X500DN, X509Cert, X509CertParse, \
44                                X509CertExpired, X509CertInvalidNotBeforeTime
45
46# Use client package to allow redirection of authorisation requests and
47# to retrieve Attribute Authority public key
48from ndg.security.common.SessionMgr import SessionMgrClient
49
50# generic parser to read INI/XML properties file
51from ndg.security.common.utils.ConfigFileParsers import \
52                                                INIPropertyFileWithValidation
53
54# utility to instantiate classes dynamically
55from ndg.security.common.utils.ClassFactory import instantiateClass
56
57
58class _SessionException(Exception):
59    """Base class for all Exceptions in this module.  Overrides Exception to
60    enable writing to the log"""
61    def __init__(self, msg):
62        log.error(msg)
63        Exception.__init__(self, msg)
64
65class UserSessionError(_SessionException):   
66    """Exception handling for NDG User Session class."""
67
68class InvalidUserSession(UserSessionError):   
69    """Problem with a session's validity"""
70
71class UserSessionExpired(UserSessionError):   
72    """Raise when session's X.509 cert. has expired"""
73
74class UserSessionX509CertNotBeforeTimeError(UserSessionError):   
75    """Raise when session's X.509 cert. not before time is before the current
76    system time"""
77   
78
79# Inheriting from 'object' allows Python 'new-style' class with Get/Set
80# access methods
81class UserSession(object):
82    """Session details - created when a user logs into NDG"""
83    sessIdLen = 32
84   
85    def __init__(self, **credWalletKeys):
86        """Initialise UserSession with keywords to CredWallet"""
87               
88        log.debug("UserSession.__init__ ...")
89       
90        # Each User Session has one or more browser sessions associated with
91        # it.  These are stored in a list
92        self.__sessIdList = []
93        self.addNewSessID()
94        self.__credWallet = CredWallet(**credWalletKeys)
95
96        log.info("Created a session with ID = %s" % self.__sessIdList[-1])
97
98    def __getCredWallet(self):
99        """Get Credential Wallet instance"""
100        return self.__credWallet
101   
102    credWallet = property(fget=__getCredWallet,
103                          doc="Read-only access to CredWallet instance")
104
105    def __getSessIdList(self):
106        """Get Session ID list - last item is latest allocated for this
107        session"""
108        return self.__sessIdList
109   
110    sessIdList = property(fget=__getSessIdList,
111                          doc="Read-only access to Session ID list")
112
113    def __latestSessID(self):
114        """Get the session ID most recently allocated"""
115        return self.__sessIdList[-1]
116   
117    # Publish as an attribute
118    latestSessID = property(fget=__latestSessID,
119                            doc="Latest Session ID allocated")
120
121    def addNewSessID(self):
122        """Add a new session ID to be associated with this UserSession
123        instance"""
124
125        # base 64 encode output from urandom - raw output from urandom is
126        # causes problems when passed over SOAP.  A consequence of this is
127        # that the string length of the session ID will almost certainly be
128        # longer than UserSession.sessIdLen
129        sessID = base64.urlsafe_b64encode(os.urandom(UserSession.sessIdLen))
130        self.__sessIdList.append(sessID)
131
132    @staticmethod
133    def encodeTxt(txt, encrKey=None):
134        """Encode Session Manager URI to allow inclusion in a web browser
135        session cookie
136       
137        The address is optionally encrypted and then base 64 encoded use a
138        URL safe encoding
139       
140        @type encrKey: string
141        @param encrKey: 16 char encryption key used to encrypt the URI.  If
142        omitted or set None, the URI is not encrypted but merely base 64
143        encoded"""
144       
145        if encrKey is not None:
146            # Text length must be a multiple of 16 for AES encryption
147            try:
148                mod = len(txt) % 16
149                nPad = mod and 16 - mod or 0
150                   
151                # Add padding
152                paddedURI = txt + ''.join(' '*nPad)
153            except Exception, e:
154                raise UserSessionError("Padding text for encryption: %s" % e)
155       
156            # Encrypt
157            try:
158                txt = AES.new(encrKey, AES.MODE_ECB).encrypt(paddedURI)
159           
160            except Exception, e:
161                raise UserSessionError("Encrypting text: %s" % e)
162
163        try:
164            return base64.urlsafe_b64encode(txt)
165       
166        except Exception, e:
167            raise UserSessionError("Encoding Session Manager URI: %s" % e)
168       
169    @staticmethod                                   
170    def decodeTxt(txt, encrKey=None):
171        """Decode the URI from cookie set by another Session Manager.  This
172        is required when reading a session cookie to find out which
173        Session Manager holds the client's session
174       
175        @type txt: string
176        @param txt: base 64 encoded encrypted text
177       
178        @type encrKey: string
179        @param encrKey: 16 char encryption key used to encrypt the URI.  If
180        omitted or set None, the URI is assumed to be unencrypted"""
181
182        try:
183            # Convert if unicode type - unicode causes TypeError with
184            # base64.urlsafe_b64decode
185            if isinstance(txt, unicode):
186                txt = str(txt)
187               
188            # Decode from base 64
189            b64DecodedEncrTxt = base64.urlsafe_b64decode(txt)
190           
191        except Exception, e:
192            raise SessionMgrError("Decoding text: %s" % e)         
193        if encrKey is not None:
194            try:
195                aes = AES.new(encrKey, AES.MODE_ECB)
196               
197                # Decrypt and strip trailing spaces
198                return aes.decrypt(b64DecodedEncrTxt).strip()
199           
200            except Exception, e:
201                raise SessionMgrError("Decrypting Session Manager URI: %s" % e)           
202        else:
203            return b64DecodedEncrTxt
204       
205class SessionMgrError(_SessionException):   
206    """Exception handling for NDG Session Manager class."""
207
208class SessionNotFound(SessionMgrError):
209    """Raise from SessionMgr._connect2UserSession when session ID is not
210    found in the Session dictionary"""
211
212
213class SessionMgr(dict):
214    """NDG authentication and session handling
215   
216    @type propertyDefaults: dict
217    @cvar propertyDefaults: list of the valid properties file element names and
218    sub-elements where appropriate
219   
220    @type _confDir: string
221    @cvar _confDir: configuration directory under $NDGSEC_DIR - default
222    location for properties file
223   
224    @type _propFileName: string
225    @cvar _propFileName: default file name for properties file under
226    _confDir
227   
228    @type credentialRepositoryPropertyDefaults: dict
229    @cvar credentialRepositoryPropertyDefaults: permitted properties file
230    elements for configuring the Crendential Repository.  Those set to
231    NotImplemented indicate properties that must be set.  For the others, the
232    value indicates the default if not present in the file"""
233
234    # valid configuration property keywords
235    AUTHN_KEYNAME = 'authNService'
236    CREDREPOS_KEYNAME = 'credentialRepository'
237    CREDWALLET_KEYNAME = 'credentialWallet'
238    defaultSectionName = 'sessionManager'
239   
240    authNServicePropertyDefaults = {
241        'modFilePath': None,
242        'modName': None,
243        'className': None,
244        'propertiesFile': None       
245    }
246   
247    credentialRepositoryPropertyDefaults = {
248        'modFilePath': None,
249        'modName': None,
250        'className': None,
251        'propertiesFile': None
252    }
253
254    propertyDefaults = {
255        'portNum':                None,
256        'useSSL':                 False,
257        'sslCertFile':            None,
258        'sslKeyFile':             None,
259        'sslCACertDir':           None,
260        AUTHN_KEYNAME:            authNServicePropertyDefaults, 
261        CREDWALLET_KEYNAME:       CredWallet.propertyDefaults,
262        CREDREPOS_KEYNAME:        credentialRepositoryPropertyDefaults
263    }
264
265    _confDir = "conf"
266    _propFileName = "sessionMgrProperties.xml"
267   
268    def __init__(self, 
269                 propFilePath=None, 
270                 propFileSection='DEFAULT',
271                 propPrefix='',
272                 **prop):       
273        """Create a new session manager to manager NDG User Sessions
274       
275        @type propFilePath: basestring
276        @param propFilePath: path to properties file
277        @type propFileSection: basestring
278        @param propFileSection: applies to ini format config files only - the
279        section to read the Session Managers settings from
280        set in properties file
281        @type prop: dict
282        @param **prop: set any other properties corresponding to the tags in
283        the properties file as keywords"""       
284
285        log.info("Initialising service ...")
286       
287        # Base class initialisation
288        dict.__init__(self)
289
290        # Key user sessions by session ID
291        self.__sessDict = {}
292
293        # Key user sessions by user DN
294        self.__dnDict = {}
295
296        # Finally, also key by username
297        self.__usernameDict = {}
298       
299        # Credential Repository interface only set if properties file is set
300        # otherwise explicit calls are necessary to set credentialRepositoryProp via
301        # setProperties/readProperties and then loadCredentialRepositoryInterface
302        self._credentialRepository = None
303   
304        # Set from input or use defaults based or environment variables
305        self.propFilePath = propFilePath
306       
307        self.propFileSection = propFileSection
308        self.propPrefix = propPrefix
309        self._cfg = None
310       
311        # Set properties from file
312        self.readProperties()
313
314        # Instantiate the authentication service to use with the session
315        # manager
316        authNModFilePath=self.__prop[self.AUTHN_KEYNAME].get('moduleFilePath')
317        self.__authNService = instantiateClass(
318                            self.__prop[self.AUTHN_KEYNAME].get('moduleName'),
319                            self.__prop[self.AUTHN_KEYNAME].get('className'),
320                            moduleFilePath=authNModFilePath,
321                            objectType=AbstractAuthNService, 
322                            classProperties=self.__prop[self.AUTHN_KEYNAME])
323       
324        # Call here as we can safely expect that all Credential Repository
325        # parameters have been set above
326        credentialRepositoryModule = \
327            self.__prop[self.CREDREPOS_KEYNAME].get('modName')
328        credentialRepositoryClassName = \
329            self.__prop[self.CREDREPOS_KEYNAME].get('className')
330           
331        if credentialRepositoryModule is None or \
332           credentialRepositoryClassName is None:
333            self._credentialRepository = NullCredentialRepository()
334        else:
335            credentialRepositoryModuleFilePath = \
336                self.__prop[self.CREDREPOS_KEYNAME].get('modFilePath')
337               
338            self._credentialRepository = instantiateClass(
339                        credentialRepositoryModule,
340                        credentialRepositoryClassName,
341                        moduleFilePath=credentialRepositoryModuleFilePath,
342                        objectType=CredentialRepository,
343                        classProperties=self.__prop[self.CREDREPOS_KEYNAME])
344       
345        # Set any properties that were provided by keyword input
346        # NB If any are duplicated with tags in the properties file they
347        # will overwrite the latter
348        self.setProperties(**prop)
349       
350#    def __repr__(self):
351#        """Return file properties dictionary as representation"""
352#        return repr(self.__prop)
353
354    def __delitem__(self, key):
355        "Session Manager keys cannot be removed"       
356        raise KeyError('Keys cannot be deleted from '+self.__class__.__name__)
357   
358    def __getitem__(self, key):
359        self.__class__.__name__ + """ behaves as data dictionary of Session
360        Manager properties
361        """
362        if key not in self.__prop:
363            raise KeyError("Invalid key '%s'" % key)
364       
365        return self.__prop[key]
366   
367   
368    def __setitem__(self, key, item):
369        self.__class__.__name__ + """ behaves as data dictionary of Session
370        Manager properties"""
371        self.setProperties(**{key: item})
372           
373    def get(self, kw):
374        return self.__prop.get(kw)
375
376    def clear(self):
377        raise KeyError("Data cannot be cleared from "+self.__class__.__name__)
378   
379    def keys(self):
380        return self.__prop.keys()
381
382    def items(self):
383        return self.__prop.items()
384
385    def values(self):
386        return self.__prop.values()
387
388    def has_key(self, key):
389        return self.__prop.has_key(key)
390
391    # 'in' operator
392    def __contains__(self, key):
393        return key in self.__prop
394   
395    def setPropFilePath(self, val=None):
396        """Set properties file from input or based on environment variable
397        settings"""
398        log.debug("Setting property file path")
399        if not val:
400            if 'NDGSEC_SM_PROPFILEPATH' in os.environ:
401                val = os.environ['NDGSEC_SM_PROPFILEPATH']
402               
403                log.debug('Set properties file path "%s" from '
404                          '"NDGSEC_SM_PROPFILEPATH"' % val)
405
406            elif 'NDGSEC_DIR' in os.environ:
407                val = os.path.join(os.environ['NDGSEC_DIR'], 
408                                   self.__class__._confDir,
409                                   self.__class__._propFileName)
410
411                log.debug('Set properties file path %s from "NDGSEC_DIR"'%val)
412            else:
413                raise AttributeError('Unable to set default Session '
414                                     'Manager properties file path: neither ' 
415                                     '"NDGSEC_SM_PROPFILEPATH" or "NDGSEC_DIR"'
416                                     ' environment variables are set')
417        else:
418             log.debug('Set properties file path %s from user input' % val)       
419
420        if not isinstance(val, basestring):
421            raise AttributeError("Input Properties file path must be a valid "
422                                 "string.")
423     
424        self._propFilePath = os.path.expandvars(val)
425        log.debug("Path set to: %s" % val)
426       
427    def getPropFilePath(self):
428        log.debug("Getting property file path")
429        if hasattr(self, '_propFilePath'):
430            return self._propFilePath
431        else:
432            return ""
433       
434    # Also set up as a property
435    propFilePath = property(fset=setPropFilePath,
436                            fget=getPropFilePath,
437                            doc="Set the path to the properties file")   
438       
439    def getPropFileSection(self):
440        '''Get the section name to extract properties from an ini file -
441        DOES NOT apply to XML file properties
442       
443        @rtype: basestring
444        @return: section name'''
445        log.debug("Getting property file section name")
446        if hasattr(self, '_propFileSection'):
447            return self._propFileSection
448        else:
449            return ""   
450   
451    def setPropFileSection(self, val=None):
452        """Set section name to read properties from ini file.  This is set from
453        input or based on environment variable setting
454        NDGSEC_SM_PROPFILESECTION
455       
456        @type val: basestring
457        @param val: section name"""
458        log.debug("Setting property file section name")
459        if not val:
460            val = os.environ.get('NDGSEC_SM_PROPFILESECTION', 'DEFAULT')
461               
462        if not isinstance(val, basestring):
463            raise AttributeError("Input Properties file section name "
464                                 "must be a valid string.")
465     
466        self._propFileSection = val
467        log.debug("Properties file section set to: %s" % val)
468       
469    # Also set up as a property
470    propFileSection = property(fset=setPropFileSection,
471                    fget=getPropFileSection,
472                    doc="Set the file section name for ini file properties")   
473   
474    def setPropPrefix(self, val=None):
475        """Set prefix for properties read from ini file.  This is set from
476        input or based on environment variable setting
477        NDGSEC_AA_PROPFILEPREFIX
478       
479        DOES NOT apply to XML file properties
480       
481        @type val: basestring
482        @param val: section name"""
483        log.debug("Setting property file section name")
484        if val is None:
485            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
486               
487        if not isinstance(val, basestring):
488            raise AttributeError("Input Properties file section name "
489                                 "must be a valid string.")
490     
491        self._propPrefix = val
492        log.debug("Properties file section set to: %s" % val)
493           
494    def setPropPrefix(self, val=None):
495        """Set prefix for properties read from ini file.  This is set from
496        input or based on environment variable setting
497        NDGSEC_SM_PROPFILEPREFIX
498       
499        DOES NOT apply to XML file properties
500       
501        @type val: basestring
502        @param val: section name"""
503        log.debug("Setting property file section name")
504        if val is None:
505            val = os.environ.get('NDGSEC_SM_PROPFILEPREFIX', 'DEFAULT')
506               
507        if not isinstance(val, basestring):
508            raise AttributeError("Input Properties file section name "
509                                 "must be a valid string.")
510     
511        self._propPrefix = val
512        log.debug("Properties file section set to: %s" % val)
513       
514    def getPropPrefix(self):
515        '''Get the prefix name used for properties in an ini file -
516        DOES NOT apply to XML file properties
517       
518        @rtype: basestring
519        @return: section name'''
520        log.debug("Getting property file prefix")
521        if hasattr(self, '_propPrefix'):
522            return self._propPrefix
523        else:
524            return ""   
525       
526    # Also set up as a property
527    propPrefix = property(fset=setPropPrefix,
528                          fget=getPropPrefix,
529                          doc="Set a prefix for ini file properties")   
530
531    def readProperties(self, section=None, prefix=None):
532        '''Read the properties files and do some checking/converting of input
533        values
534         
535        @type section: basestring
536        @param section: ini file section to read properties from - doesn't
537        apply to XML format properties files.  section setting defaults to
538        current propFileSection attribute
539       
540        @type prefix: basestring
541        @param prefix: apply prefix to ini file properties - doesn't
542        apply to XML format properties files.  This enables filtering of
543        properties so that only those relevant to this class are read in
544        '''
545        if section is None:
546            section = self.propFileSection
547       
548        if prefix is None:
549            prefix = self.propPrefix
550           
551        # Configuration file properties are held together in a dictionary
552        readPropertiesFile = INIPropertyFileWithValidation()
553        fileProp = readPropertiesFile(self.propFilePath,
554                                      validKeys=SessionMgr.propertyDefaults,
555                                      prefix=prefix,
556                                      sections=(section,))
557       
558        # Keep a copy of the config file for the CredWallet to reference so
559        # that it can retrieve WS-Security settings
560        self._cfg = readPropertiesFile.cfg
561       
562        # Allow for section and prefix names which will nest the Attribute
563        # Authority properties in a hierarchy
564        propBranch = fileProp
565        if section != 'DEFAULT':
566            propBranch = propBranch[section]
567           
568        if prefix:
569            propBranch = propBranch[prefix]
570           
571        self.__prop = propBranch
572
573        log.info('Loaded properties from "%s"' % self.propFilePath)
574       
575    def setProperties(self, **prop):
576        """Update existing properties from an input dictionary
577        Check input keys are valid names"""
578       
579        log.debug("Calling SessionMgr.setProperties with kw = %s" % prop)
580       
581        for key in prop.keys():
582            if key not in self.propertyDefaults:
583                raise SessionMgrError("Property name \"%s\" is invalid" % key)
584        for key, value in prop.items():
585                       
586            if key == 'authNProp':
587                self.__authNService.setProperties(prop[key])
588   
589            elif key == self.CREDREPOS_KEYNAME:
590                self.__prop[self.CREDREPOS_KEYNAME] = prop[key].copy()
591
592            elif key in self.propertyDefaults:
593                # Only update other keys if they are not None or ""
594                if value:
595                    if isinstance(value, basestring):
596                        self.__prop[key] = os.path.expandvars(value).strip()
597                    else:
598                        self.__prop[key] = value             
599            else:
600                raise SessionMgrError('Key "%s" is not a valid Session '
601                                      'Manager property' % key)
602       
603    def getSessionStatus(self, sessID=None, userDN=None):
604        """Check the status of a given session identified by sessID or
605        user Distinguished Name
606       
607        @type sessID: string
608        @param sessID: session identifier as returned from a call to connect()
609        @type userDN: string
610        @param userDN: user Distinguished Name of session to check
611        @rtype: bool
612        @return: True if session is active, False if no session found"""
613
614        log.debug("Calling SessionMgr.getSessionStatus ...")
615       
616        # Look for a session corresponding to this ID
617        if sessID and userDN:
618            raise SessionMgrError('Only "SessID" or "userDN" keywords may be '
619                                  'set')
620        elif sessID:
621            if sessID in self.__sessDict:               
622                log.info("Session found with ID = %s" % sessID)
623                return True
624            else:
625                # User session not found with given ID
626                log.info("No user session found matching input ID = %s" % \
627                         sessID)
628                return False
629                         
630        elif userDN:
631            try:
632                # Enables re-ordering of DN fields for following dict search
633                userDN = str(X500DN(userDN))
634               
635            except Exception, e:
636                log.error("Parsing input user certificate DN for "
637                          "getSessionStatus: %s" % e)
638                raise
639           
640            if userDN in self.__dnDict:
641                log.info("Session found with DN = %s" % userDN)
642                return True                       
643            else:
644                # User session not found with given proxy cert
645                log.info("No user session found matching input userDN = %s" %\
646                         userDN)
647                return False
648
649    def connect(self, 
650                createServerSess=True,
651                username=None,
652                passphrase=None, 
653                userCert=None, 
654                sessID=None):       
655        """Create a new user session or connect to an existing one:
656
657        connect([createServerSess=True/False, ]|[, username=u, passphrase=p]|
658                [, userCert=px]|[, sessID=id])
659
660        @type createUserSess: bool
661        @param createServerSess: If set to True, the SessionMgr will create
662        and manage a session for the user.  For command line case, it's
663        possible to choose to have a client or server side session using this
664        keyword.
665       
666        @type username: string
667        @param username: username of account to connect to
668
669        @type passphrase: string
670        @param passphrase: pass-phrase - user with username arg
671       
672        @type userCert: string
673        @param userCert: connect to existing session with proxy certificate
674        corresponding to user.  username/pass-phrase not required
675       
676        @type sessID: string
677        @param sessID: connect to existing session corresponding to this ID.
678        username/pass-phrase not required.
679       
680        @rtype: tuple
681        @return user certificate, private key, issuing certificate and
682        session ID respectively.  Session ID will be none if createUserSess
683        keyword is set to False
684        """
685       
686        log.debug("Calling SessionMgr.connect ...")
687       
688        # Initialise proxy cert to be returned
689        userCert = None
690       
691        if sessID is not None:           
692            # Connect to an existing session identified by a session ID and
693            # return equivalent proxy cert
694            userSess = self._connect2UserSession(sessID=sessID)
695            userCert = userSess.credWallet.userCert
696           
697        elif userCert is not None:
698            # Connect to an existing session identified by a proxy
699            # certificate
700            userSess = self._connect2UserSession(userCert=userCert)
701            sessID = userSess.latestSessID
702           
703        else:
704            # Create a fresh session
705            try:           
706                # Get a proxy certificate to represent users ID for the new
707                # session
708                userCreds = self.__authNService.logon(username, passphrase)
709            except AuthNServiceError:
710                # Filter out known AuthNService execptions
711               
712                raise
713            except Exception, e:
714                # Catch all here for AuthNService but the particular
715                # implementation should make full use of AuthN* exception
716                # types
717                raise AuthNServiceError("Authentication Service: %s" % e)
718                           
719            # unpack output
720            if userCreds is None:
721                nUserCreds = 0
722            else:
723                nUserCreds = len(userCreds)
724               
725            if nUserCreds > 1:
726                userCert = userCreds[0]
727                userPriKey = userCreds[1]
728            else:
729                userCert = userPriKey = None
730               
731            # Issuing cert is needed only if userCert is a proxy
732            issuingCert = nUserCreds > 2 and userCreds[2] or None       
733
734            if createServerSess:
735                # Session Manager creates and manages user's session
736                userSess = self._createUserSession(username, userCreds)
737                sessID = userSess.latestSessID
738            else:
739                sessID = None
740                               
741        # Return proxy details and cookie
742        return userCert, userPriKey, issuingCert, sessID       
743       
744       
745    def _createUserSession(self, username, userCreds=None):
746        """Create a new user session from input user credentials       
747        and return
748       
749        @type username: basestring
750        @param username: username user logged in with
751        @type userCreds: tuple
752        @param userCreds: tuple containing user certificate, private key
753        and optionally an issuing certificate.  An issuing certificate is
754        present if user certificate is a proxy and therefore it's issuer is
755        other than the CA. userCreds may default to None if no user certificate
756        is available.  In this case, the Session Manager server certificate
757        is used to secure connections to Attribute Authorities and other
758        services where required"""
759       
760        log.debug("Calling SessionMgr._createUserSession ...")
761       
762        # Check for an existing session for the same user
763        if username in self.__usernameDict:
764            # Update existing session with user cert and add a new
765            # session ID to access it - a single session can be accessed
766            # via multiple session IDs e.g. a user may wish to access the
767            # same session from the their desktop PC and their laptop.
768            # Different session IDs are allocated in each case.
769            userSess = self.__usernameDict[username]
770            userSess.addNewSessID()           
771        else:
772            # Create a new user session using the username, session ID and
773            # X.509 credentials
774
775            # Copy global Credential Wallet settings into specific set for this
776            # user
777            credWalletKw = self.__prop['credentialWallet'].copy()
778            credWalletKw['userId'] = username
779           
780            # Update user PKI settings if any are present
781            if userCreds is None:
782                nUserCreds = 0
783            else:
784                nUserCreds = len(userCreds)
785               
786            if nUserCreds > 1:
787                credWalletKw['userX509Cert'] = userCreds[0]
788                credWalletKw['userPriKey'] = userCreds[1]
789           
790            if nUserCreds == 3:
791                credWalletKw['issuingX509Cert'] = userCreds[2]
792               
793            try:   
794                userSess = UserSession(**credWalletKw)     
795            except Exception, e:
796                log.error("Error occurred whilst creating User Session: %s"%e)
797                raise 
798           
799            # Also allow access by user DN if individual user PKI credentials
800            # have been passed in
801            if userCreds is not None:
802                try:
803                    userDN = str(X509CertParse(userCreds[0]).dn)
804                   
805                except Exception, e:
806                    log.error("Parsing input certificate DN for session create: "
807                              "%s" % e)
808                    raise
809
810                self.__dnDict[userDN] = userSess
811       
812        newSessID = userSess.latestSessID
813       
814        # Check for unique session ID
815        if newSessID in self.__sessDict:
816            raise SessionMgrError("New Session ID is already in use:\n\n %s" % 
817                                  newSessID)
818
819        # Add new session to list                 
820        self.__sessDict[newSessID] = userSess
821                       
822        # Return new session
823        return userSess
824
825
826    def _connect2UserSession(self, username=None, userX509Cert=None, sessID=None):
827        """Connect to an existing session by providing a valid session ID or
828        proxy certificate
829
830        _connect2UserSession([username|userX509Cert]|[sessID])
831
832        @type username: string
833        @param username: username
834       
835        @type userX509Cert: string
836        @param userX509Cert: user's X.509 certificate corresponding to an
837        existing session to connect to.
838       
839        @type sessID: string
840        @param sessID: similiarly, a web browser session ID linking to an
841        an existing session."""
842       
843        log.debug("Calling SessionMgr._connect2UserSession ...")
844           
845        # Look for a session corresponding to this ID
846        if username:
847            userSess = self.__usernameDict.get(username)
848            if userSess is None:
849                log.error("Session not found for username=%s" % username)
850                raise SessionNotFound("No user session found matching the "
851                                      "input username")
852
853            log.info("Connecting to session userDN=%s; sessID=%s using "
854                     "username=%s" % (userSess.credWallet.userX509Cert.dn, 
855                                      userSess.sessIdList,
856                                      username))           
857        elif sessID:
858            userSess = self.__sessDict.get(sessID)
859            if userSess is None: 
860                log.error("Session not found for sessID=%s" % sessID)
861                raise SessionNotFound("No user session found matching input "
862                                      "session ID")
863
864            log.info("Connecting to session userDN=%s; username=%s using "
865                     "sessID=%s" % (userSess.credWallet.userX509Cert.dn, 
866                                    username,
867                                    userSess.sessIdList))
868
869        elif userX509Cert is not None:
870            if isinstance(userX509Cert, basestring):
871                try:
872                    userDN = str(X509CertParse(userX509Cert).dn)
873                   
874                except Exception, e:
875                    log.error("Parsing input user certificate DN for session "
876                              "connect: %s" %e)
877                    raise
878            else:
879                try:
880                    userDN = str(userX509Cert.dn)
881                   
882                except Exception, e:
883                    log.error("Parsing input user certificate DN for session "
884                              "connect: %s" % e)
885                    raise
886               
887            userSess = self.__dnDict.get(userDN)
888            if userSess is None:
889                log.error("Session not found for userDN=%s" % userDN)
890                raise SessionNotFound("No user session found matching input "
891                                      "user X.509 certificate")
892           
893            log.info("Connecting to session sessID=%s; username=%s using "
894                     "userDN=%s" % (userSess.sessIdList, 
895                                    userSess.credWallet.username, 
896                                    userDN))
897        else:
898            raise SessionMgrError('"username", "sessID" or "userX509Cert" keywords '
899                                  'must be set')
900           
901        # Check that the Credentials held in the wallet are still valid           
902        try:
903            userSess.credWallet.isValid(raiseExcep=True)
904            return userSess
905       
906        except X509CertInvalidNotBeforeTime, e:
907            # ! Delete user session since it's user certificate is invalid
908            self.deleteUserSession(userSess=userSess)
909            raise UserSessionX509CertNotBeforeTimeError(
910                                    "User session is invalid: %s" % e)       
911   
912        except X509CertExpired, e:
913            # ! Delete user session since it's user certificate is invalid
914            self.deleteUserSession(userSess=userSess)
915            raise UserSessionExpired, "User session is invalid: %s" % e         
916       
917        except Exception, e:
918            raise InvalidUserSession, "User session is invalid: %s" % e
919               
920
921    def deleteUserSession(self, sessID=None, userCert=None, userSess=None):
922        """Delete an existing session by providing a valid session ID or
923        proxy certificate - use for user logout
924
925        deleteUserSession([userCert]|[sessID]|[userSess])
926       
927        @type userCert: ndg.security.common.X509.X509Cert
928        @param userCert: proxy certificate corresponding to an existing
929        session to connect to.
930       
931        @type sessID: string
932        @param sessID: similiarly, a web browser session ID linking to an
933        an existing session.
934       
935        @type userSess: UserSession
936        @param userSess: user session object to be deleted
937        """
938       
939        log.debug("Calling SessionMgr.deleteUserSession ...")
940       
941        # Look for a session corresponding to the session ID/proxy cert.
942        if sessID:
943            try:
944                userSess = self.__sessDict[sessID]
945               
946            except KeyError:
947                raise SessionMgrError(
948                    "Deleting user session - no matching session ID exists")
949
950            # Get associated user Distinguished Name
951            userDN = str(userSess.credWallet.userCert.dn)
952           
953        elif userCert:
954            try:
955                userDN = str(userCert.dn)
956               
957            except Exception, e:
958                raise SessionMgrError(
959                "Parsing input proxy certificate DN for session connect: %s"%\
960                                                                        str(e))
961            try:
962                userSess = self.__dnDict[userDN]
963                       
964            except KeyError:
965                # User session not found with given proxy cert
966                raise SessionMgrError(
967                    "No user session found matching input proxy certificate")
968       
969        if userSess:
970            userDN = str(userSess.credWallet.userCert.dn)
971        else:
972            # User session not found with given ID
973            raise SessionMgrError('"sessID", "userCert" or "userSess" '
974                                  'keywords must be set')
975 
976        # Delete associated sessions
977        try:
978            # Each session may have a number of session IDs allocated to
979            # it. 
980            #
981            # Use pop rather than del so that key errors are ignored
982            for userSessID in userSess.sessIdList:
983                self.__sessDict.pop(userSessID, None)
984
985            self.__dnDict.pop(userDN, None)
986       
987        except Exception, e:
988            raise SessionMgrError("Deleting user session: %s" % e) 
989
990        log.info("Deleted user session: user DN = %s, sessID = %s" %
991                 (userDN, userSess.sessIdList))
992
993    def getAttCert(self,
994                   username=None,
995                   userCert=None,
996                   sessID=None,
997                   **credWalletKw):
998        """For a given user, request Attribute Certificate from an Attribute
999        Authority given by service URI.  If sucessful, an attribute
1000        certificate is added to the user session credential wallet and also
1001        returned from this method
1002       
1003        A user identifier must be provided in the form of a user ID, user X.509
1004        certificate or a user session ID
1005
1006        @type username: string
1007        @param username: username to key into their session
1008
1009        @type userCert: string
1010        @param userCert: user's X.509 certificate to key into their session
1011       
1012        @type sessID: string
1013        @param sessID: user's ID to key into their session
1014       
1015        @type credWalletKw: dict
1016        @param **credWalletKw: keywords to CredWallet.getAttCert
1017        """
1018       
1019        log.debug("Calling SessionMgr.getAttCert ...")
1020       
1021        # Retrieve session corresponding to user's session ID using relevant
1022        # input credential
1023        userSess = self._connect2UserSession(username=username, sessID=sessID, 
1024                                             userCert=userCert)
1025       
1026        # The user's Credential Wallet carries out attribute request to the
1027        # Attribute Authority
1028        try:
1029            attCert = userSess.credWallet.getAttCert(**credWalletKw)
1030            return attCert, None, []
1031           
1032        except CredWalletAttributeRequestDenied, e:
1033            # Exception object contains a list of attribute certificates
1034            # which could be used to re-try to get authorisation via a mapped
1035            # certificate
1036            return None, str(e), e.extAttCertList
1037       
1038    def auditCredentialRepository(self):
1039        """Remove expired Attribute Certificates from the Credential
1040        Repository"""
1041        log.debug("Calling SessionMgr.auditCredentialRepository ...")
1042        self._credentialRepository.auditCredentials()
1043       
1044
1045class AuthNServiceError(Exception):
1046    """Base class for AbstractAuthNService exceptions
1047   
1048    A standard message is raised set by the msg class variable but the actual
1049    exception details are logged to the error log.  The use of a standard
1050    message enbales callers to use its content for user error messages.
1051   
1052    @type msg: basestring
1053    @cvar msg: standard message to be raised for this exception"""
1054    msg = "An error occured with login"
1055    def __init__(self, *arg, **kw):
1056        Exception.__init__(self, AuthNServiceError.msg, *arg, **kw)
1057        if len(arg) > 0:
1058            msg = arg[0]
1059        else:
1060            msg = AuthNServiceError.msg
1061           
1062        log.error(msg)
1063       
1064class AuthNServiceInvalidCredentials(AuthNServiceError):
1065    """User has provided incorrect username/password.  Raise from logon"""
1066    msg = "Invalid username/password provided"
1067   
1068class AuthNServiceRetrieveError(AuthNServiceError):
1069    """Error with retrieval of information to authenticate user e.g. error with
1070    database look-up.  Raise from logon"""
1071    msg = \
1072    "An error occured retrieving information to check the login credentials"
1073
1074class AuthNServiceInitError(AuthNServiceError):
1075    """Error with initialisation of AuthNService.  Raise from __init__"""
1076    msg = "An error occured with the initialisation of the Session " + \
1077        "Manager's Authentication Service"
1078   
1079class AuthNServiceConfigError(AuthNServiceError):
1080    """Error with Authentication configuration.  Raise from __init__"""
1081    msg = "An error occured with the Session Manager's Authentication " + \
1082        "Service configuration"
1083
1084
1085class AbstractAuthNService(object):
1086    """
1087    An abstract base class to define the authentication service interface for
1088    use with a SessionMgr service
1089    """
1090
1091    def __init__(self, propertiesFile=None, **prop):
1092        """Make any initial settings
1093       
1094        Settings are held in a dictionary which can be set from **prop,
1095        a call to setProperties() or by passing settings in an XML file
1096        given by propFilePath
1097       
1098        @param propertiesFile:   set properties via a configuration file
1099        @param **prop:         set properties via keywords - see __validKeys
1100        class variable for a list of these
1101        @raise AuthNServiceInitError: error with initialisation
1102        @raise AuthNServiceConfigError: error with configuration
1103        @raise AuthNServiceError: generic exception not described by the other
1104        specific exception types.
1105        """
1106        pass
1107   
1108    def setProperties(self, **prop):
1109        """Update existing properties from an input dictionary
1110        Check input keys are valid names"""
1111        raise NotImplementedError(
1112                            self.setProperties.__doc__.replace('\n       ',''))
1113       
1114    def logon(self, username, passphrase, lifetime=None):
1115        """
1116        Retrieve a proxy credential from a proxy server
1117       
1118        @type username: basestring
1119        @param username: username of credential
1120       
1121        @type passphrase: basestring
1122        @param passphrase: pass-phrase for private key of credential held on
1123        server
1124       
1125        @type lifetime: int
1126        @param lifetime: lifetime for generated certificate
1127       
1128        @raise AuthNServiceInvalidCredentials: invalid username/passphrase
1129        @raise AuthNServiceError: error
1130        @raise AuthNServiceRetrieveError: error with retrieval of information
1131        to authenticate user e.g. error with database look-up.
1132        @raise AuthNServiceError: generic exception not described by the other
1133        specific exception types.
1134        @rtype: tuple
1135        @return: this may be either user PKI credentials or an empty tuple
1136        depending on the nature of the authentication service.  The UserSession
1137        object in the Session Manager instance can receive an individual user
1138        certificate and private key as returned by for example MyProxy.  In
1139        this case, the tuple consists of strings in PEM format:
1140         - the user certificate
1141         - corresponding private key
1142         - the issuing certificate. 
1143        The issuing certificate is optional.  It is only set if the user
1144        certificate is a proxy certificate
1145        """
1146        raise NotImplementedError(self.logon.__doc__.replace('\n       ',''))
1147           
Note: See TracBrowser for help on using the repository browser.