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

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