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

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

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

  • integrating SQLite test user db into 'Site A' test Attribute Authority
  • 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
456        """
457        if not isinstance(attributeQuery, AttributeQuery):
458            raise TypeError('Expecting %r for attribute query; got %r' %
459                            (AttributeQuery, type(attributeQuery)))
460       
461        # Attribute Query validation ...
462        utcNow = datetime.utcnow()
463        if (attributeQuery.subject.nameID.format != 
464            ESGFSamlNamespaces.NAMEID_FORMAT):
465            log.error('SAML Attribute Query subject format is %r; expecting '
466                      '%r' % (attributeQuery.subject.nameID.format,
467                                ESGFSamlNamespaces.NAMEID_FORMAT))
468            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
469            samlResponse.status.statusMessage.value = \
470                                "Subject Name ID format is not recognised"
471            return samlResponse
472       
473        elif attributeQuery.issuer.format != Issuer.X509_SUBJECT:
474            log.error('SAML Attribute Query issuer format is %r; expecting '
475                      '%r' % (attributeQuery.issuer.format,
476                              Issuer.X509_SUBJECT))
477            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
478            samlResponse.status.statusMessage.value = \
479                                            "Issuer format is not recognised"
480            return samlResponse
481       
482        try:
483            # Return a dictionary of name, value pairs
484            self.attributeInterface.getAttributes(attributeQuery, samlResponse)
485           
486        except InvalidUserId, e:
487            log.exception(e)
488            samlResponse.status.statusCode.value = \
489                                        StatusCode.UNKNOWN_PRINCIPAL_URI
490            return samlResponse
491           
492        except UserIdNotKnown, e:
493            log.exception(e)
494            samlResponse.status.statusCode.value = \
495                                        StatusCode.UNKNOWN_PRINCIPAL_URI
496            samlResponse.status.statusMessage.value = str(e)
497            return samlResponse
498           
499        except InvalidRequestorId, e:
500            log.exception(e)
501            samlResponse.status.statusCode.value = StatusCode.REQUEST_DENIED_URI
502            samlResponse.status.statusMessage.value = str(e)
503            return samlResponse
504           
505        except AttributeReleaseDenied, e:
506            log.exception(e)
507            samlResponse.status.statusCode.value = \
508                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
509            samlResponse.status.statusMessage.value = str(e)
510            return samlResponse
511           
512        except AttributeNotKnownError, e:
513            log.exception(e)
514            samlResponse.status.statusCode.value = \
515                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
516            samlResponse.status.statusMessage.value = str(e)
517            return samlResponse
518           
519        except Exception, e:
520            log.exception("Unexpected error calling Attribute Interface "
521                          "for subject [%s] and query issuer [%s]" %
522                          (attributeQuery.subject.nameID.value,
523                           attributeQuery.issuer.value))
524           
525            # SAML spec says application server should set a HTTP 500 Internal
526            # Server error in this case
527            raise 
528
529        return samlResponse
530
531    def samlAttributeQueryFactory(self):
532        """Factory method to create SAML Attribute Query wrapper function
533        @rtype: function
534        @return: samlAttributeQuery method function wrapper
535        """
536        def samlAttributeQueryWrapper(attributeQuery, response):
537            """Attribute Query method.  This must adhere to the function
538            signature specified by
539            ndg.security.server.wsgi.saml.SOAPQueryInterfaceMiddleware
540            @type attributeQuery: ndg.saml.saml2.core.AttributeQuery
541            @param attributeQuery: SAML Attribute Query
542            @rtype: ndg.saml.saml2.core.Response
543            @return: SAML response
544            """
545            return self.samlAttributeQuery(attributeQuery, response)
546       
547        return samlAttributeQueryWrapper
548   
549               
550class AttributeInterfaceError(Exception):
551    """Exception handling for NDG Attribute Authority User Roles interface
552    class."""
553 
554                     
555class AttributeInterfaceConfigError(AttributeInterfaceError):
556    """Invalid configuration set for Attribute interface"""
557 
558                     
559class AttributeInterfaceRetrieveError(AttributeInterfaceError):
560    """Error retrieving attributes for Attribute interface class"""
561
562                       
563class AttributeReleaseDenied(AttributeInterfaceError):
564    """Requestor was denied release of the requested attributes"""
565
566                       
567class AttributeNotKnownError(AttributeInterfaceError):
568    """Requested attribute names are not known to this authority"""
569
570
571class InvalidRequestorId(AttributeInterfaceError):
572    """Requestor is not known or not allowed to request attributes"""
573   
574
575class UserIdNotKnown(AttributeInterfaceError): 
576    """User ID passed to getAttributes is not known to the authority"""
577   
578   
579class InvalidUserId(AttributeInterfaceError):
580    """User Id passed to getAttributes is invalid"""
581   
582   
583class InvalidAttributeFormat(AttributeInterfaceError):
584    """Format for Attribute requested is invalid or not supported"""
585   
586     
587class AttributeInterface(object):
588    """An abstract base class to define the user roles interface to an
589    Attribute Authority.
590
591    Each NDG data centre should implement a derived class which implements
592    the way user roles are provided to its representative Attribute Authority.
593   
594    Roles are expected to indexed by user Distinguished Name (DN).  They
595    could be stored in a database or file."""
596   
597    # Enable derived classes to use slots if desired
598    __slots__ = ()
599   
600    # User defined class may wish to specify a URI for a database interface or
601    # path for a user roles configuration file
602    def __init__(self, **prop):
603        """User Roles base class - derive from this class to define
604        roles interface to Attribute Authority
605       
606        @type prop: dict
607        @param prop: custom properties to pass to this class
608        """
609
610    def getRoles(self, userId):
611        """Virtual method - Derived method should return the roles for the
612        given user's Id or else raise an exception
613       
614        @type userId: string
615        @param userId: user identity e.g. user Distinguished Name
616        @rtype: list
617        @return: list of roles for the given user ID
618        @raise AttributeInterfaceError: an error occured requesting
619        attributes
620        """
621        raise NotImplementedError(self.getRoles.__doc__)
622 
623    def getAttributes(self, attributeQuery, response):
624        """Virtual method should be implemented in a derived class to enable
625        AttributeAuthority.samlAttributeQuery - The derived method should
626        return the attributes requested for the given user's Id or else raise
627        an exception
628       
629        @type attributeQuery: saml.saml2.core.AttributeQuery
630        @param userId: query containing requested attributes
631        @type: saml.saml2.core.Response
632        @param: Response - add an assertion with the list of attributes
633        for the given subject ID in the query or set an error Status code and
634        message
635        @raise AttributeInterfaceError: an error occured requesting
636        attributes
637        @raise AttributeReleaseDeniedError: Requestor was denied release of the
638        requested attributes
639        @raise AttributeNotKnownError: Requested attribute names are not known
640        to this authority
641        """
642        raise NotImplementedError(self.getAttributes.__doc__)
643
644
645class CSVFileAttributeInterface(AttributeInterface):
646    """Attribute Interface based on a Comma Separated Variable file containing
647    user identities and associated attributes.  For test/development purposes
648    only.  The SAML getAttributes method is NOT implemented here
649   
650    The expected file format is:
651   
652    <userID>, <role1>, <role2>, ... <roleN>
653    """
654    def __init__(self, propertiesFilePath=None):
655        """
656        @param propertiesFilePath: file path to Comma Separated file
657        containing user ids and roles
658        @type propertiesFilePath: basestring
659        """
660        if propertiesFilePath is None:
661            raise AttributeError("Expecting propertiesFilePath setting")
662       
663        propertiesFile = open(propertiesFilePath)
664        lines = propertiesFile.readlines()
665       
666        self.attributeMap = {}
667        for line in lines:
668            fields = re.split(',\s*', line.strip())
669            self.attributeMap[fields[0]] = fields[1:]
670   
671    def getRoles(self, userId):
672        """
673        @param userId: user identity to key into attributeMap
674        @type userId: basestring
675        """ 
676        log.debug('CSVFileAttributeInterface.getRoles for user "%s" ...', 
677                  userId)
678        return self.attributeMap.get(userId, [])
679
680
681# Properties file
682from ConfigParser import SafeConfigParser, NoOptionError
683
684try:
685    # PostgreSQL interface
686    from psycopg2 import connect
687except ImportError:
688    pass
689
690class PostgresAttributeInterface(AttributeInterface):
691    """User Roles interface to Postgres database
692   
693    The SAML getAttributes method is NOT implemented
694   
695    The configuration file follows the form,
696   
697    [Connection]
698    # name of database
699    dbName: user.db
700   
701    # database host machine
702    host: mydbhost.ac.uk
703   
704    # database account username
705    username: mydbaccount
706   
707    # Password - comment out to prompt from stdin instead
708    pwd: mydbpassword
709   
710    [getRoles]
711    query0: select distinct grp from users_table, where user = '%%s'
712    defaultRoles = publicRole
713    """
714
715    CONNECTION_SECTION_NAME = "Connection"
716    GETROLES_SECTION_NAME = "getRoles"
717    HOST_OPTION_NAME = "host"
718    DBNAME_OPTION_NAME = "dbName"
719    USERNAME_OPTION_NAME = "username"
720    PWD_OPTION_NAME = "pwd"
721    QUERYN_OPTION_NAME = "query%d"
722    DEFAULT_ROLES_OPTION_NAME = "defaultRoles"
723   
724    def __init__(self, propertiesFilePath=None):
725        """Connect to Postgres database"""
726        self.__con = None
727        self.__host = None
728        self.__dbName = None
729        self.__username = None
730        self.__pwd = None
731
732        if propertiesFilePath is None:
733            raise AttributeError("No Configuration file was set")
734
735        self.readConfigFile(propertiesFilePath)
736
737    def __del__(self):
738        """Close database connection"""
739        self.close()
740
741    def readConfigFile(self, propertiesFilePath):
742        """Read the configuration for the database connection
743
744        @type propertiesFilePath: string
745        @param propertiesFilePath: file path to config file"""
746
747        if not isinstance(propertiesFilePath, basestring):
748            raise TypeError("Input Properties file path must be a valid "
749                            "string; got %r" % type(propertiesFilePath))
750
751        cfg = SafeConfigParser()
752        cfg.read(propertiesFilePath)
753
754        self.__host = cfg.get(
755                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
756                        PostgresAttributeInterface.HOST_OPTION_NAME)
757        self.__dbName = cfg.get(
758                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
759                        PostgresAttributeInterface.DBNAME_OPTION_NAME)
760        self.__username = cfg.get(
761                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
762                        PostgresAttributeInterface.USERNAME_OPTION_NAME)
763        self.__pwd = cfg.get(
764                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
765                        PostgresAttributeInterface.PWD_OPTION_NAME)
766
767        try:
768            self.__getRolesQuery = []
769            for i in range(10):
770                queryStr = cfg.get(
771                        PostgresAttributeInterface.GETROLES_SECTION_NAME, 
772                        PostgresAttributeInterface.QUERYN_OPTION_NAME % i)
773                self.__getRolesQuery += [queryStr]
774        except NoOptionError:
775             # Continue until no more query<n> items left
776             pass
777
778        # This option may be omitted in the config file
779        try:
780            self.__defaultRoles = cfg.get(
781                PostgresAttributeInterface.GETROLES_SECTION_NAME, 
782                PostgresAttributeInterface.DEFAULT_ROLES_OPTION_NAME).split()
783        except NoOptionError:
784            self.__defaultRoles = []
785
786    def connect(self,
787                username=None,
788                dbName=None,
789                host=None,
790                pwd=None,
791                prompt="Database password: "):
792        """Connect to database
793
794        Values for keywords omitted are derived from the config file.  If pwd
795        is not in the config file it will be prompted for from stdin
796
797        @type username: string
798        @keyword username: database account username
799        @type dbName: string
800        @keyword dbName: name of database
801        @type host: string
802        @keyword host: database host machine
803        @type pwd: string
804        @keyword pwd: password for database account.  If omitted and not in
805        the config file it will be prompted for from stdin
806        @type prompt: string
807        @keyword prompt: override default password prompt"""
808
809        if not host:
810            host = self.__host
811
812        if not dbName:
813            dbName = self.__dbName
814
815        if not username:
816            username = self.__username
817
818        if not pwd:
819            pwd = self.__pwd
820
821            if not pwd:
822                import getpass
823                pwd = getpass.getpass(prompt=prompt)
824
825        try:
826            self.__db = connect("host=%s dbname=%s user=%s password=%s" % \
827                                (host, dbName, username, pwd))
828            self.__cursor = self.__db.cursor()
829
830        except NameError, e:
831            raise AttributeInterfaceError("psycopg2 Postgres package not "
832                                          "installed? %s" % e)
833        except Exception, e:
834            raise AttributeInterfaceError("Error connecting to database "
835                                          "\"%s\": %s" % (dbName, e))
836
837    def close(self):
838        """Close database connection"""
839        if self.__con:
840            self.__con.close()
841
842    def getRoles(self, userId):
843        """Return valid roles for the given userId
844
845        @type userId: basestring
846        @param userId: user identity"""
847
848        try:
849            self.connect()
850
851            # Process each query in turn appending role names
852            roles = self.__defaultRoles[:]
853            for query in self.__getRolesQuery:
854                try:
855                    self.__cursor.execute(query % userId)
856                    queryRes = self.__cursor.fetchall()
857
858                except Exception, e:
859                    raise AttributeInterfaceError("Query for %s: %s" %
860                                                  (userId, e))
861
862                roles += [res[0] for res in queryRes if res[0]]
863        finally:
864            self.close()
865
866        return roles
867
868    def __getCursor(self):
869        """Return a database cursor instance"""
870        return self.__cursor
871
872    cursor = property(fget=__getCursor, doc="database cursor")
873
874
875import traceback
876from string import Template
877try:
878    from sqlalchemy import create_engine, exc
879    sqlAlchemyInstalled = True
880except ImportError:
881    sqlAlchemyInstalled = False
882   
883
884class SQLAlchemyAttributeInterface(AttributeInterface):
885    '''SQLAlchemy based Attribute interface enables the Attribute Authority
886    to interface to any database type supported by it
887   
888    @type SQLQUERY_USERID_KEYNAME: basestring
889    @cvar SQLQUERY_USERID_KEYNAME: key corresponding to string to be
890    substituted into attribute query for user identifier e.g.
891   
892    select attr from user_table where username = $userId
893   
894    @type SAML_VALID_REQUESTOR_DNS_PAT: _sre.SRE_Pattern
895    @param SAML_VALID_REQUESTOR_DNS_PAT: regular expression to split list of
896    SAML requestor DNs.  These must comma separated.  Each comma may be
897    separated by any white space including new line characters
898    ''' 
899    DEFAULT_SAML_ASSERTION_LIFETIME = timedelta(seconds=60*60*8) 
900     
901    SQLQUERY_USERID_KEYNAME = 'userId'
902   
903    ISSUER_NAME_FORMAT = Issuer.X509_SUBJECT
904    CONNECTION_STRING_OPTNAME = 'connectionString'
905    ATTRIBUTE_SQLQUERY_OPTNAME = 'attributeSqlQuery'
906    SAML_SUBJECT_SQLQUERY_OPTNAME = 'samlSubjectSqlQuery'
907    SAML_VALID_REQUESTOR_DNS_OPTNAME = 'samlValidRequestorDNs'
908    SAML_ASSERTION_LIFETIME_OPTNAME = 'samlAssertionLifetime'
909    SAML_ATTRIBUTE2SQLQUERY_OPTNAME = 'samlAttribute2SqlQuery'
910    SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN = len(SAML_ATTRIBUTE2SQLQUERY_OPTNAME)
911   
912    SAML_ATTRIBUTE2SQLQUERY_ATTRNAME_DELIMITERS = ('.', '_')
913    SAML_ATTRIBUTE2SQLQUERY_ATTRVAL_PAT = re.compile('\"\W+\"')
914             
915    __slots__ = (
916        CONNECTION_STRING_OPTNAME,
917        ATTRIBUTE_SQLQUERY_OPTNAME,
918        SAML_SUBJECT_SQLQUERY_OPTNAME,
919        SAML_VALID_REQUESTOR_DNS_OPTNAME,
920        SAML_ASSERTION_LIFETIME_OPTNAME,
921        SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
922    )
923    __PRIVATE_ATTR_PREFIX = '_SQLAlchemyAttributeInterface__'
924    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
925    del i
926   
927#    For Reference - split based on space separated ' or " quoted items
928#    SAML_VALID_REQUESTOR_DNS_PAT = re.compile("['\"]?\s*['\"]")
929   
930    SAML_VALID_REQUESTOR_DNS_PAT = re.compile(',\s*')
931   
932    def __init__(self, **properties):
933        '''Instantiate object taking in settings from the input properties'''
934        log.debug('Initialising SQLAlchemyAttributeInterface instance ...')
935       
936        if not sqlAlchemyInstalled:
937            raise AttributeInterfaceConfigError("SQLAlchemy is not installed")
938
939        self.__connectionString = None
940        self.__attributeSqlQuery = None
941        self.__samlSubjectSqlQuery = None
942        self.__samlValidRequestorDNs = []
943        self.__samlAssertionLifetime = \
944            SQLAlchemyAttributeInterface.DEFAULT_SAML_ASSERTION_LIFETIME
945        self.__samlAttribute2SqlQuery = {}
946       
947        self.setProperties(**properties)
948
949    def __setattr__(self, name, value):
950        """Provide a way to set the attribute map by dynamically handling
951        attribute names containing the SAML attribute name as a suffix e.g.
952       
953        attributeInterface.samlAttribute2SqlQuery_firstName = 'Philip'
954       
955        will update __samlAttribute2SqlQuery with the 'firstName', 'Philip'
956        key value pair.  Similarly,
957       
958        setattr('samlAttribute2SqlQuery.emailAddress', 'pjk@somewhere.ac.uk')
959       
960        sets __samlAttribute2SqlQuery with the 'emailAddress',
961        'pjk@somewhere.ac.uk' key value pair
962       
963        This is useful in enabling settings to be made direct from a dict of
964        option name and values parsed from an ini file.
965        """
966        cls = SQLAlchemyAttributeInterface
967       
968        if name in cls.__slots__:
969            object.__setattr__(self, name, value)
970           
971        elif (len(name) > cls.SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN and
972              name[cls.SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN] in 
973              cls.SAML_ATTRIBUTE2SQLQUERY_ATTRNAME_DELIMITERS):
974            # A special 'samlAttribute2SqlQuery[._]+' attribute name has been
975            # found.  The first item is the attribute name and the second, the
976            # corresponding SQL query to get the values corresponding to that
977            # name.  An optional 3rd element is a callback which converts the
978            # retrieved SQL query result to required the attribute value type. 
979            # This defaults to do a conversion to XS:String if not explicitly
980            # set       
981            attr2sqlQueryOpts = \
982                self.__class__.SAML_ATTRIBUTE2SQLQUERY_ATTRVAL_PAT.split(value)
983            if len(attr2sqlQueryOpts) > 2:
984                (samlAttributeName, 
985                 samlAttributeSqlQuery, 
986                 samlAttributeValueParserName) = attr2sqlQueryOpts
987               
988                # Get parser from module path provided
989                samlAttributeParser = importModuleObject(
990                                        samlAttributeValueParserName.strip('"'))
991            else:
992                # No attribute value conversion callback given - default to
993                # XS:String
994                samlAttributeName, samlAttributeSqlQuery = attr2sqlQueryOpts
995                samlAttributeParser = self.xsstringAttributeValueParser
996           
997            # Items may be quoted with " quotes
998            samlAttributeName = samlAttributeName.strip('"')
999           
1000            # Set mapping of attribute name to SQL query + conversion routine
1001            # tuple
1002            self.__samlAttribute2SqlQuery[samlAttributeName
1003                ] = (samlAttributeSqlQuery.strip('"'), samlAttributeParser)
1004        else:
1005            raise AttributeError("'SQLAlchemyAttributeInterface' has no "
1006                                 "attribute %r" % name)
1007
1008    def xsstringAttributeValueParser(self, attrVal):
1009        """Convert string attribute value retrieved from database query into
1010        the respective SAML Attribute Value type
1011        """
1012        return XSStringAttributeValue(attrVal)
1013   
1014    def setProperties(self, prefix='', **properties):
1015        for name, val in properties.items():
1016            if prefix:
1017                if name.startswith(prefix):
1018                    name = name.replace(prefix, '', 1)
1019                    setattr(self, name, val)
1020            else:
1021                setattr(self, name, val)
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        # Assumes SAML response issuer name set independently -
1243        # ndg.security.server.wsgi.saml.SOAPQueryInterfaceMiddleware
1244        assertion.issuer = Issuer()
1245        assertion.issuer.value = response.issuer.value
1246        assertion.issuer.format = Issuer.X509_SUBJECT
1247
1248        assertion.conditions = Conditions()
1249        assertion.conditions.notBefore = assertion.issueInstant
1250        assertion.conditions.notOnOrAfter = (assertion.conditions.notBefore + 
1251                                             self.samlAssertionLifetime)
1252
1253        assertion.subject = Subject()
1254        assertion.subject.nameID = NameID()
1255        assertion.subject.nameID.format = attributeQuery.subject.nameID.format
1256        assertion.subject.nameID.value = attributeQuery.subject.nameID.value
1257
1258        attributeStatement = AttributeStatement()
1259
1260        # Query the database for the requested attributes and return them
1261        # mapped to their attribute names as specified by the attributeNames
1262        # property
1263        for requestedAttribute in attributeQuery.attributes:
1264            attributeVals = self._queryDbForSamlAttributes(
1265                                                    requestedAttribute.name, 
1266                                                    userId)
1267
1268            # Make a new SAML attribute object to hold the values obtained
1269            attribute = Attribute()
1270            attribute.name = requestedAttribute.name
1271           
1272            # Check name format requested - only XSString is currently
1273            # supported
1274            if (requestedAttribute.nameFormat != 
1275                XSStringAttributeValue.DEFAULT_FORMAT):
1276                raise InvalidAttributeFormat('Requested attribute type %r but '
1277                                     'only %r type is supported' %
1278                                     (requestedAttribute.nameFormat,
1279                                      XSStringAttributeValue.DEFAULT_FORMAT))
1280           
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            field2SamlAttributeVal = self.samlAttribute2SqlQuery[
1289                                        requestedAttribute.nameFormat](-1)
1290            for val in attributeVals:
1291                attributeValue = field2SamlAttributeVal(self, val)
1292                attribute.attributeValues.append(attributeValue)
1293#                attribute.attributeValues.append(XSStringAttributeValue())
1294#                attribute.attributeValues[-1].value = val
1295
1296            attributeStatement.attributes.append(attribute)
1297
1298        assertion.attributeStatements.append(attributeStatement)
1299        response.assertions.append(assertion)
1300       
1301    def _queryDbForSamlSubject(self, userId):     
1302        """Check a given SAML subject (user) is registered in the database.
1303        This method is called from the getAttributes() method
1304
1305        @type userId: basestring
1306        @param userId: user identity
1307        @rtype: bool
1308        @return: True/False is user registered?
1309        """
1310        if self.samlSubjectSqlQuery is None:
1311            log.debug('No "self.samlSubjectSqlQuery" property has been set, '
1312                      'skipping SAML subject query step')
1313            return True
1314       
1315        if self.connectionString is None:
1316            raise AttributeInterfaceConfigError('No "connectionString" setting '
1317                                                'has been made')
1318           
1319        dbEngine = create_engine(self.connectionString)
1320       
1321        try:
1322            queryInputs = {
1323                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME: userId
1324            }
1325            query = Template(self.samlSubjectSqlQuery).substitute(queryInputs)
1326           
1327        except KeyError, e:
1328            raise AttributeInterfaceConfigError("Invalid key for SAML subject "
1329                        "query string.  The valid key is %r" % 
1330                        SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME)   
1331
1332        log.debug('Checking for SAML subject with SQL Query = "%s"', query)
1333        try:
1334            connection = dbEngine.connect()
1335            result = connection.execute(query)
1336
1337        except (exc.ProgrammingError, exc.OperationalError):
1338            raise AttributeInterfaceRetrieveError('SQL error: %s' %
1339                                                  traceback.format_exc()) 
1340        finally:
1341            connection.close()
1342
1343        try:
1344            found = [entry for entry in result][0][0] > 0
1345       
1346        except (IndexError, TypeError):
1347            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
1348                                                  traceback.format_exc())
1349       
1350        log.debug('user=%r found=%r' % (userId, found))
1351       
1352        return found
1353     
1354    def _queryDbForSamlAttributes(self, attributeName, userId):     
1355        """Query the database in response to a SAML attribute query
1356       
1357        This method is called from the getAttributes() method
1358
1359        @type userId: basestring
1360        @param userId: user identity
1361        @rtype: bool
1362        @return: True/False is user registered?
1363        """
1364       
1365        if self.connectionString is None:
1366            raise AttributeInterfaceConfigError('No "connectionString" setting '
1367                                                'has been made')
1368
1369        dbEngine = create_engine(self.connectionString)
1370       
1371        queryTmpl = self.samlAttribute2SqlQuery.get(attributeName)[0]
1372        if queryTmpl is None:
1373            raise AttributeInterfaceConfigError('No SQL query set for '
1374                                                'attribute %r' % attributeName)
1375       
1376        try:
1377            queryInputs = {
1378                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME: userId
1379            }
1380            query = Template(queryTmpl).substitute(queryInputs)
1381           
1382        except KeyError, e:
1383            raise AttributeInterfaceConfigError("Invalid key %s for SAML "
1384                        "attribute query string.  The valid key is %r" % 
1385                        (e,
1386                         SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME))
1387           
1388        log.debug('Checking for SAML attributes with SQL Query = "%s"', query)
1389               
1390        try:
1391            connection = dbEngine.connect()
1392            result = connection.execute(query)
1393           
1394        except (exc.ProgrammingError, exc.OperationalError):
1395            raise AttributeInterfaceRetrieveError('SQL error: %s' %
1396                                                  traceback.format_exc())
1397        finally:
1398            connection.close()
1399
1400        try:
1401            attributeValues = [entry[0] for entry in result]
1402           
1403        except (IndexError, TypeError):
1404            raise AttributeInterfaceRetrieveError("Error with result set: "
1405                                                  "%s" % traceback.format_exc())
1406       
1407        log.debug('Database results for SAML Attribute query user=%r '
1408                  'attribute values=%r' % (userId, attributeValues))
1409       
1410        return attributeValues
1411     
1412    def __getstate__(self):
1413        '''Explicit pickling required with __slots__'''
1414        return dict([(attrName, getattr(self, attrName)) 
1415                      for attrName in SQLAlchemyAttributeInterface.__slots__])
1416       
1417    def __setstate__(self, attrDict):
1418        '''Enable pickling for use with beaker.session'''
1419        for attr, val in attrDict.items():
1420            setattr(self, attr, val)           
1421
1422       
1423   
1424       
Note: See TracBrowser for help on using the repository browser.