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

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

Added capability to allow for clock skew between client and server for WS-Security timestamp handling. This is included in the 4Suite and DOM based implementations.

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