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

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

SessionMgr? -> SessionManager?

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