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

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

Fixes for integration testing with OAI editor:

  • Attribute Authority and Session Manager clients now use Domlette element proxy classes for XML parsing and 4Suite based signature handler
  • ndg.security.server.wsgi.authz.PIPMiddleware: parse list items correctly
  • ndg.security.server.wsgi.authn.SessionHandlerMiddleware?: allow OpenID AX params for Session ID and Session Manager to default to None to allow for OpenID Providers which cannot support passing of these attributes.
  • 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        # Credential Repository interface only set if properties file is set
295        # otherwise explicit calls are necessary to set credentialRepositoryProp via
296        # setProperties/readProperties and then loadCredentialRepositoryInterface
297        self._credentialRepository = None
298   
299        # Set from input or use defaults based or environment variables
300        self.propFilePath = propFilePath
301       
302        self.propFileSection = propFileSection
303        self.propPrefix = propPrefix
304        self._cfg = None
305       
306        # Set properties from file
307        self.readProperties()
308
309       
310        # Set any properties that were provided by keyword input
311        # NB If any are duplicated with tags in the properties file they
312        # will overwrite the latter
313        self.setProperties(**prop)
314
315        # Instantiate the authentication service to use with the session
316        # manager
317        self.initAuthNService()
318       
319        # Call here as we can safely expect that all Credential Repository
320        # parameters have been set above
321        self.initCredentialRepository()   
322       
323       
324    def initAuthNService(self):
325        '''Load Authentication Service Interface from property settings'''
326        authNProp = self.__prop[SessionManager.AUTHN_KEYNAME]
327        authNModFilePath = authNProp.pop('moduleFilePath', None)
328       
329        self.__authNService = instantiateClass(authNProp.pop('moduleName'),
330                                               authNProp.pop('className'),
331                                               moduleFilePath=authNModFilePath,
332                                               objectType=AbstractAuthNService, 
333                                               classProperties=authNProp)           
334       
335    def initCredentialRepository(self):
336        '''Load Credential Repository instance from property settings
337        If non module or class name were set a null interface is loaded by
338        default'''
339       
340        credReposProp = self.__prop.get(SessionManager.CREDREPOS_KEYNAME, {})
341
342        credentialRepositoryModule = credReposProp.get('moduleName')
343        credentialRepositoryClassName = credReposProp.get('className')
344           
345        if credentialRepositoryModule is None or \
346           credentialRepositoryClassName is None:
347            # Default to NullCredentialRepository if no settings have been made
348            self._credentialRepository = NullCredentialRepository()
349        else:
350            credReposModuleFilePath = credReposProp.get('moduleFilePath')
351               
352            self._credentialRepository = instantiateClass(
353                                        credentialRepositoryModule,
354                                        credentialRepositoryClassName,
355                                        moduleFilePath=credReposModuleFilePath,
356                                        objectType=CredentialRepository,
357                                        classProperties=credReposProp)
358
359    def __delitem__(self, key):
360        "Session Manager keys cannot be removed"       
361        raise KeyError('Keys cannot be deleted from '+self.__class__.__name__)
362   
363    def __getitem__(self, key):
364        """Enables behaviour as data dictionary of Session Manager properties
365        """
366        if key not in self.__prop:
367            raise KeyError("Invalid key '%s'" % key)
368       
369        return self.__prop[key]
370   
371    def __setitem__(self, key, item):
372        self.__class__.__name__ + """ behaves as data dictionary of Session
373        Manager properties"""
374        self.setProperties(**{key: item})
375           
376    def get(self, kw):
377        return self.__prop.get(kw)
378
379    def clear(self):
380        raise KeyError("Data cannot be cleared from "+SessionManager.__name__)
381   
382    def keys(self):
383        return self.__prop.keys()
384
385    def items(self):
386        return self.__prop.items()
387
388    def values(self):
389        return self.__prop.values()
390
391    def has_key(self, key):
392        return self.__prop.has_key(key)
393
394    # 'in' operator
395    def __contains__(self, key):
396        return key in self.__prop
397   
398    def setPropFilePath(self, val=None):
399        """Set properties file from input or based on environment variable
400        settings"""
401        log.debug("Setting property file path")
402        if not val:
403            if 'NDGSEC_SM_PROPFILEPATH' in os.environ:
404                val = os.environ['NDGSEC_SM_PROPFILEPATH']
405               
406                log.debug('Set properties file path "%s" from '
407                          '"NDGSEC_SM_PROPFILEPATH"' % val)
408
409            elif 'NDGSEC_DIR' in os.environ:
410                val = os.path.join(os.environ['NDGSEC_DIR'], 
411                                   self.__class__._confDir,
412                                   self.__class__._propFileName)
413
414                log.debug('Set properties file path %s from "NDGSEC_DIR"'%val)
415            else:
416                raise AttributeError('Unable to set default Session '
417                                     'Manager properties file path: neither ' 
418                                     '"NDGSEC_SM_PROPFILEPATH" or "NDGSEC_DIR"'
419                                     ' environment variables are set')
420        else:
421             log.debug('Set properties file path %s from user input' % val)       
422
423        if not isinstance(val, basestring):
424            raise AttributeError("Input Properties file path must be a valid "
425                                 "string.")
426     
427        self._propFilePath = os.path.expandvars(val)
428        log.debug("Path set to: %s" % val)
429       
430    def getPropFilePath(self):
431        log.debug("Getting property file path")
432        if hasattr(self, '_propFilePath'):
433            return self._propFilePath
434        else:
435            return ""
436       
437    # Also set up as a property
438    propFilePath = property(fset=setPropFilePath,
439                            fget=getPropFilePath,
440                            doc="Set the path to the properties file")   
441       
442    def getPropFileSection(self):
443        '''Get the section name to extract properties from an ini file -
444        DOES NOT apply to XML file properties
445       
446        @rtype: basestring
447        @return: section name'''
448        log.debug("Getting property file section name")
449        if hasattr(self, '_propFileSection'):
450            return self._propFileSection
451        else:
452            return ""   
453   
454    def setPropFileSection(self, val=None):
455        """Set section name to read properties from ini file.  This is set from
456        input or based on environment variable setting
457        NDGSEC_SM_PROPFILESECTION
458       
459        @type val: basestring
460        @param val: section name"""
461        log.debug("Setting property file section name")
462        if not val:
463            val = os.environ.get('NDGSEC_SM_PROPFILESECTION', 'DEFAULT')
464               
465        if not isinstance(val, basestring):
466            raise AttributeError("Input Properties file section name "
467                                 "must be a valid string.")
468     
469        self._propFileSection = val
470        log.debug("Properties file section set to: %s" % val)
471       
472    # Also set up as a property
473    propFileSection = property(fset=setPropFileSection,
474                    fget=getPropFileSection,
475                    doc="Set the file section name for ini file properties")   
476   
477    def setPropPrefix(self, val=None):
478        """Set prefix for properties read from ini file.  This is set from
479        input or based on environment variable setting
480        NDGSEC_AA_PROPFILEPREFIX
481       
482        DOES NOT apply to XML file properties
483       
484        @type val: basestring
485        @param val: section name"""
486        log.debug("Setting property file section name")
487        if val is None:
488            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
489               
490        if not isinstance(val, basestring):
491            raise AttributeError("Input Properties file section name "
492                                 "must be a valid string.")
493     
494        self._propPrefix = val
495        log.debug("Properties file section set to: %s" % val)
496           
497    def setPropPrefix(self, val=None):
498        """Set prefix for properties read from ini file.  This is set from
499        input or based on environment variable setting
500        NDGSEC_SM_PROPFILEPREFIX
501       
502        DOES NOT apply to XML file properties
503       
504        @type val: basestring
505        @param val: section name"""
506        log.debug("Setting property file section name")
507        if val is None:
508            val = os.environ.get('NDGSEC_SM_PROPFILEPREFIX', 'DEFAULT')
509               
510        if not isinstance(val, basestring):
511            raise AttributeError("Input Properties file section name "
512                                 "must be a valid string.")
513     
514        self._propPrefix = val
515        log.debug("Properties file section set to: %s" % val)
516       
517    def getPropPrefix(self):
518        '''Get the prefix name used for properties in an ini file -
519        DOES NOT apply to XML file properties
520       
521        @rtype: basestring
522        @return: section name'''
523        log.debug("Getting property file prefix")
524        if hasattr(self, '_propPrefix'):
525            return self._propPrefix
526        else:
527            return ""   
528       
529    # Also set up as a property
530    propPrefix = property(fset=setPropPrefix,
531                          fget=getPropPrefix,
532                          doc="Set a prefix for ini file properties")   
533
534    def readProperties(self, section=None, prefix=None):
535        '''Read the properties files and do some checking/converting of input
536        values
537         
538        @type section: basestring
539        @param section: ini file section to read properties from - doesn't
540        apply to XML format properties files.  section setting defaults to
541        current propFileSection attribute
542       
543        @type prefix: basestring
544        @param prefix: apply prefix to ini file properties - doesn't
545        apply to XML format properties files.  This enables filtering of
546        properties so that only those relevant to this class are read in
547        '''
548        if section is None:
549            section = self.propFileSection
550       
551        if prefix is None:
552            prefix = self.propPrefix
553           
554        # Configuration file properties are held together in a dictionary
555        readPropertiesFile = INIPropertyFileWithValidation()
556        fileProp=readPropertiesFile(self.propFilePath,
557                                    validKeys=SessionManager.propertyDefaults,
558                                    prefix=prefix,
559                                    sections=(section,))
560       
561        # Keep a copy of the config file for the CredentialWallet to reference
562        # so that it can retrieve WS-Security settings
563        self._cfg = readPropertiesFile.cfg
564       
565        # Allow for section and prefix names which will nest the Attribute
566        # Authority properties in a hierarchy
567        propBranch = fileProp
568        if section != 'DEFAULT':
569            propBranch = propBranch[section]
570           
571        self.__prop = propBranch
572
573        log.info('Loaded properties from "%s"' % self.propFilePath)
574
575    @staticmethod
576    def _setProperty(value):
577        if value and isinstance(value, basestring):
578            return os.path.expandvars(value).strip()
579        else:
580            return value             
581       
582    def setProperties(self, **prop):
583        """Update existing properties from an input dictionary
584        Check input keys are valid names"""
585       
586        log.debug("Calling SessionManager.setProperties with kw = %s" % prop)
587       
588        for key in prop.keys():
589            if key not in self.propertyDefaults:
590                raise SessionManagerError('Property name "%s" is invalid'%key)
591           
592        for key, value in prop.iteritems():
593                       
594            if key == SessionManager.AUTHN_KEYNAME:
595                for subKey, subVal in prop[key].iteritems():
596#                    if subKey not in \
597#                       SessionManager.authNServicePropertyDefaults:
598#                        raise SessionManagerError('Key "%s" is not a valid '
599#                                            'Session Manager AuthNService '
600#                                            'property' % subKey)
601#                       
602                    if subVal:
603                        self.__prop[key][subKey] = SessionManager._setProperty(
604                                                                        subVal)
605   
606            elif key == SessionManager.CREDREPOS_KEYNAME:
607                for subKey, subVal in self.__prop[key].iteritems():
608#                    if subKey not in \
609#                       SessionManager.credentialRepositoryPropertyDefaults:
610#                        raise SessionManagerError('Key "%s" is not a valid '
611#                                        'Session Manager credentialRepository '
612#                                        'property' % subKey)
613#                       
614                    if subVal:
615                        self.__prop[key][subKey] = SessionManager._setProperty(
616                                                                        subVal)
617
618            elif key in SessionManager.propertyDefaults:
619                # Only update other keys if they are not None or ""
620                if value:
621                    self.__prop[key] = SessionManager._setProperty(value)             
622            else:
623                raise SessionManagerError('Key "%s" is not a valid Session '
624                                          'Manager property' % key)
625       
626    def getSessionStatus(self, sessID=None, userDN=None):
627        """Check the status of a given session identified by sessID or
628        user Distinguished Name
629       
630        @type sessID: string
631        @param sessID: session identifier as returned from a call to connect()
632        @type userDN: string
633        @param userDN: user Distinguished Name of session to check
634        @rtype: bool
635        @return: True if session is active, False if no session found"""
636
637        log.debug("Calling SessionManager.getSessionStatus ...")
638       
639        # Look for a session corresponding to this ID
640        if sessID and userDN:
641            raise SessionManagerError('Only "SessID" or "userDN" keywords may '
642                                      'be set')
643        elif sessID:
644            if sessID in self.__sessDict:               
645                log.info("Session found with ID = %s" % sessID)
646                return True
647            else:
648                # User session not found with given ID
649                log.info("No user session found matching input ID = %s"%sessID)
650                return False
651                         
652        elif userDN:
653            try:
654                # Enables re-ordering of DN fields for following dict search
655                userDN = str(X500DN(userDN))
656               
657            except Exception, e:
658                log.error("Parsing input user certificate DN for "
659                          "getSessionStatus: %s" % e)
660                raise
661           
662            if userDN in self.__dnDict:
663                log.info("Session found with DN = %s" % userDN)
664                return True                       
665            else:
666                # User session not found with given proxy cert
667                log.info("No user session found matching input userDN = %s" %
668                         userDN)
669                return False
670
671    def connect(self, 
672                createServerSess=True,
673                username=None,
674                passphrase=None, 
675                userX509Cert=None, 
676                sessID=None):       
677        """Create a new user session or connect to an existing one:
678
679        connect([createServerSess=True/False, ]|[, username=u, passphrase=p]|
680                [, userX509Cert=px]|[, sessID=id])
681
682        @type createUserSess: bool
683        @param createServerSess: If set to True, the SessionManager will create
684        and manage a session for the user.  For command line case, it's
685        possible to choose to have a client or server side session using this
686        keyword.
687       
688        @type username: string
689        @param username: username of account to connect to
690
691        @type passphrase: string
692        @param passphrase: pass-phrase - user with username arg
693       
694        @type userX509Cert: string
695        @param userX509Cert: connect to existing session with proxy certificate
696        corresponding to user.  username/pass-phrase not required
697       
698        @type sessID: string
699        @param sessID: connect to existing session corresponding to this ID.
700        username/pass-phrase not required.
701       
702        @rtype: tuple
703        @return user certificate, private key, issuing certificate and
704        session ID respectively.  Session ID will be none if createUserSess
705        keyword is set to False
706       
707        @raise AuthNServiceError: error with response from Authentication
708        service.  An instance of this class or derived class instance may be
709        raised.
710        """
711       
712        log.debug("Calling SessionManager.connect ...")
713       
714        # Initialise proxy cert to be returned
715        userX509Cert = None
716       
717        if sessID is not None:           
718            # Connect to an existing session identified by a session ID and
719            # return equivalent proxy cert
720            userSess = self._connect2UserSession(sessID=sessID)
721            userX509Cert = userSess.credentialWallet.userX509Cert
722           
723        elif userX509Cert is not None:
724            # Connect to an existing session identified by a proxy
725            # certificate
726            userSess = self._connect2UserSession(userX509Cert=userX509Cert)
727            sessID = userSess.latestSessID
728           
729        else:
730            # Create a fresh session
731            try:
732                # Get a proxy certificate to represent users ID for the new
733                # session
734                userCreds = self.__authNService.logon(username, passphrase)
735            except AuthNServiceError:
736                # Filter out known AuthNService exceptions
737                raise
738            except Exception, e:
739                # Catch all here for AuthNService but the particular
740                # implementation should make full use of AuthN* exception
741                # types
742                raise AuthNServiceError("Authentication Service: %s" % e)
743                           
744            # Unpack output
745            if userCreds is None:
746                nUserCreds = 0
747            else:
748                nUserCreds = len(userCreds)
749               
750            if nUserCreds > 1:
751                userX509Cert = userCreds[0]
752                userPriKey = userCreds[1]
753            else:
754                userX509Cert = userPriKey = None
755               
756            # Issuing cert is needed only if userX509Cert is a proxy
757            issuingCert = nUserCreds > 2 and userCreds[2] or None       
758
759            if createServerSess:
760                # Session Manager creates and manages user's session
761                userSess = self.createUserSession(username, 
762                                                  passphrase,
763                                                userCreds)
764                sessID = userSess.latestSessID
765            else:
766                sessID = None
767                               
768        # Return proxy details and cookie
769        return userX509Cert, userPriKey, issuingCert, sessID       
770       
771       
772    def createUserSession(self, username, userPriKeyPwd=None, userCreds=None):
773        """Create a new user session from input user credentials       
774        and return it.  This is an alternative to connect() useful in cases
775        where a session needs to be created for an existing authenticated user
776       
777        @type username: basestring
778        @param username: username user logged in with
779        @type userPriKeyPwd: basestring
780        @param userPriKeyPwd: password protecting the private key if set.
781        @type userCreds: tuple
782        @param userCreds: tuple containing user certificate, private key
783        and optionally an issuing certificate.  An issuing certificate is
784        present if user certificate is a proxy and therefore it's issuer is
785        other than the CA. userCreds may default to None if no user certificate
786        is available.  In this case, the Session Manager server certificate
787        is used to secure connections to Attribute Authorities and other
788        services where required
789       
790        @raise SessionManagerError: session ID added already exists in session
791        list
792        @raise ndg.security.common.X509.X509CertError: from parsing X.509 cert
793        set in the userCreds keyword"""
794       
795        log.debug("Calling SessionManager.createUserSession ...")
796       
797        # Check for an existing session for the same user
798        if username in self.__usernameDict:
799            # Update existing session with user cert and add a new
800            # session ID to access it - a single session can be accessed
801            # via multiple session IDs e.g. a user may wish to access the
802            # same session from the their desktop PC and their laptop.
803            # Different session IDs are allocated in each case.
804            userSess = self.__usernameDict[username]
805            userSess.addNewSessID()           
806        else:
807            # Create a new user session using the username, session ID and
808            # X.509 credentials
809
810            # Copy global Credential Wallet settings into specific set for this
811            # user
812            if self.propPrefix:
813                credentialWalletPropPfx = '.'.join((self.propPrefix, 
814                                            SessionManager.CREDWALLET_KEYNAME))
815            else:
816                credentialWalletPropPfx = SessionManager.CREDWALLET_KEYNAME
817               
818            # Include cfg setting to enable WS-Security Signature handler to
819            # pick up settings
820            credentialWalletProp = {
821                'cfg': self._cfg,
822                'userId': username,
823                'userPriKeyPwd': userPriKeyPwd,
824                'credentialRepository': self._credentialRepository,
825                'cfgPrefix': credentialWalletPropPfx
826            }
827                                                   
828   
829            # Update user PKI settings if any are present
830            if userCreds is None:
831                nUserCreds = 0
832            else:
833                nUserCreds = len(userCreds)
834               
835            if nUserCreds > 1:
836                credentialWalletProp['userX509Cert'] = userCreds[0]
837                credentialWalletProp['userPriKey'] = userCreds[1]
838           
839            if nUserCreds == 3:
840                credentialWalletProp['issuingX509Cert'] = userCreds[2]
841               
842            try:   
843                userSess = UserSession(**credentialWalletProp)     
844            except Exception, e:
845                log.error("Creating User Session: %s" % e)
846                raise 
847           
848            # Also allow access by user DN if individual user PKI credentials
849            # have been passed in
850            if userCreds is not None:
851                try:
852                    userDN = str(X509CertParse(userCreds[0]).dn)
853                   
854                except Exception, e:
855                    log.error("Parsing input certificate DN for session "
856                              "create: %s" % e)
857                    raise
858
859                self.__dnDict[userDN] = userSess
860       
861        newSessID = userSess.latestSessID
862       
863        # Check for unique session ID
864        if newSessID in self.__sessDict:
865            raise SessionManagerError("New Session ID is already in use:\n\n "
866                                      "%s" % newSessID)
867
868        # Add new session to list                 
869        self.__sessDict[newSessID] = userSess
870                       
871        # Return new session
872        return userSess
873
874
875    def _connect2UserSession(self,username=None,userX509Cert=None,sessID=None):
876        """Connect to an existing session by providing a valid session ID or
877        proxy certificate
878
879        _connect2UserSession([username|userX509Cert]|[sessID])
880
881        @type username: string
882        @param username: username
883       
884        @type userX509Cert: string
885        @param userX509Cert: user's X.509 certificate corresponding to an
886        existing session to connect to.
887       
888        @type sessID: string
889        @param sessID: similiarly, a web browser session ID linking to an
890        an existing session.
891       
892        @raise SessionNotFound: no matching session to the inputs
893        @raise UserSessionExpired: existing session has expired
894        @raise InvalidUserSession: user credential wallet is invalid
895        @raise UserSessionNotBeforeTimeError: """
896       
897        log.debug("Calling SessionManager._connect2UserSession ...")
898           
899        # Look for a session corresponding to this ID
900        if username:
901            userSess = self.__usernameDict.get(username)
902            if userSess is None:
903                log.error("Session not found for username=%s" % username)
904                raise SessionNotFound("No user session found matching the "
905                                      "input username")
906
907            if userSess.credentialWallet.userX509Cert:
908                userDN = userSess.credentialWallet.userX509Cert.dn
909            else:
910                userDN = None
911               
912            log.info("Connecting to session userDN=%s; sessID=%s using "
913                     "username=%s" % (userDN, userSess.sessIdList, username))           
914        elif sessID:
915            userSess = self.__sessDict.get(sessID)
916            if userSess is None: 
917                log.error("Session not found for sessID=%s" % sessID)
918                raise SessionNotFound("No user session found matching input "
919                                      "session ID")
920               
921            if userSess.credentialWallet.userX509Cert:
922                userDN = userSess.credentialWallet.userX509Cert.dn
923            else:
924                userDN = None
925               
926            log.info("Connecting to session userDN=%s; username=%s using "
927                     "sessID=%s" % (userDN, username, userSess.sessIdList))
928
929        elif userX509Cert is not None:
930            if isinstance(userX509Cert, basestring):
931                try:
932                    userDN = str(X509CertParse(userX509Cert).dn)
933                   
934                except Exception, e:
935                    log.error("Parsing input user certificate DN for session "
936                              "connect: %s" %e)
937                    raise
938            else:
939                try:
940                    userDN = str(userX509Cert.dn)
941                   
942                except Exception, e:
943                    log.error("Parsing input user certificate DN for session "
944                              "connect: %s" % e)
945                    raise
946               
947            userSess = self.__dnDict.get(userDN)
948            if userSess is None:
949                log.error("Session not found for userDN=%s" % userDN)
950                raise SessionNotFound("No user session found matching input "
951                                      "user X.509 certificate")
952           
953            log.info("Connecting to session sessID=%s; username=%s using "
954                     "userDN=%s" % (userSess.sessIdList, 
955                                    userSess.credentialWallet.userId, 
956                                    userDN))
957        else:
958            raise KeyError('"username", "sessID" or "userX509Cert" keywords '
959                           'must be set')
960           
961        # Check that the Credentials held in the wallet are still valid           
962        try:
963            userSess.isValid(raiseExcep=True)
964            return userSess
965       
966        except (UserSessionNotBeforeTimeError,X509CertInvalidNotBeforeTime), e:
967            # ! Delete user session since it's user certificate is invalid
968            self.deleteUserSession(userSess=userSess)
969            raise       
970   
971        except (UserSessionExpired, X509CertExpired), e:
972            # ! Delete user session since it's user certificate is invalid
973            self.deleteUserSession(userSess=userSess)
974            raise     
975       
976        except Exception, e:
977            raise InvalidUserSession("User session is invalid: %s" % e)
978               
979
980    def deleteUserSession(self, sessID=None, userX509Cert=None, userSess=None):
981        """Delete an existing session by providing a valid session ID or
982        proxy certificate - use for user logout
983
984        deleteUserSession([userX509Cert]|[sessID]|[userSess])
985       
986        @type userX509Cert: ndg.security.common.X509.X509Cert
987        @param userX509Cert: proxy certificate corresponding to an existing
988        session to connect to.
989       
990        @type sessID: string
991        @param sessID: similiarly, a web browser session ID linking to an
992        an existing session.
993       
994        @type userSess: UserSession
995        @param userSess: user session object to be deleted
996        """
997       
998        log.debug("Calling SessionManager.deleteUserSession ...")
999       
1000        # Look for a session corresponding to the session ID/proxy cert.
1001        if sessID:
1002            try:
1003                userSess = self.__sessDict[sessID]
1004               
1005            except KeyError:
1006                raise SessionManagerError("Deleting user session - "
1007                                          "no matching session ID exists")
1008
1009            # Get associated user Distinguished Name if a certificate has been
1010            # set
1011            if userSess.credentialWallet.userX509Cert is None:
1012                userDN = None
1013            else:
1014                userDN = str(userSess.credentialWallet.userX509Cert.dn)
1015           
1016        elif userX509Cert:
1017            try:
1018                userDN = str(userX509Cert.dn)
1019               
1020            except Exception, e:
1021                raise SessionManagerError("Parsing input user certificate "
1022                                          "DN for session connect: %s" % e)
1023            try:
1024                userSess = self.__dnDict[userDN]
1025                       
1026            except KeyError:
1027                # User session not found with given proxy cert
1028                raise SessionManagerError("No user session found matching "
1029                                          "input user certificate")       
1030        elif userSess:
1031            if userSess.credentialWallet.userX509Cert is None:
1032                userDN = None
1033            else:
1034                userDN = str(userSess.credentialWallet.userX509Cert.dn)
1035        else:
1036            # User session not found with given ID
1037            raise SessionManagerError('"sessID", "userX509Cert" or "userSess" '
1038                                      'keywords must be set')
1039 
1040        # Delete associated sessions
1041        try:
1042            # Each session may have a number of session IDs allocated to
1043            # it. 
1044            #
1045            # Use pop rather than del so that key errors are ignored
1046            for userSessID in userSess.sessIdList:
1047                self.__sessDict.pop(userSessID, None)
1048
1049            self.__dnDict.pop(userDN, None)
1050       
1051        except Exception, e:
1052            raise SessionManagerError("Deleting user session: %s" % e) 
1053
1054        log.info("Deleted user session: user DN = %s, sessID = %s" %
1055                 (userDN, userSess.sessIdList))
1056
1057    def auditSessions(self):
1058        """Remove invalid sessions i.e. ones which have expired"""
1059       
1060        log.debug("Auditing user sessions ...")
1061        for session in self.__sessDict.values():
1062            if not session.isValid():
1063                self.deleteUserSession(userSess=session)
1064           
1065    def getAttCert(self,
1066                   username=None,
1067                   userX509Cert=None,
1068                   sessID=None,
1069                   **credentialWalletKw):
1070        """For a given user, request Attribute Certificate from an Attribute
1071        Authority given by service URI.  If sucessful, an attribute
1072        certificate is added to the user session credential wallet and also
1073        returned from this method
1074       
1075        A user identifier must be provided in the form of a user ID, user X.509
1076        certificate or a user session ID
1077
1078        @type username: string
1079        @param username: username to key into their session
1080
1081        @type userX509Cert: string
1082        @param userX509Cert: user's X.509 certificate to key into their session
1083       
1084        @type sessID: string
1085        @param sessID: user's ID to key into their session
1086       
1087        @type credentialWalletKw: dict
1088        @param **credentialWalletKw: keywords to CredentialWallet.getAttCert
1089        """
1090       
1091        log.debug("Calling SessionManager.getAttCert ...")
1092       
1093        # Retrieve session corresponding to user's session ID using relevant
1094        # input credential
1095        userSess = self._connect2UserSession(username=username, sessID=sessID, 
1096                                             userX509Cert=userX509Cert)
1097       
1098        # The user's Credential Wallet carries out an attribute request to the
1099        # Attribute Authority
1100        attCert = userSess.credentialWallet.getAttCert(**credentialWalletKw)
1101        return attCert
1102
1103       
1104    def auditCredentialRepository(self):
1105        """Remove expired Attribute Certificates from the Credential
1106        Repository"""
1107        log.debug("Calling SessionManager.auditCredentialRepository ...")
1108        self._credentialRepository.auditCredentials()
1109       
1110
1111class AuthNServiceError(Exception):
1112    """Base class for AbstractAuthNService exceptions
1113   
1114    A standard message is raised set by the msg class variable but the actual
1115    exception details are logged to the error log.  The use of a standard
1116    message enbales callers to use its content for user error messages.
1117   
1118    @type msg: basestring
1119    @cvar msg: standard message to be raised for this exception"""
1120    msg = "An error occurred with login"
1121    def __init__(self, *arg, **kw):
1122        Exception.__init__(self, AuthNServiceError.msg, *arg, **kw)
1123        if len(arg) > 0:
1124            msg = arg[0]
1125        else:
1126            msg = AuthNServiceError.msg
1127           
1128        log.error(msg)
1129       
1130class AuthNServiceInvalidCredentials(AuthNServiceError):
1131    """User has provided incorrect username/password.  Raise from logon"""
1132    msg = "Invalid username/password provided"
1133   
1134class AuthNServiceRetrieveError(AuthNServiceError):
1135    """Error with retrieval of information to authenticate user e.g. error with
1136    database look-up.  Raise from logon"""
1137    msg = \
1138    "An error occurred retrieving information to check the login credentials"
1139
1140class AuthNServiceInitError(AuthNServiceError):
1141    """Error with initialisation of AuthNService.  Raise from __init__"""
1142    msg = "An error occurred with the initialisation of the Session " + \
1143        "Manager's Authentication Service"
1144   
1145class AuthNServiceConfigError(AuthNServiceError):
1146    """Error with Authentication configuration.  Raise from __init__"""
1147    msg = "An error occurred with the Session Manager's Authentication " + \
1148        "Service configuration"
1149
1150
1151class AbstractAuthNService(object):
1152    """
1153    An abstract base class to define the authentication service interface for
1154    use with a SessionManager service
1155    """
1156
1157    def __init__(self, propertiesFile=None, **prop):
1158        """Make any initial settings
1159       
1160        Settings are held in a dictionary which can be set from **prop,
1161        a call to setProperties() or by passing settings in an XML file
1162        given by propFilePath
1163       
1164        @type propertiesFile: basestring
1165        @param propertiesFile: set properties via a configuration file
1166        @type **prop: dict
1167        @param **prop: set properties via keywords - see __validKeys
1168        class variable for a list of these
1169        @raise AuthNServiceInitError: error with initialisation
1170        @raise AuthNServiceConfigError: error with configuration
1171        @raise AuthNServiceError: generic exception not described by the other
1172        specific exception types.
1173        """
1174   
1175    def setProperties(self, **prop):
1176        """Update existing properties from an input dictionary
1177        Check input keys are valid names"""
1178        raise NotImplementedError(
1179                            self.setProperties.__doc__.replace('\n       ',''))
1180       
1181    def logon(self, username, passphrase):
1182        """Interface login method
1183       
1184        @type username: basestring
1185        @param username: username of credential
1186       
1187        @type passphrase: basestring
1188        @param passphrase: passphrase corresponding to username
1189        @raise AuthNServiceInvalidCredentials: invalid username/passphrase
1190        @raise AuthNServiceError: error
1191        @raise AuthNServiceRetrieveError: error with retrieval of information
1192        to authenticate user e.g. error with database look-up.
1193        @raise AuthNServiceError: generic exception not described by the other
1194        specific exception types.
1195        @rtype: tuple
1196        @return: this may be either user PKI credentials or an empty tuple
1197        depending on the nature of the authentication service.  The UserSession
1198        object in the Session Manager instance can receive an individual user
1199        certificate and private key as returned by for example MyProxy.  In
1200        this case, the tuple consists of strings in PEM format:
1201         - the user certificate
1202         - corresponding private key
1203         - the issuing certificate. 
1204        The issuing certificate is optional.  It is only set if the user
1205        certificate is a proxy certificate
1206        """
1207        raise NotImplementedError(self.logon.__doc__.replace('\n       ',''))
1208           
Note: See TracBrowser for help on using the repository browser.