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

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

Fix problem with search and replace licence not adding a new line.

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