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

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

Added and tested dbauthn module to Session Manager authentication interfaces. This uses SQLAlchemy to enable the Session Manager to use database based authentication as an alternative to MyProxy.

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