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

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

Incomplete - task 16: NDG Security 2.x.x - incl. updated Paster templates

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