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

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

Refactoring Attribute Authority to remove NDG Attribute Certificate and role mapping code.

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