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

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

Started pruning trunk of old security code. None of this code is used by the current system but it remains in the code base and is still unit tested:

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