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

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

Updated ndg.security.server.wsgi.authz module to include AuthorizationMiddleware? classes to support PIP attribute retrieval with either the NDG Attribute Authority interface (SOAP/WSDL + NDG Attribute Certificates) or (SOAP/SAML + SAML Assertions) - NDGAuthorizationMiddleware and SAMLAuthorizationMiddleware respectively. AuthorizationMiddlewareBase? provides an ABC and AuthorizationMiddleware? definition is an alias to NDGAuthorizationMiddleware for backwards compatibility.

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