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

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

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

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