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

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