source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/attributeauthority.py @ 7698

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/attributeauthority.py@7698
Revision 7698, 54.1 KB checked in by pjkersha, 10 years ago (diff)

Integrated SAML ESGF Group/Role? attribute value type into SAML Attribute Authority client unit tests.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""NDG Attribute Authority server side code
2
3handles security user attribute (role) allocation
4
5NERC Data Grid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "15/04/05"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = '$Id:attributeauthority.py 4367 2008-10-29 09:27:59Z pjkersha $'
13import logging
14log = logging.getLogger(__name__)
15
16import os
17import re
18import traceback
19
20# For parsing of properties file
21try: # python 2.5
22    from xml.etree import cElementTree as ElementTree
23except ImportError:
24    # if you've installed it yourself it comes this way
25    import cElementTree as ElementTree
26
27# SAML 2.0 Attribute Query Support - added 20/08/2009
28from uuid import uuid4
29from datetime import datetime, timedelta
30
31from ndg.saml.utils import SAMLDateTime
32from ndg.saml.saml2.core import (Response, Assertion, Attribute, 
33                                 AttributeStatement, SAMLVersion, Subject, 
34                                 NameID, Issuer, Conditions, AttributeQuery, 
35                                 XSStringAttributeValue, Status, 
36                                 StatusCode, StatusMessage)
37
38from ndg.security.common.saml_utils.esgf import ESGFSamlNamespaces
39from ndg.security.common.X509 import X500DN
40from ndg.security.common.utils import TypedList
41from ndg.security.common.utils.classfactory import instantiateClass
42from ndg.security.common.utils.configfileparsers import (
43    CaseSensitiveConfigParser)
44
45
46class AttributeAuthorityError(Exception):
47    """Exception handling for NDG Attribute Authority class."""
48    def __init__(self, msg):
49        log.error(msg)
50        Exception.__init__(self, msg)
51
52
53class AttributeAuthorityConfigError(Exception):
54    """NDG Attribute Authority error with configuration. e.g. properties file
55    directory permissions or role mapping file"""
56    def __init__(self, msg):
57        log.error(msg)
58        Exception.__init__(self, msg) 
59
60
61class AttributeAuthority(object):
62    """NDG Attribute Authority - rewritten with a SAML 2.0 Attribute Query
63    interface for Earth System Grid
64   
65    @type PROPERTY_DEFAULTS: dict
66    @cvar PROPERTY_DEFAULTS: valid configuration property keywords
67   
68    @type ATTRIBUTE_INTERFACE_PROPERTY_DEFAULTS: dict
69    @cvar ATTRIBUTE_INTERFACE_PROPERTY_DEFAULTS: valid configuration property
70    keywords for the Attribute Interface plugin
71   
72    @type DEFAULT_CONFIG_DIRNAME: string
73    @cvar DEFAULT_CONFIG_DIRNAME: configuration directory under $NDGSEC_DIR -
74    default location for properties file
75   
76    @type DEFAULT_PROPERTY_FILENAME: string
77    @cvar DEFAULT_PROPERTY_FILENAME: default file name for properties file
78    under DEFAULT_CONFIG_DIRNAME
79   
80    @type ATTRIBUTE_INTERFACE_OPTPREFIX: basestring
81    @param ATTRIBUTE_INTERFACE_OPTPREFIX: attribute interface parameters key
82    name - see initAttributeInterface for details
83    """
84
85    DEFAULT_CONFIG_DIRNAME = "conf"
86    DEFAULT_PROPERTY_FILENAME = "attributeAuthority.cfg"
87   
88    # Config file special parameters
89    HERE_OPTNAME = 'here'
90    PREFIX_OPTNAME = 'prefix'
91   
92    # Config file option names
93    ISSUER_NAME_OPTNAME = 'issuerName'
94    ASSERTION_LIFETIME_OPTNAME = 'assertionLifetime'
95   
96    ATTRIBUTE_INTERFACE_OPTPREFIX = 'attributeInterface'
97    ATTRIBUTE_INTERFACE_MOD_FILEPATH_OPTNAME = 'modFilePath'
98    ATTRIBUTE_INTERFACE_CLASSNAME_OPTNAME = 'className'
99   
100    CONFIG_LIST_SEP_PAT = re.compile(',\s*')
101   
102   
103    ATTRIBUTE_INTERFACE_PROPERTY_DEFAULTS = {
104        ATTRIBUTE_INTERFACE_MOD_FILEPATH_OPTNAME:  '',
105        ATTRIBUTE_INTERFACE_CLASSNAME_OPTNAME:    ''
106    }
107   
108    # valid configuration property keywords with accepted default values. 
109    # Values set to not NotImplemented here denote keys which must be specified
110    # in the config
111    PROPERTY_DEFAULTS = { 
112        ISSUER_NAME_OPTNAME:            '',
113        ASSERTION_LIFETIME_OPTNAME:     -1,
114        ATTRIBUTE_INTERFACE_OPTPREFIX:  ATTRIBUTE_INTERFACE_PROPERTY_DEFAULTS
115    }
116
117    __slots__ = (
118        '__assertionLifetime', 
119        '__propFilePath',
120        '__propFileSection',
121        '__propPrefix',
122        '__attributeInterface',
123        '__attributeInterfaceCfg'
124    )
125   
126    def __init__(self):
127        """Create new Attribute Authority instance"""
128        log.info("Initialising service ...")
129       
130        # Initial config file property based attributes
131        self.__assertionLifetime = None
132       
133        self.__propFilePath = None       
134        self.__propFileSection = 'DEFAULT'
135        self.__propPrefix = ''
136       
137        self.__attributeInterfaceCfg = \
138                AttributeAuthority.ATTRIBUTE_INTERFACE_PROPERTY_DEFAULTS.copy()
139       
140    def __getstate__(self):
141        '''Enable pickling with __slots__'''
142        _dict = {}
143        for attrName in AttributeAuthority.__slots__:
144            # Ugly hack to allow for derived classes setting private member
145            # variables
146            if attrName.startswith('__'):
147                attrName = "_AttributeAuthority" + attrName
148               
149            _dict[attrName] = getattr(self, attrName)
150           
151        return _dict
152 
153    def __setstate__(self, attrDict):
154        '''Enable pickling with __slots__'''
155        for attrName, val in attrDict.items():
156            setattr(self, attrName, val)
157   
158    def _getAssertionLifetime(self):
159        return self.__assertionLifetime
160
161    def _setAssertionLifetime(self, value):
162        if isinstance(value, float):
163            self.__assertionLifetime = value
164           
165        elif isinstance(value, (basestring, int, long)):
166            self.__assertionLifetime = float(value)
167        else:
168            raise TypeError('Expecting float, int, long or string type for '
169                            '"assertionLifetime"; got %r' % type(value))
170
171    def _getAttributeInterface(self):
172        return self.__attributeInterface
173
174    def _setAttributeInterface(self, value):
175        if not isinstance(value, AttributeInterface):
176            raise TypeError('Expecting %r type for "attributeInterface" '
177                            'attribute; got %r' %
178                            (AttributeInterface, type(value)))
179           
180        self.__attributeInterface = value
181
182    def _get_attributeInterfaceCfg(self):
183        return self.__attributeInterfaceCfg
184   
185    attributeInterfaceCfg = property(fget=_get_attributeInterfaceCfg,
186                                     doc="Settings for Attribute Interface "
187                                         "initialisation")
188
189    def setPropFilePath(self, val=None):
190        """Set properties file from input or based on environment variable
191        settings
192       
193        @type val: basestring
194        @param val: properties file path"""
195        log.debug("Setting property file path")
196        if not val:
197            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
198                val = os.environ['NDGSEC_AA_PROPFILEPATH']
199               
200            elif 'NDGSEC_DIR' in os.environ:
201                val = os.path.join(os.environ['NDGSEC_DIR'], 
202                                   AttributeAuthority.DEFAULT_CONFIG_DIRNAME,
203                                   AttributeAuthority.DEFAULT_PROPERTY_FILENAME)
204            else:
205                raise AttributeError('Unable to set default Attribute '
206                                     'Authority properties file path: neither '
207                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
208                                     ' environment variables are set')
209               
210        if not isinstance(val, basestring):
211            raise AttributeError("Input Properties file path "
212                                 "must be a valid string.")
213     
214        self.__propFilePath = os.path.expandvars(val)
215        log.debug("Path set to: %s" % val)
216       
217    def getPropFilePath(self):
218        '''Get the properties file path
219       
220        @rtype: basestring
221        @return: properties file path'''
222        return self.__propFilePath
223       
224    # Also set up as a property
225    propFilePath = property(fset=setPropFilePath,
226                            fget=getPropFilePath,
227                            doc="path to file containing Attribute Authority "
228                                "configuration parameters.  It defaults to "
229                                "$NDGSEC_AA_PROPFILEPATH or if not set, "
230                                "$NDGSEC_DIR/conf/attributeAuthority.cfg")   
231   
232    def setPropFileSection(self, val=None):
233        """Set section name to read properties from ini file.  This is set from
234        input or based on environment variable setting
235        NDGSEC_AA_PROPFILESECTION
236       
237        @type val: basestring
238        @param val: section name"""
239        if not val:
240            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
241               
242        if not isinstance(val, basestring):
243            raise AttributeError("Input Properties file section name "
244                                 "must be a valid string.")
245     
246        self.__propFileSection = val
247        log.debug("Properties file section set to: \"%s\"" % val)
248       
249    def getPropFileSection(self):
250        '''Get the section name to extract properties from an ini file -
251        DOES NOT apply to XML file properties
252       
253        @rtype: basestring
254        @return: section name'''
255        return self.__propFileSection
256       
257    # Also set up as a property
258    propFileSection = property(fset=setPropFileSection,
259                               fget=getPropFileSection,
260                               doc="Set the file section name for ini file "
261                                   "properties")   
262   
263    def setPropPrefix(self, val=None):
264        """Set prefix for properties read from ini file.  This is set from
265        input or based on environment variable setting
266        NDGSEC_AA_PROPFILEPREFIX
267       
268        DOES NOT apply to XML file properties
269       
270        @type val: basestring
271        @param val: section name"""
272        log.debug("Setting property file section name")
273        if val is None:
274            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
275               
276        if not isinstance(val, basestring):
277            raise AttributeError("Input Properties file section name "
278                                 "must be a valid string.")
279     
280        self.__propPrefix = val
281        log.debug("Properties file section set to: %s" % val)
282       
283    def getPropPrefix(self):
284        '''Get the prefix name used for properties in an ini file -
285        DOES NOT apply to XML file properties
286       
287        @rtype: basestring
288        @return: section name'''
289        return self.__propPrefix
290   
291       
292    # Also set up as a property
293    propPrefix = property(fset=setPropPrefix,
294                          fget=getPropPrefix,
295                          doc="Set a prefix for ini file properties")   
296
297    assertionLifetime = property(fget=_getAssertionLifetime, 
298                                 fset=_setAssertionLifetime, 
299                                 doc="validity lifetime (s) for Attribute "
300                                     "assertions issued")
301
302    attributeInterface = property(fget=_getAttributeInterface, 
303                                  fset=_setAttributeInterface,
304                                  doc="Attribute Interface object")
305       
306    @classmethod
307    def fromPropertyFile(cls, propFilePath=None, section='DEFAULT',
308                         prefix='attributeauthority.'):
309        """Create new NDG Attribute Authority instance from the property file
310        settings
311
312        @type propFilePath: string
313        @param propFilePath: path to file containing Attribute Authority
314        configuration parameters.  It defaults to $NDGSEC_AA_PROPFILEPATH or
315        if not set, $NDGSEC_DIR/conf/attributeAuthority.cfg
316        @type section: basestring
317        @param section: section of properties file to read from.
318        properties files
319        @type prefix: basestring
320        @param prefix: set a prefix for filtering attribute authority
321        property names - useful where properties are being parsed from a file
322        section containing parameter names for more than one application
323        """
324           
325        attributeAuthority = AttributeAuthority()
326        if section:
327            attributeAuthority.propFileSection = section
328           
329        if prefix:
330            attributeAuthority.propPrefix = prefix
331
332        # If path is None it will default to setting derived from environment
333        # variable - see setPropFilePath()
334        attributeAuthority.propFilePath = propFilePath
335                     
336        attributeAuthority.readProperties()
337        attributeAuthority.initialise()
338   
339        return attributeAuthority
340
341    @classmethod
342    def fromProperties(cls, prefix='attributeauthority.', **prop):
343        """Create new NDG Attribute Authority instance from input property
344        keywords
345
346        @type propPrefix: basestring
347        @param propPrefix: set a prefix for filtering attribute authority
348        property names - useful where properties are being parsed from a file
349        section containing parameter names for more than one application
350        """
351        attributeAuthority = AttributeAuthority()
352        if prefix:
353            attributeAuthority.propPrefix = prefix
354               
355        attributeAuthority.setProperties(**prop)
356        attributeAuthority.initialise()
357       
358        return attributeAuthority
359   
360    def initialise(self):
361        """Convenience method for set up of Attribute Interface, map
362        configuration and PKI"""
363
364        # Instantiate Certificate object
365        log.debug("Reading and checking Attribute Authority X.509 cert. ...")
366       
367        # Load user - user attribute look-up plugin
368        self.initAttributeInterface()
369
370    def setProperties(self, **prop):
371        """Set configuration from an input property dictionary
372        @type prop: dict
373        @param prop: properties dictionary containing configuration items
374        to be set
375        """
376        lenPropPrefix = len(self.propPrefix)
377       
378        # '+ 1' allows for the dot separator
379        lenAttributeInterfacePrefix = len(
380                        AttributeAuthority.ATTRIBUTE_INTERFACE_OPTPREFIX) + 1
381       
382        for name, val in prop.items():
383            if name.startswith(self.propPrefix):
384                name = name[lenPropPrefix:]
385           
386            if name.startswith(
387                            AttributeAuthority.ATTRIBUTE_INTERFACE_OPTPREFIX):
388                name = name[lenAttributeInterfacePrefix:]
389                self.attributeInterfaceCfg[name] = val
390                continue
391           
392            if name not in AttributeAuthority.PROPERTY_DEFAULTS:
393                raise AttributeError('Invalid attribute name "%s"' % name)
394           
395            if isinstance(val, basestring):
396                val = os.path.expandvars(val)
397           
398            if isinstance(AttributeAuthority.PROPERTY_DEFAULTS[name], list):
399                val = AttributeAuthority.CONFIG_LIST_SEP_PAT.split(val)
400               
401            # This makes an implicit call to the appropriate property method
402            try:
403                setattr(self, name, val)
404            except AttributeError:
405                raise AttributeError("Can't set attribute \"%s\": %s" % 
406                                     (name, traceback.format_exc()))
407           
408    def readProperties(self):
409        '''Read the properties files and do some checking/converting of input
410        values
411        '''
412        if not os.path.isfile(self.propFilePath):
413            raise IOError('Error parsing properties file "%s": No such file' % 
414                          self.propFilePath)
415           
416        defaultItems = {
417            AttributeAuthority.HERE_OPTNAME: os.path.dirname(self.propFilePath)
418        }
419       
420        cfg = CaseSensitiveConfigParser(defaults=defaultItems)
421        cfg.read(self.propFilePath)
422       
423        if cfg.has_option(self.propFileSection, 
424                          AttributeAuthority.PREFIX_OPTNAME):
425            self.propPrefix = cfg.get(self.propFileSection, 
426                                      AttributeAuthority.PREFIX_OPTNAME)
427           
428        cfgItems = dict([(name, val) 
429                         for name, val in cfg.items(self.propFileSection)
430                         if (name != AttributeAuthority.HERE_OPTNAME and 
431                             name != AttributeAuthority.PREFIX_OPTNAME)])
432        self.setProperties(**cfgItems)
433
434    def initAttributeInterface(self):
435        '''Load host sites custom user roles interface to enable the AA to
436        # assign roles in an attribute certificate on a getAttCert request'''
437        classProperties = {}
438        classProperties.update(self.attributeInterfaceCfg)
439       
440        className = classProperties.pop('className', None) 
441        if className is None:
442            raise AttributeAuthorityConfigError('No Attribute Interface '
443                                                '"className" property set')
444       
445        # file path may be omitted   
446        modFilePath = classProperties.pop('modFilePath', None) 
447                     
448        self.__attributeInterface = instantiateClass(className,
449                                             moduleFilePath=modFilePath,
450                                             objectType=AttributeInterface,
451                                             classProperties=classProperties)
452
453    def samlAttributeQuery(self, attributeQuery, samlResponse):
454        """Respond to SAML 2.0 Attribute Query
455        """
456        if not isinstance(attributeQuery, AttributeQuery):
457            raise TypeError('Expecting %r for attribute query; got %r' %
458                            (AttributeQuery, type(attributeQuery)))
459       
460        # Attribute Query validation ...
461        utcNow = datetime.utcnow()
462        if (attributeQuery.subject.nameID.format != 
463            ESGFSamlNamespaces.NAMEID_FORMAT):
464            log.error('SAML Attribute Query subject format is %r; expecting '
465                      '%r' % (attributeQuery.subject.nameID.format,
466                                ESGFSamlNamespaces.NAMEID_FORMAT))
467            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
468            samlResponse.status.statusMessage.value = \
469                                "Subject Name ID format is not recognised"
470            return samlResponse
471       
472        elif attributeQuery.issuer.format != Issuer.X509_SUBJECT:
473            log.error('SAML Attribute Query issuer format is %r; expecting '
474                      '%r' % (attributeQuery.issuer.format,
475                              Issuer.X509_SUBJECT))
476            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
477            samlResponse.status.statusMessage.value = \
478                                            "Issuer format is not recognised"
479            return samlResponse
480       
481        try:
482            # Return a dictionary of name, value pairs
483            self.attributeInterface.getAttributes(attributeQuery, samlResponse)
484           
485        except InvalidUserId, e:
486            log.exception(e)
487            samlResponse.status.statusCode.value = \
488                                        StatusCode.UNKNOWN_PRINCIPAL_URI
489            return samlResponse
490           
491        except UserIdNotKnown, e:
492            log.exception(e)
493            samlResponse.status.statusCode.value = \
494                                        StatusCode.UNKNOWN_PRINCIPAL_URI
495            samlResponse.status.statusMessage.value = str(e)
496            return samlResponse
497           
498        except InvalidRequestorId, e:
499            log.exception(e)
500            samlResponse.status.statusCode.value = StatusCode.REQUEST_DENIED_URI
501            samlResponse.status.statusMessage.value = str(e)
502            return samlResponse
503           
504        except AttributeReleaseDenied, e:
505            log.exception(e)
506            samlResponse.status.statusCode.value = \
507                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
508            samlResponse.status.statusMessage.value = str(e)
509            return samlResponse
510           
511        except AttributeNotKnownError, e:
512            log.exception(e)
513            samlResponse.status.statusCode.value = \
514                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
515            samlResponse.status.statusMessage.value = str(e)
516            return samlResponse
517           
518        except Exception, e:
519            log.exception("Unexpected error calling Attribute Interface "
520                          "for subject [%s] and query issuer [%s]" %
521                          (attributeQuery.subject.nameID.value,
522                           attributeQuery.issuer.value))
523           
524            # SAML spec says application server should set a HTTP 500 Internal
525            # Server error in this case
526            raise 
527
528        return samlResponse
529
530    def samlAttributeQueryFactory(self):
531        """Factory method to create SAML Attribute Query wrapper function
532        @rtype: function
533        @return: samlAttributeQuery method function wrapper
534        """
535        def samlAttributeQueryWrapper(attributeQuery, response):
536            """Attribute Query method.  This must adhere to the function
537            signature specified by
538            ndg.security.server.wsgi.saml.SOAPQueryInterfaceMiddleware
539            @type attributeQuery: ndg.saml.saml2.core.AttributeQuery
540            @param attributeQuery: SAML Attribute Query
541            @rtype: ndg.saml.saml2.core.Response
542            @return: SAML response
543            """
544            return self.samlAttributeQuery(attributeQuery, response)
545       
546        return samlAttributeQueryWrapper
547   
548               
549class AttributeInterfaceError(Exception):
550    """Exception handling for NDG Attribute Authority User Roles interface
551    class."""
552 
553                     
554class AttributeInterfaceConfigError(AttributeInterfaceError):
555    """Invalid configuration set for Attribute interface"""
556 
557                     
558class AttributeInterfaceRetrieveError(AttributeInterfaceError):
559    """Error retrieving attributes for Attribute interface class"""
560
561                       
562class AttributeReleaseDenied(AttributeInterfaceError):
563    """Requestor was denied release of the requested attributes"""
564
565                       
566class AttributeNotKnownError(AttributeInterfaceError):
567    """Requested attribute names are not known to this authority"""
568
569
570class InvalidRequestorId(AttributeInterfaceError):
571    """Requestor is not known or not allowed to request attributes"""
572   
573
574class UserIdNotKnown(AttributeInterfaceError): 
575    """User ID passed to getAttributes is not known to the authority"""
576   
577   
578class InvalidUserId(AttributeInterfaceError):
579    """User Id passed to getAttributes is invalid"""
580   
581   
582class InvalidAttributeFormat(AttributeInterfaceError):
583    """Format for Attribute requested is invalid or not supported"""
584   
585     
586class AttributeInterface(object):
587    """An abstract base class to define the user roles interface to an
588    Attribute Authority.
589
590    Each NDG data centre should implement a derived class which implements
591    the way user roles are provided to its representative Attribute Authority.
592   
593    Roles are expected to indexed by user Distinguished Name (DN).  They
594    could be stored in a database or file."""
595   
596    # Enable derived classes to use slots if desired
597    __slots__ = ()
598   
599    # User defined class may wish to specify a URI for a database interface or
600    # path for a user roles configuration file
601    def __init__(self, **prop):
602        """User Roles base class - derive from this class to define
603        roles interface to Attribute Authority
604       
605        @type prop: dict
606        @param prop: custom properties to pass to this class
607        """
608
609    def getRoles(self, userId):
610        """Virtual method - Derived method should return the roles for the
611        given user's Id or else raise an exception
612       
613        @type userId: string
614        @param userId: user identity e.g. user Distinguished Name
615        @rtype: list
616        @return: list of roles for the given user ID
617        @raise AttributeInterfaceError: an error occured requesting
618        attributes
619        """
620        raise NotImplementedError(self.getRoles.__doc__)
621 
622    def getAttributes(self, attributeQuery, response):
623        """Virtual method should be implemented in a derived class to enable
624        AttributeAuthority.samlAttributeQuery - The derived method should
625        return the attributes requested for the given user's Id or else raise
626        an exception
627       
628        @type attributeQuery: saml.saml2.core.AttributeQuery
629        @param userId: query containing requested attributes
630        @type: saml.saml2.core.Response
631        @param: Response - add an assertion with the list of attributes
632        for the given subject ID in the query or set an error Status code and
633        message
634        @raise AttributeInterfaceError: an error occured requesting
635        attributes
636        @raise AttributeReleaseDeniedError: Requestor was denied release of the
637        requested attributes
638        @raise AttributeNotKnownError: Requested attribute names are not known
639        to this authority
640        """
641        raise NotImplementedError(self.getAttributes.__doc__)
642
643
644class CSVFileAttributeInterface(AttributeInterface):
645    """Attribute Interface based on a Comma Separated Variable file containing
646    user identities and associated attributes.  For test/development purposes
647    only.  The SAML getAttributes method is NOT implemented here
648   
649    The expected file format is:
650   
651    <userID>, <role1>, <role2>, ... <roleN>
652    """
653    def __init__(self, propertiesFilePath=None):
654        """
655        @param propertiesFilePath: file path to Comma Separated file
656        containing user ids and roles
657        @type propertiesFilePath: basestring
658        """
659        if propertiesFilePath is None:
660            raise AttributeError("Expecting propertiesFilePath setting")
661       
662        propertiesFile = open(propertiesFilePath)
663        lines = propertiesFile.readlines()
664       
665        self.attributeMap = {}
666        for line in lines:
667            fields = re.split(',\s*', line.strip())
668            self.attributeMap[fields[0]] = fields[1:]
669   
670    def getRoles(self, userId):
671        """
672        @param userId: user identity to key into attributeMap
673        @type userId: basestring
674        """ 
675        log.debug('CSVFileAttributeInterface.getRoles for user "%s" ...', 
676                  userId)
677        return self.attributeMap.get(userId, [])
678
679
680# Properties file
681from ConfigParser import SafeConfigParser, NoOptionError
682
683try:
684    # PostgreSQL interface
685    from psycopg2 import connect
686except ImportError:
687    pass
688
689class PostgresAttributeInterface(AttributeInterface):
690    """User Roles interface to Postgres database
691   
692    The SAML getAttributes method is NOT implemented
693   
694    The configuration file follows the form,
695   
696    [Connection]
697    # name of database
698    dbName: user.db
699   
700    # database host machine
701    host: mydbhost.ac.uk
702   
703    # database account username
704    username: mydbaccount
705   
706    # Password - comment out to prompt from stdin instead
707    pwd: mydbpassword
708   
709    [getRoles]
710    query0: select distinct grp from users_table, where user = '%%s'
711    defaultRoles = publicRole
712    """
713
714    CONNECTION_SECTION_NAME = "Connection"
715    GETROLES_SECTION_NAME = "getRoles"
716    HOST_OPTION_NAME = "host"
717    DBNAME_OPTION_NAME = "dbName"
718    USERNAME_OPTION_NAME = "username"
719    PWD_OPTION_NAME = "pwd"
720    QUERYN_OPTION_NAME = "query%d"
721    DEFAULT_ROLES_OPTION_NAME = "defaultRoles"
722   
723    def __init__(self, propertiesFilePath=None):
724        """Connect to Postgres database"""
725        self.__con = None
726        self.__host = None
727        self.__dbName = None
728        self.__username = None
729        self.__pwd = None
730
731        if propertiesFilePath is None:
732            raise AttributeError("No Configuration file was set")
733
734        self.readConfigFile(propertiesFilePath)
735
736    def __del__(self):
737        """Close database connection"""
738        self.close()
739
740    def readConfigFile(self, propertiesFilePath):
741        """Read the configuration for the database connection
742
743        @type propertiesFilePath: string
744        @param propertiesFilePath: file path to config file"""
745
746        if not isinstance(propertiesFilePath, basestring):
747            raise TypeError("Input Properties file path must be a valid "
748                            "string; got %r" % type(propertiesFilePath))
749
750        cfg = SafeConfigParser()
751        cfg.read(propertiesFilePath)
752
753        self.__host = cfg.get(
754                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
755                        PostgresAttributeInterface.HOST_OPTION_NAME)
756        self.__dbName = cfg.get(
757                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
758                        PostgresAttributeInterface.DBNAME_OPTION_NAME)
759        self.__username = cfg.get(
760                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
761                        PostgresAttributeInterface.USERNAME_OPTION_NAME)
762        self.__pwd = cfg.get(
763                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
764                        PostgresAttributeInterface.PWD_OPTION_NAME)
765
766        try:
767            self.__getRolesQuery = []
768            for i in range(10):
769                queryStr = cfg.get(
770                        PostgresAttributeInterface.GETROLES_SECTION_NAME, 
771                        PostgresAttributeInterface.QUERYN_OPTION_NAME % i)
772                self.__getRolesQuery += [queryStr]
773        except NoOptionError:
774             # Continue until no more query<n> items left
775             pass
776
777        # This option may be omitted in the config file
778        try:
779            self.__defaultRoles = cfg.get(
780                PostgresAttributeInterface.GETROLES_SECTION_NAME, 
781                PostgresAttributeInterface.DEFAULT_ROLES_OPTION_NAME).split()
782        except NoOptionError:
783            self.__defaultRoles = []
784
785    def connect(self,
786                username=None,
787                dbName=None,
788                host=None,
789                pwd=None,
790                prompt="Database password: "):
791        """Connect to database
792
793        Values for keywords omitted are derived from the config file.  If pwd
794        is not in the config file it will be prompted for from stdin
795
796        @type username: string
797        @keyword username: database account username
798        @type dbName: string
799        @keyword dbName: name of database
800        @type host: string
801        @keyword host: database host machine
802        @type pwd: string
803        @keyword pwd: password for database account.  If omitted and not in
804        the config file it will be prompted for from stdin
805        @type prompt: string
806        @keyword prompt: override default password prompt"""
807
808        if not host:
809            host = self.__host
810
811        if not dbName:
812            dbName = self.__dbName
813
814        if not username:
815            username = self.__username
816
817        if not pwd:
818            pwd = self.__pwd
819
820            if not pwd:
821                import getpass
822                pwd = getpass.getpass(prompt=prompt)
823
824        try:
825            self.__db = connect("host=%s dbname=%s user=%s password=%s" % \
826                                (host, dbName, username, pwd))
827            self.__cursor = self.__db.cursor()
828
829        except NameError, e:
830            raise AttributeInterfaceError("psycopg2 Postgres package not "
831                                          "installed? %s" % e)
832        except Exception, e:
833            raise AttributeInterfaceError("Error connecting to database "
834                                          "\"%s\": %s" % (dbName, e))
835
836    def close(self):
837        """Close database connection"""
838        if self.__con:
839            self.__con.close()
840
841    def getRoles(self, userId):
842        """Return valid roles for the given userId
843
844        @type userId: basestring
845        @param userId: user identity"""
846
847        try:
848            self.connect()
849
850            # Process each query in turn appending role names
851            roles = self.__defaultRoles[:]
852            for query in self.__getRolesQuery:
853                try:
854                    self.__cursor.execute(query % userId)
855                    queryRes = self.__cursor.fetchall()
856
857                except Exception, e:
858                    raise AttributeInterfaceError("Query for %s: %s" %
859                                                  (userId, e))
860
861                roles += [res[0] for res in queryRes if res[0]]
862        finally:
863            self.close()
864
865        return roles
866
867    def __getCursor(self):
868        """Return a database cursor instance"""
869        return self.__cursor
870
871    cursor = property(fget=__getCursor, doc="database cursor")
872
873
874import traceback
875from string import Template
876try:
877    from sqlalchemy import create_engine, exc
878    sqlAlchemyInstalled = True
879except ImportError:
880    sqlAlchemyInstalled = False
881   
882
883class SQLAlchemyAttributeInterface(AttributeInterface):
884    '''SQLAlchemy based Attribute interface enables the Attribute Authority
885    to interface to any database type supported by it
886   
887    @type SQLQUERY_USERID_KEYNAME: basestring
888    @cvar SQLQUERY_USERID_KEYNAME: key corresponding to string to be
889    substituted into attribute query for user identifier e.g.
890   
891    select attr from user_table where username = $userId
892   
893    @type SAML_VALID_REQUESTOR_DNS_PAT: _sre.SRE_Pattern
894    @param SAML_VALID_REQUESTOR_DNS_PAT: regular expression to split list of
895    SAML requestor DNs.  These must comma separated.  Each comma may be
896    separated by any white space including new line characters
897    ''' 
898    DEFAULT_SAML_ASSERTION_LIFETIME = timedelta(seconds=60*60*8) 
899     
900    SQLQUERY_USERID_KEYNAME = 'userId'
901   
902    ISSUER_NAME_FORMAT = Issuer.X509_SUBJECT
903    CONNECTION_STRING_OPTNAME = 'connectionString'
904    ATTRIBUTE_SQLQUERY_OPTNAME = 'attributeSqlQuery'
905    SAML_SUBJECT_SQLQUERY_OPTNAME = 'samlSubjectSqlQuery'
906    SAML_VALID_REQUESTOR_DNS_OPTNAME = 'samlValidRequestorDNs'
907    SAML_ASSERTION_LIFETIME_OPTNAME = 'samlAssertionLifetime'
908    SAML_ATTRIBUTE2SQLQUERY_OPTNAME = 'samlAttribute2SqlQuery'
909    SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN = len(SAML_ATTRIBUTE2SQLQUERY_OPTNAME)
910   
911    SAML_ATTRIBUTE2SQLQUERY_ATTRNAME_DELIMITERS = ('.', '_')
912   
913    __slots__ = (
914        CONNECTION_STRING_OPTNAME,
915        ATTRIBUTE_SQLQUERY_OPTNAME,
916        SAML_SUBJECT_SQLQUERY_OPTNAME,
917        SAML_VALID_REQUESTOR_DNS_OPTNAME,
918        SAML_ASSERTION_LIFETIME_OPTNAME,
919        SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
920    )
921    __PRIVATE_ATTR_PREFIX = '_SQLAlchemyAttributeInterface__'
922    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
923    del i
924   
925#    For Reference - split based on space separated ' or " quoted items
926#    SAML_VALID_REQUESTOR_DNS_PAT = re.compile("['\"]?\s*['\"]")
927   
928    SAML_VALID_REQUESTOR_DNS_PAT = re.compile(',\s*')
929   
930    def __init__(self, **properties):
931        '''Instantiate object taking in settings from the input properties'''
932        log.debug('Initialising SQLAlchemyAttributeInterface instance ...')
933       
934        if not sqlAlchemyInstalled:
935            raise AttributeInterfaceConfigError("SQLAlchemy is not installed")
936
937        self.__connectionString = None
938        self.__attributeSqlQuery = None
939        self.__samlSubjectSqlQuery = None
940        self.__samlValidRequestorDNs = []
941        self.__samlAssertionLifetime = \
942            SQLAlchemyAttributeInterface.DEFAULT_SAML_ASSERTION_LIFETIME
943        self.__samlAttribute2SqlQuery = {}
944       
945        self.setProperties(**properties)
946
947    def __setattr__(self, name, value):
948        """Provide a way to set the attribute map by dynamically handling
949        attribute names containing the SAML attribute name as a suffix e.g.
950       
951        attributeInterface.samlAttribute2SqlQuery_firstName = 'Philip'
952       
953        will update __samlAttribute2SqlQuery with the 'firstName', 'Philip'
954        key value pair.  Similarly,
955       
956        setattr('samlAttribute2SqlQuery.emailAddress', 'pjk@somewhere.ac.uk')
957       
958        sets __samlAttribute2SqlQuery with the 'emailAddress',
959        'pjk@somewhere.ac.uk' key value pair
960       
961        This is useful in enabling settings to be made direct from a dict of
962        option name and values parsed from an ini file.
963        """
964        cls = SQLAlchemyAttributeInterface
965       
966        if name in cls.__slots__:
967            object.__setattr__(self, name, value)
968           
969        elif (len(name) > cls.SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN and
970              name[cls.SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN] in 
971              cls.SAML_ATTRIBUTE2SQLQUERY_ATTRNAME_DELIMITERS):
972            # A special 'samlAttribute2SqlQuery[._]+' attribute name has been
973            # found.  The first item is the attribute name and the second, the
974            # corresponding SQL query to get the values corresponding to that
975            # name.           
976            samlAttributeName, samlAttributeSqlQuery = value.split(None, 1)
977           
978            # Items may be quoted with " quotes
979            self.__samlAttribute2SqlQuery[samlAttributeName.strip('"')
980                                          ] = samlAttributeSqlQuery.strip('"')
981        else:
982            raise AttributeError("'SQLAlchemyAttributeInterface' has no "
983                                 "attribute %r" % name)
984
985    def setProperties(self, prefix='', **properties):
986        for name, val in properties.items():
987            if prefix:
988                if name.startswith(prefix):
989                    name = name.replace(prefix, '', 1)
990                    setattr(self, name, val)
991            else:
992                setattr(self, name, val)
993           
994    def _getSamlAssertionLifetime(self):
995        return self.__samlAssertionLifetime
996
997    def _setSamlAssertionLifetime(self, value):
998        if isinstance(value, timedelta):
999            self.__samlAssertionLifetime = value
1000           
1001        if isinstance(value, (float, int, long)):
1002            self.__samlAssertionLifetime = timedelta(seconds=value)
1003           
1004        elif isinstance(value, basestring):
1005            self.__samlAssertionLifetime = timedelta(seconds=float(value))
1006        else:
1007            raise TypeError('Expecting float, int, long, string or timedelta '
1008                'type for "%s"; got %r' % 
1009                (SQLAlchemyAttributeInterface.SAML_ASSERTION_LIFETIME_OPTNAME,
1010                 type(value)))
1011
1012    samlAssertionLifetime = property(_getSamlAssertionLifetime, 
1013                                     _setSamlAssertionLifetime, 
1014                                     doc="Time validity for SAML Assertion "
1015                                         "set in SAML Response returned from "
1016                                         "getAttributes")
1017
1018    def _getSamlSubjectSqlQuery(self):
1019        return self.__samlSubjectSqlQuery
1020
1021    def _setSamlSubjectSqlQuery(self, value):
1022        if not isinstance(value, basestring):
1023            raise TypeError('Expecting string type for "%s" attribute; got %r'%
1024                    (SQLAlchemyAttributeInterface.SAML_SUBJECT_SQLQUERY_OPTNAME,
1025                     type(value)))
1026           
1027        self.__samlSubjectSqlQuery = value
1028
1029    samlSubjectSqlQuery = property(_getSamlSubjectSqlQuery, 
1030                                   _setSamlSubjectSqlQuery, 
1031                                   doc="SAML Subject SQL Query")
1032
1033    def _getSamlAttribute2SqlQuery(self):
1034        return self.__samlAttribute2SqlQuery
1035
1036    def _setSamlAttribute2SqlQuery(self, value):
1037        if isinstance(value, dict):
1038            # Validate string type for keys and values
1039            invalidItems = [(k, v) for k, v in value.items() 
1040                            if (not isinstance(k, basestring) or 
1041                                not isinstance(v, basestring))]
1042            if invalidItems:
1043                raise TypeError('Expecting string type for "%s" dict items; '
1044                                'got these/this invalid item(s) %r' % 
1045                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
1046                 invalidItems))
1047               
1048            self.__samlAttribute2SqlQuery = value
1049           
1050        elif isinstance(value, (tuple, list)):
1051            for query in value:
1052                if not isinstance(query, basestring):
1053                    raise TypeError('Expecting string type for "%s" '
1054                                    'attribute items; got %r' %
1055                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
1056                 type(value)))
1057                   
1058            self.__samlAttribute2SqlQuery = value                 
1059        else:
1060            raise TypeError('Expecting dict type for "%s" attribute; got %r' %
1061                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
1062                 type(value)))
1063           
1064    samlAttribute2SqlQuery = property(_getSamlAttribute2SqlQuery, 
1065                                      _setSamlAttribute2SqlQuery, 
1066                                      doc="SQL Query or queries to obtain the "
1067                                          "attribute information to respond "
1068                                          "a SAML attribute query.  The "
1069                                          "attributes returned from each "
1070                                          "query concatenated together, must "
1071                                          "exactly match the SAML attribute "
1072                                          "names set in the samlAttributeNames "
1073                                          "property")
1074
1075    def _getSamlValidRequestorDNs(self):
1076        return self.__samlValidRequestorDNs
1077
1078    def _setSamlValidRequestorDNs(self, value):
1079        if isinstance(value, basestring):
1080           
1081            pat = SQLAlchemyAttributeInterface.SAML_VALID_REQUESTOR_DNS_PAT
1082            self.__samlValidRequestorDNs = [
1083                X500DN.fromString(dn) for dn in pat.split(value)
1084            ]
1085           
1086        elif isinstance(value, (tuple, list)):
1087            self.__samlValidRequestorDNs = [X500DN.fromString(dn) 
1088                                            for dn in value]
1089        else:
1090            raise TypeError('Expecting list/tuple or basestring type for "%s" '
1091                'attribute; got %r' %
1092                (SQLAlchemyAttributeInterface.SAML_VALID_REQUESTOR_DNS_OPTNAME,
1093                 type(value)))
1094   
1095    samlValidRequestorDNs = property(_getSamlValidRequestorDNs, 
1096                                     _setSamlValidRequestorDNs, 
1097                                     doc="list of certificate Distinguished "
1098                                         "Names referring to the client "
1099                                         "identities permitted to query the "
1100                                         "Attribute Authority via the SAML "
1101                                         "Attribute Query interface")
1102   
1103    def _getConnectionString(self):
1104        return self.__connectionString
1105
1106    def _setConnectionString(self, value):
1107        if not isinstance(value, basestring):
1108            raise TypeError('Expecting string type for "%s" attribute; got %r'%
1109                        (SQLAlchemyAttributeInterface.CONNECTION_STRING_OPTNAME,
1110                         type(value)))
1111        self.__connectionString = value
1112
1113    connectionString = property(fget=_getConnectionString, 
1114                                fset=_setConnectionString, 
1115                                doc="Database connection string")
1116
1117    def _getAttributeSqlQuery(self):
1118        return self.__attributeSqlQuery
1119
1120    def _setAttributeSqlQuery(self, value):
1121        if not isinstance(value, basestring):
1122            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
1123                    (SQLAlchemyAttributeInterface.ATTRIBUTE_SQLQUERY_OPTNAME,
1124                     type(value)))
1125        self.__attributeSqlQuery = value
1126
1127    attributeSqlQuery = property(fget=_getAttributeSqlQuery, 
1128                                 fset=_setAttributeSqlQuery, 
1129                                 doc="SQL Query for attribute query")
1130   
1131    def getRoles(self, userId):     
1132        """Return valid roles for the given userId
1133
1134        @type userId: basestring
1135        @param userId: user identity
1136        @rtype: list
1137        @return: list of roles for the given user
1138        """
1139
1140        dbEngine = create_engine(self.connectionString)
1141        connection = dbEngine.connect()
1142       
1143        try:
1144            queryInputs = {
1145                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME:
1146                userId
1147            }
1148            query = Template(self.attributeSqlQuery).substitute(queryInputs)
1149            result = connection.execute(query)
1150
1151        except exc.ProgrammingError:
1152            raise AttributeInterfaceRetrieveError("Error with SQL Syntax: %s" %
1153                                                  traceback.format_exc())
1154        finally:
1155            connection.close()
1156
1157        try:
1158            attributes = [attr for attr in result][0][0]
1159       
1160        except (IndexError, TypeError):
1161            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
1162                                                  traceback.format_exc())
1163       
1164        log.debug('Attributes=%r retrieved for user=%r' % (attributes, 
1165                                                           userId))
1166       
1167        return attributes
1168
1169    def getAttributes(self, attributeQuery, response):
1170        """Attribute Authority SAML AttributeQuery
1171       
1172        @type attributeQuery: saml.saml2.core.AttributeQuery
1173        @param userId: query containing requested attributes
1174        @type: saml.saml2.core.Response
1175        @param: Response - add an assertion with the list of attributes
1176        for the given subject ID in the query or set an error Status code and
1177        message
1178        @raise AttributeInterfaceError: an error occured requesting
1179        attributes
1180        @raise AttributeReleaseDeniedError: Requestor was denied release of the
1181        requested attributes
1182        @raise AttributeNotKnownError: Requested attribute names are not known
1183        to this authority
1184        """
1185        userId = attributeQuery.subject.nameID.value
1186        requestedAttributeNames = [attribute.name
1187                                   for attribute in attributeQuery.attributes]
1188       
1189        requestorDN = X500DN.fromString(attributeQuery.issuer.value)
1190
1191        if not self._queryDbForSamlSubject(userId):
1192            raise UserIdNotKnown('Subject Id "%s" is not known to this '
1193                                 'authority' % userId)
1194
1195        if requestorDN not in self.samlValidRequestorDNs:
1196            raise InvalidRequestorId('Requestor identity "%s" is invalid' %
1197                                     requestorDN)
1198
1199        unknownAttrNames = [attrName for attrName in requestedAttributeNames
1200                            if attrName not in self.samlAttribute2SqlQuery]
1201
1202        if len(unknownAttrNames) > 0:
1203            raise AttributeNotKnownError("Unknown attributes requested: %r" %
1204                                         unknownAttrNames)
1205       
1206        # Create a new assertion to hold the attributes to be returned
1207        assertion = Assertion()
1208
1209        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
1210        assertion.id = str(uuid4())
1211        assertion.issueInstant = response.issueInstant
1212   
1213        # Assumes SAML response issuer name set independently -
1214        # ndg.security.server.wsgi.saml.SOAPQueryInterfaceMiddleware
1215        assertion.issuer = Issuer()
1216        assertion.issuer.value = response.issuer.value
1217        assertion.issuer.format = Issuer.X509_SUBJECT
1218
1219        assertion.conditions = Conditions()
1220        assertion.conditions.notBefore = assertion.issueInstant
1221        assertion.conditions.notOnOrAfter = (assertion.conditions.notBefore + 
1222                                             self.samlAssertionLifetime)
1223
1224        assertion.subject = Subject()
1225        assertion.subject.nameID = NameID()
1226        assertion.subject.nameID.format = attributeQuery.subject.nameID.format
1227        assertion.subject.nameID.value = attributeQuery.subject.nameID.value
1228
1229        attributeStatement = AttributeStatement()
1230
1231        # Query the database for the requested attributes and return them
1232        # mapped to their attribute names as specified by the attributeNames
1233        # property
1234        for requestedAttribute in attributeQuery.attributes:
1235            attributeVals = self._queryDbForSamlAttributes(
1236                                                    requestedAttribute.name, 
1237                                                    userId)
1238
1239            # Make a new SAML attribute object to hold the values obtained
1240            attribute = Attribute()
1241            attribute.name = requestedAttribute.name
1242           
1243            # Check name format requested - only XSString is currently
1244            # supported
1245            if (requestedAttribute.nameFormat != 
1246                XSStringAttributeValue.DEFAULT_FORMAT):
1247                raise InvalidAttributeFormat('Requested attribute type %r but '
1248                                     'only %r type is supported' %
1249                                     (requestedAttribute.nameFormat,
1250                                      XSStringAttributeValue.DEFAULT_FORMAT))
1251           
1252            attribute.nameFormat = requestedAttribute.nameFormat
1253
1254            if requestedAttribute.friendlyName is not None:
1255                attribute.friendlyName = requestedAttribute.friendlyName
1256
1257            for val in attributeVals:
1258                attribute.attributeValues.append(XSStringAttributeValue())
1259                attribute.attributeValues[-1].value = val
1260
1261            attributeStatement.attributes.append(attribute)
1262
1263        assertion.attributeStatements.append(attributeStatement)
1264        response.assertions.append(assertion)
1265       
1266    def _queryDbForSamlSubject(self, userId):     
1267        """Check a given SAML subject (user) is registered in the database.
1268        This method is called from the getAttributes() method
1269
1270        @type userId: basestring
1271        @param userId: user identity
1272        @rtype: bool
1273        @return: True/False is user registered?
1274        """
1275        if self.samlSubjectSqlQuery is None:
1276            log.debug('No "self.samlSubjectSqlQuery" property has been set, '
1277                      'skipping SAML subject query step')
1278            return True
1279       
1280        if self.connectionString is None:
1281            raise AttributeInterfaceConfigError('No "connectionString" setting '
1282                                                'has been made')
1283           
1284        dbEngine = create_engine(self.connectionString)
1285       
1286        try:
1287            queryInputs = {
1288                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME: userId
1289            }
1290            query = Template(self.samlSubjectSqlQuery).substitute(queryInputs)
1291           
1292        except KeyError, e:
1293            raise AttributeInterfaceConfigError("Invalid key for SAML subject "
1294                        "query string.  The valid key is %r" % 
1295                        SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME)   
1296
1297        log.debug('Checking for SAML subject with SQL Query = "%s"', query)
1298        try:
1299            connection = dbEngine.connect()
1300            result = connection.execute(query)
1301
1302        except (exc.ProgrammingError, exc.OperationalError):
1303            raise AttributeInterfaceRetrieveError('SQL error: %s' %
1304                                                  traceback.format_exc()) 
1305        finally:
1306            connection.close()
1307
1308        try:
1309            found = [entry for entry in result][0][0] > 0
1310       
1311        except (IndexError, TypeError):
1312            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
1313                                                  traceback.format_exc())
1314       
1315        log.debug('user=%r found=%r' % (userId, found))
1316       
1317        return found
1318     
1319    def _queryDbForSamlAttributes(self, attributeName, userId):     
1320        """Query the database in response to a SAML attribute query
1321       
1322        This method is called from the getAttributes() method
1323
1324        @type userId: basestring
1325        @param userId: user identity
1326        @rtype: bool
1327        @return: True/False is user registered?
1328        """
1329       
1330        if self.connectionString is None:
1331            raise AttributeInterfaceConfigError('No "connectionString" setting '
1332                                                'has been made')
1333
1334        dbEngine = create_engine(self.connectionString)
1335       
1336        queryTmpl = self.samlAttribute2SqlQuery.get(attributeName)
1337        if queryTmpl is None:
1338            raise AttributeInterfaceConfigError('No SQL query set for '
1339                                                'attribute %r' % attributeName)
1340       
1341        try:
1342            queryInputs = {
1343                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME: userId
1344            }
1345            query = Template(queryTmpl).substitute(queryInputs)
1346           
1347        except KeyError, e:
1348            raise AttributeInterfaceConfigError("Invalid key %s for SAML "
1349                        "attribute query string.  The valid key is %r" % 
1350                        (e,
1351                         SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME))
1352           
1353        log.debug('Checking for SAML attributes with SQL Query = "%s"', query)
1354               
1355        try:
1356            connection = dbEngine.connect()
1357            result = connection.execute(query)
1358           
1359        except (exc.ProgrammingError, exc.OperationalError):
1360            raise AttributeInterfaceRetrieveError('SQL error: %s' %
1361                                                  traceback.format_exc())
1362        finally:
1363            connection.close()
1364
1365        try:
1366            attributeValues = [entry[0] for entry in result]
1367           
1368        except (IndexError, TypeError):
1369            raise AttributeInterfaceRetrieveError("Error with result set: "
1370                                                  "%s" % traceback.format_exc())
1371       
1372        log.debug('Database results for SAML Attribute query user=%r '
1373                  'attribute values=%r' % (userId, attributeValues))
1374       
1375        return attributeValues
1376     
1377    def __getstate__(self):
1378        '''Explicit pickling required with __slots__'''
1379        return dict([(attrName, getattr(self, attrName)) 
1380                      for attrName in SQLAlchemyAttributeInterface.__slots__])
1381       
1382    def __setstate__(self, attrDict):
1383        '''Enable pickling for use with beaker.session'''
1384        for attr, val in attrDict.items():
1385            setattr(self, attr, val)           
1386
1387       
1388   
1389       
Note: See TracBrowser for help on using the repository browser.