source: TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/attributeauthority.py @ 6672

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/attributeauthority.py@6672
Revision 6672, 96.9 KB checked in by pjkersha, 10 years ago (diff)

Patched ndg.security.common.AttCert? so that it uses a proxy to ndg.security.common.XMLSec.XMLSecDoc for Python versions >= 2.5.5. This is to allow for PyXML incompatibility with later versions of Python. Disabling XMLSecDoc means that Attribute Certificates are not signed but the NDG Attribute Certificates are no longer used. SAML assertions take their place. NDG AC functionality will be deleted from the trunk.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""NDG Attribute Authority server side code
2
3handles security user attribute (role) allocation
4
5NERC Data Grid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "15/04/05"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = '$Id:attributeauthority.py 4367 2008-10-29 09:27:59Z pjkersha $'
13import logging
14log = logging.getLogger(__name__)
15
16import os
17import re
18import traceback
19
20# For parsing of properties file
21try: # python 2.5
22    from xml.etree import cElementTree as ElementTree
23except ImportError:
24    # if you've installed it yourself it comes this way
25    import cElementTree as ElementTree
26
27# SAML 2.0 Attribute Query Support - added 20/08/2009
28from uuid import uuid4
29from datetime import datetime, timedelta
30
31from saml.utils import SAMLDateTime
32from saml.saml2.core import (Response, Assertion, Attribute, AttributeStatement,
33                             SAMLVersion, Subject, NameID, Issuer, Conditions,
34                             AttributeQuery, 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# X.509 Certificate handling
44from ndg.security.common.X509 import X509Cert, X500DN
45
46# NDG Attribute Certificate
47from ndg.security.common.AttCert import AttCert
48
49
50class AttributeAuthorityError(Exception):
51    """Exception handling for NDG Attribute Authority class."""
52    def __init__(self, msg):
53        log.error(msg)
54        Exception.__init__(self, msg)
55
56
57class AttributeAuthorityConfigError(Exception):
58    """NDG Attribute Authority error with configuration. e.g. properties file
59    directory permissions or role mapping file"""
60    def __init__(self, msg):
61        log.error(msg)
62        Exception.__init__(self, msg)
63       
64       
65class AttributeAuthorityAccessDenied(AttributeAuthorityError):
66    """NDG Attribute Authority - access denied exception.
67
68    Raise from getAttCert method where no roles are available for the user
69    but that the request is otherwise valid.  In all other error cases raise
70    AttributeAuthorityError"""   
71   
72   
73class AttributeAuthorityNoTrustedHosts(AttributeAuthorityError):
74    """Raise from getTrustedHosts if there are no trusted hosts defined in
75    the map configuration"""
76
77
78class AttributeAuthorityNoMatchingRoleInTrustedHosts(AttributeAuthorityError):
79    """Raise from getTrustedHosts if there is no mapping to any of the
80    trusted hosts for the given input role name"""
81
82
83class AttributeAuthority(object):
84    """NDG Attribute Authority - service for allocation of user authorization
85    tokens - attribute certificates.
86   
87    @type propertyDefaults: dict
88    @cvar propertyDefaults: valid configuration property keywords
89   
90    @type attributeInterfacePropertyDefaults: dict
91    @cvar attributeInterfacePropertyDefaults: valid configuration property
92    keywords for the Attribute Interface plugin
93   
94    @type mapConfigHostDefaults: dict
95    @cvar mapConfigHostDefaults: valid configuration property
96    keywords for the Map Configuration XML Host element
97   
98    @type DEFAULT_CONFIG_DIRNAME: string
99    @cvar DEFAULT_CONFIG_DIRNAME: configuration directory under $NDGSEC_DIR -
100    default location for properties file
101   
102    @type DEFAULT_PROPERTY_FILENAME: string
103    @cvar DEFAULT_PROPERTY_FILENAME: default file name for properties file
104    under DEFAULT_CONFIG_DIRNAME
105   
106    @type ATTRIBUTE_INTERFACE_KEYNAME: basestring
107    @param ATTRIBUTE_INTERFACE_KEYNAME: attribute interface parameters key
108    name - see initAttributeInterface for details
109    """
110
111    # Code designed from NERC Data Grid Enterprise and Information Viewpoint
112    # documents.
113    #
114    # Also, draws from Neil Bennett's ACServer class used in the Java
115    # implementation of NDG Security
116
117    DEFAULT_CONFIG_DIRNAME = "conf"
118    DEFAULT_PROPERTY_FILENAME = "attributeAuthority.cfg"
119    ATTRIBUTE_INTERFACE_KEYNAME = 'attributeInterface'
120    CONFIG_LIST_SEP_PAT = re.compile(',\s*')
121   
122    attributeInterfacePropertyDefaults = {
123        'modFilePath':  '',
124        'modName':      '',
125        'className':    ''
126    }
127   
128    # valid configuration property keywords with accepted default values. 
129    # Values set to not NotImplemented here denote keys which must be specified
130    # in the config
131    propertyDefaults = { 
132        'name':                         '',
133        'signingCertFilePath':          '',
134        'signingPriKeyFilePath':        '',
135        'signingPriKeyPwd':             None,
136        'caCertFilePathList':           [],
137        'attCertLifetime':              -1,
138        'attCertNotBeforeOff':          0.,
139        'clockSkew':                    timedelta(seconds=0.),
140        'attCertFileName':              '',
141        'attCertFileLogCnt':            0,
142        'mapConfigFilePath':            '',
143        'attCertDir':                   '',
144        'dnSeparator':                  '/',
145        ATTRIBUTE_INTERFACE_KEYNAME:    attributeInterfacePropertyDefaults
146    }
147   
148    mapConfigHostDefaults = {
149        'siteName':                 None,
150        'aaURI':                    NotImplemented,
151        'aaDN':                     NotImplemented,
152        'loginURI':                 NotImplemented,
153        'loginServerDN':            NotImplemented,
154        'loginRequestServerDN':     NotImplemented
155    }
156
157    def __init__(self):
158        """Create new Attribute Authority instance"""
159        log.info("Initialising service ...")
160       
161        # Initial config file property based attributes
162        for name, val in AttributeAuthority.propertyDefaults.items():
163            setattr(self, '_AttributeAuthority__%s' % name, val)
164       
165        self.__caCertFilePathList = TypedList(basestring)
166       
167        self.__propFilePath = None       
168        self.__propFileSection = 'DEFAULT'
169        self.__propPrefix = ''
170       
171        # Initialise role mapping look-ups - These are set in readMapConfig()
172        self.__mapConfig = None
173        self.__localRole2RemoteRole = None
174        self.__remoteRole2LocalRole = None
175       
176        self.__cert = None
177       
178        # Issuer details - serialise using the separator string set in the
179        # properties file
180        self.__issuer = None
181        self.__issuerSerialNumber = None
182        self.__attCertLog = None
183        self.__name = None
184       
185        self.__attributeInterfaceCfg = {}
186
187    def _getMapConfig(self):
188        return self.__mapConfig
189
190    def _getCert(self):
191        return self.__cert
192
193    def _getIssuer(self):
194        return self.__issuer
195
196    def _getIssuerSerialNumber(self):
197        return self.__issuerSerialNumber
198
199    def _getAttCertLog(self):
200        return self.__attCertLog
201
202    def _getName(self):
203        return self.__name
204
205    def _getAttCertLifetime(self):
206        return self.__attCertLifetime
207
208    def _getAttCertNotBeforeOff(self):
209        return self.__attCertNotBeforeOff
210
211    def _getClockSkew(self):
212        return self.__clockSkew
213
214    def _getAttCertDir(self):
215        return self.__attCertDir
216
217    def _getAttributeInterface(self):
218        return self.__attributeInterface
219
220    def _getTrustedHostInfo(self):
221        return self.__trustedHostInfo
222
223    def _setCert(self, value):
224        if not isinstance(value, X509Cert):
225            raise TypeError('Expecting %r type for "cert"; got %r' %
226                            (X509Cert, type(value)))
227           
228        self.__cert = value
229
230    def _setIssuer(self, value):
231        self.__issuer = value
232
233    def _setIssuerSerialNumber(self, value):
234        if not isinstance(value, (long, int)):
235            raise TypeError('Expecting long or int type for "name"; got %r' %
236                            type(value))
237        self.__issuerSerialNumber = value
238
239    def _setAttCertLog(self, value):
240        if not isinstance(value, AttCertLog):
241            raise TypeError('Expecting %r type for "attCertLog"; got %r' %
242                            (AttCertLog, type(value)))
243        self.__attCertLog = value
244
245    def _setName(self, value):
246        if not isinstance(value, basestring):
247            raise TypeError('Expecting string type for "name"; got %r' %
248                            type(value))
249        self.__name = value
250
251    def _setAttCertLifetime(self, value):
252        if isinstance(value, float):
253            self.__attCertLifetime = value
254           
255        elif isinstance(value, (basestring, int, long)):
256            self.__attCertLifetime = float(value)
257        else:
258            raise TypeError('Expecting float, int, long or string type for '
259                            '"attCertLifetime"; got %r' % type(value))
260
261    def _setAttCertNotBeforeOff(self, value):
262        if isinstance(value, float):
263            self.__attCertNotBeforeOff = value
264           
265        elif isinstance(value, (basestring, int, long)):
266            self.__attCertNotBeforeOff = float(value)
267        else:
268            raise TypeError('Expecting float, int, long or string type for '
269                            '"attCertNotBeforeOff"; got %r' % type(value))
270
271    def _setClockSkew(self, value):
272        if isinstance(value, (float, int, long)):
273            self.__clockSkew = timedelta(seconds=value)
274           
275        elif isinstance(value, basestring):
276            self.__clockSkew = timedelta(seconds=float(value))
277        else:
278            raise TypeError('Expecting float, int, long or string type for '
279                            '"clockSkew"; got %r' % type(value))
280
281    def _setAttCertDir(self, value):
282        if not isinstance(value, basestring):
283            raise TypeError('Expecting string type for "attCertDir"; got %r' % 
284                            type(value))
285
286        # Check directory path
287        try:
288            dirList = os.listdir(value)
289
290        except OSError, osError:
291            raise AttributeAuthorityConfigError('Invalid directory path for '
292                                                'Attribute Certificates store '
293                                                '"%s": %s' % 
294                                                (value, osError.strerror))
295        self.__attCertDir = value
296
297    def _setAttributeInterface(self, value):
298        if not isinstance(value, AttributeInterface):
299            raise TypeError('Expecting %r type for "attributeInterface" '
300                            'attribute; got %r' %
301                            (AttributeInterface, type(value)))
302           
303        self.__attributeInterface = value
304
305    def _setTrustedHostInfo(self, value):
306        self.__trustedHostInfo = value
307
308    def _get_caCertFilePathList(self):
309        return self.__caCertFilePathList
310
311    def _set_caCertFilePathList(self, val):
312        if not isinstance(val, (list, tuple)):
313            raise TypeError('Expecting list or tuple type for '
314                            '"caCertFilePathList"; got %r' % type(val))
315           
316        # Overwrite any original settings
317        self.__caCertFilePathList = TypedList(basestring)
318       
319        # Update with new items
320        self.__caCertFilePathList += val
321   
322    caCertFilePathList = property(fget=_get_caCertFilePathList,
323                                  fset=_set_caCertFilePathList,
324                                  doc="list of file paths for CA certificates "
325                                      "used to validate an Attribute "
326                                      "Certificate")
327   
328    def _get_signingCertFilePath(self):
329        return self.__signingCertFilePath
330   
331    def _set_signingCertFilePath(self, value):
332        if not isinstance(value, basestring):
333            raise TypeError('Expecting string type for "signingCertFilePath"; '
334                            'got %r' % type(value))
335        self.__signingCertFilePath = value
336         
337    signingCertFilePath = property(fget=_get_signingCertFilePath, 
338                                   fset=_set_signingCertFilePath,
339                                   doc="X.509 certificate used for Attribute "
340                                       "certificate signature")
341   
342    def _get_signingPriKeyFilePath(self):
343        return self.__signingPriKeyFilePath
344   
345    def _set_signingPriKeyFilePath(self, value):
346        if not isinstance(value, basestring):
347            raise TypeError('Expecting string type for '
348                            '"signingPriKeyFilePath"; got %r' % type(value))
349        self.__signingPriKeyFilePath = value
350         
351    signingPriKeyFilePath = property(fget=_get_signingPriKeyFilePath, 
352                                     fset=_set_signingPriKeyFilePath,
353                                     doc="File Path for private key used to "
354                                         "sign Attribute certificate")
355   
356    def _get_signingPriKeyPwd(self):
357        return self.__signingPriKeyPwd
358   
359    def _set_signingPriKeyPwd(self, value):
360        if not isinstance(value, (type(None), basestring)):
361            raise TypeError('Expecting string or None type for '
362                            '"signingPriKeyPwd"; got %r' % type(value))
363        self.__signingPriKeyPwd = value
364         
365    signingPriKeyPwd = property(fget=_get_signingPriKeyPwd, 
366                                fset=_set_signingPriKeyPwd,
367                                doc="Password for private key file used to "
368                                    "for Attribute certificate signature")
369
370    def _get_attributeInterfaceCfg(self):
371        return self.__attributeInterfaceCfg
372   
373    attributeInterfaceCfg = property(fget=_get_attributeInterfaceCfg,
374                                     doc="Settings for Attribute Interface "
375                                         "initialisation")
376   
377    def _get_attCertFileName(self):
378        return self.__attCertFileName
379   
380    def _set_attCertFileName(self, value):
381        if not isinstance(value, basestring):
382            raise TypeError('Expecting string type for "attCertFileName"; got '
383                            '%r' % type(value))
384           
385        self.__attCertFileName = value
386         
387    attCertFileName = property(fget=_get_attCertFileName, 
388                                fset=_set_attCertFileName,
389                                doc="Attribute certificate file name for log "
390                                    "initialisation")
391   
392    def _get_attCertFileLogCnt(self):
393        return self.__attCertFileLogCnt
394   
395    def _set_attCertFileLogCnt(self, value):
396        if isinstance(value, int):
397            self.__attCertFileLogCnt = value
398        elif isinstance(value, basestring):
399            self.__attCertFileLogCnt = int(value)
400        else:
401            raise TypeError('Expecting int or string type for '
402                            '"attCertFileLogCnt"; got %r' % type(value))
403         
404    attCertFileLogCnt = property(fget=_get_attCertFileLogCnt, 
405                                 fset=_set_attCertFileLogCnt,
406                                 doc="Counter for Attribute Certificate log "
407                                     "rotating file handler")
408   
409    def _get_dnSeparator(self):
410        return self.__dnSeparator
411   
412    def _set_dnSeparator(self, value):
413        if not isinstance(value, basestring):
414            raise TypeError('Expecting string type for "dnSeparator"; got '
415                            '%r' % type(value))
416        self.__dnSeparator = value
417         
418    dnSeparator = property(fget=_get_dnSeparator, 
419                           fset=_set_dnSeparator,
420                           doc="Distinguished Name separator character used "
421                               "with X.509 Certificate issuer certificate")
422           
423    def _getMapConfigFilePath(self):
424        return self.__mapConfigFilePath
425   
426    def _setMapConfigFilePath(self, val):
427        if not isinstance(val, basestring):
428            raise AttributeAuthorityConfigError("Input Map Configuration "
429                                                "file path must be a "
430                                                "valid string.")
431        self.__mapConfigFilePath = val
432         
433    mapConfigFilePath = property(fget=_getMapConfigFilePath,
434                                 fset=_setMapConfigFilePath,
435                                 doc="File path for Role Mapping "
436                                     "configuration") 
437
438    def setPropFilePath(self, val=None):
439        """Set properties file from input or based on environment variable
440        settings
441       
442        @type val: basestring
443        @param val: properties file path"""
444        log.debug("Setting property file path")
445        if not val:
446            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
447                val = os.environ['NDGSEC_AA_PROPFILEPATH']
448               
449            elif 'NDGSEC_DIR' in os.environ:
450                val = os.path.join(os.environ['NDGSEC_DIR'], 
451                                   AttributeAuthority.DEFAULT_CONFIG_DIRNAME,
452                                   AttributeAuthority.DEFAULT_PROPERTY_FILENAME)
453            else:
454                raise AttributeError('Unable to set default Attribute '
455                                     'Authority properties file path: neither '
456                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
457                                     ' environment variables are set')
458               
459        if not isinstance(val, basestring):
460            raise AttributeError("Input Properties file path "
461                                 "must be a valid string.")
462     
463        self.__propFilePath = os.path.expandvars(val)
464        log.debug("Path set to: %s" % val)
465       
466    def getPropFilePath(self):
467        '''Get the properties file path
468       
469        @rtype: basestring
470        @return: properties file path'''
471        return self.__propFilePath
472       
473    # Also set up as a property
474    propFilePath = property(fset=setPropFilePath,
475                            fget=getPropFilePath,
476                            doc="path to file containing Attribute Authority "
477                                "configuration parameters.  It defaults to "
478                                "$NDGSEC_AA_PROPFILEPATH or if not set, "
479                                "$NDGSEC_DIR/conf/attributeAuthority.cfg")   
480   
481    def setPropFileSection(self, val=None):
482        """Set section name to read properties from ini file.  This is set from
483        input or based on environment variable setting
484        NDGSEC_AA_PROPFILESECTION
485       
486        @type val: basestring
487        @param val: section name"""
488        if not val:
489            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
490               
491        if not isinstance(val, basestring):
492            raise AttributeError("Input Properties file section name "
493                                 "must be a valid string.")
494     
495        self.__propFileSection = val
496        log.debug("Properties file section set to: \"%s\"" % val)
497       
498    def getPropFileSection(self):
499        '''Get the section name to extract properties from an ini file -
500        DOES NOT apply to XML file properties
501       
502        @rtype: basestring
503        @return: section name'''
504        return self.__propFileSection
505       
506    # Also set up as a property
507    propFileSection = property(fset=setPropFileSection,
508                               fget=getPropFileSection,
509                               doc="Set the file section name for ini file "
510                                   "properties")   
511   
512    def setPropPrefix(self, val=None):
513        """Set prefix for properties read from ini file.  This is set from
514        input or based on environment variable setting
515        NDGSEC_AA_PROPFILEPREFIX
516       
517        DOES NOT apply to XML file properties
518       
519        @type val: basestring
520        @param val: section name"""
521        log.debug("Setting property file section name")
522        if val is None:
523            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
524               
525        if not isinstance(val, basestring):
526            raise AttributeError("Input Properties file section name "
527                                 "must be a valid string.")
528     
529        self.__propPrefix = val
530        log.debug("Properties file section set to: %s" % val)
531       
532    def getPropPrefix(self):
533        '''Get the prefix name used for properties in an ini file -
534        DOES NOT apply to XML file properties
535       
536        @rtype: basestring
537        @return: section name'''
538        return self.__propPrefix
539   
540       
541    # Also set up as a property
542    propPrefix = property(fset=setPropPrefix,
543                          fget=getPropPrefix,
544                          doc="Set a prefix for ini file properties")   
545   
546    mapConfig = property(fget=_getMapConfig, 
547                         doc="MapConfig object")
548
549    cert = property(fget=_getCert, 
550                    fset=_setCert, 
551                    doc="X.509 Issuer Certificate")
552
553    issuer = property(fget=_getIssuer, 
554                      fset=_setIssuer, 
555                      doc="Issuer name")
556
557    issuerSerialNumber = property(fget=_getIssuerSerialNumber, 
558                                  fset=_setIssuerSerialNumber, 
559                                  doc="Issuer Serial Number")
560
561    attCertLog = property(fget=_getAttCertLog,
562                          fset=_setAttCertLog, 
563                          doc="Attribute certificate logging object")
564
565    name = property(fget=_getName, 
566                    fset=_setName, 
567                    doc="Issuer organisation name")
568
569    attCertLifetime = property(fget=_getAttCertLifetime, 
570                               fset=_setAttCertLifetime, 
571                               doc="Attribute certificate lifetime")
572
573    attCertNotBeforeOff = property(fget=_getAttCertNotBeforeOff, 
574                                   fset=_setAttCertNotBeforeOff, 
575                                   doc="Attribute certificate clock skew in "
576                                       "seconds")
577
578    clockSkew = property(fget=_getClockSkew, 
579                         fset=_setClockSkew, 
580                         doc="Allow a clock skew in seconds for SAML Attribute"
581                             " Query issueInstant parameter check")
582
583    attCertDir = property(fget=_getAttCertDir, 
584                          fset=_setAttCertDir, 
585                          doc="Attribute certificate log directory")
586
587    attributeInterface = property(fget=_getAttributeInterface, 
588                                  fset=_setAttributeInterface,
589                                  doc="Attribute Interface object")
590
591    name = property(fget=_getName, fset=_setName, doc="Organisation Name")
592
593    trustedHostInfo = property(fget=_getTrustedHostInfo, 
594                               fset=_setTrustedHostInfo, 
595                               doc="Dictionary of trusted organisations")
596       
597    @classmethod
598    def fromPropertyFile(cls, propFilePath=None, propFileSection='DEFAULT',
599                         propPrefix='attributeauthority.', 
600                         bReadMapConfig=True):
601        """Create new NDG Attribute Authority instance from the property file
602        settings
603
604        @type propFilePath: string
605        @param propFilePath: path to file containing Attribute Authority
606        configuration parameters.  It defaults to $NDGSEC_AA_PROPFILEPATH or
607        if not set, $NDGSEC_DIR/conf/attributeAuthority.cfg
608        @type propFileSection: basestring
609        @param propFileSection: section of properties file to read from.
610        properties files
611        @type propPrefix: basestring
612        @param propPrefix: set a prefix for filtering attribute authority
613        property names - useful where properties are being parsed from a file
614        section containing parameter names for more than one application
615        @type bReadMapConfig: boolean
616        @param bReadMapConfig: by default the Map Configuration file is
617        read.  Set this flag to False to override.
618        """
619           
620        attributeAuthority = AttributeAuthority()
621        if propFileSection:
622            attributeAuthority.propFileSection = propFileSection
623           
624        if propPrefix:
625            attributeAuthority.propPrefix = propPrefix
626
627        attributeAuthority.propFilePath = propFilePath           
628        attributeAuthority.readProperties()
629        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
630   
631        return attributeAuthority
632
633       
634    @classmethod
635    def fromProperties(cls, propPrefix='attributeauthority.', 
636                       bReadMapConfig=True, **prop):
637        """Create new NDG Attribute Authority instance from input property
638        keywords
639
640        @type propPrefix: basestring
641        @param propPrefix: set a prefix for filtering attribute authority
642        property names - useful where properties are being parsed from a file
643        section containing parameter names for more than one application
644        @type bReadMapConfig: boolean
645        @param bReadMapConfig: by default the Map Configuration file is
646        read.  Set this flag to False to override.
647        """
648        attributeAuthority = AttributeAuthority()
649        if propPrefix:
650            attributeAuthority.propPrefix = propPrefix
651               
652        attributeAuthority.setProperties(**prop)
653        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
654       
655        return attributeAuthority
656   
657    def initialise(self, bReadMapConfig=True):
658        """Convenience method for set up of Attribute Interface, map
659        configuration and PKI"""
660       
661        # Read the Map Configuration file
662        if bReadMapConfig:
663            self.readMapConfig()
664
665        # Instantiate Certificate object
666        log.debug("Reading and checking Attribute Authority X.509 cert. ...")
667        self.cert = X509Cert.Read(self.signingCertFilePath)
668
669        # Check it's valid
670        try:
671            self.cert.isValidTime(raiseExcep=True)
672           
673        except Exception, e:
674            raise AttributeAuthorityError("Attribute Authority's certificate "
675                                          "is invalid: %s" % e)
676       
677        # Check CA certificate
678        log.debug("Reading and checking X.509 CA certificate ...")
679        for caCertFile in self.caCertFilePathList:
680            caCert = X509Cert(caCertFile)
681            caCert.read()
682           
683            try:
684                caCert.isValidTime(raiseExcep=True)
685               
686            except Exception, e:
687                raise AttributeAuthorityError('CA certificate "%s" is '
688                                              'invalid: %s'% (caCert.dn, e))
689       
690        # Issuer details - serialise using the separator string set in the
691        # properties file
692        self.issuer = self.cert.dn.serialise(separator=self.dnSeparator)
693
694        self.issuerSerialNumber = self.cert.serialNumber
695       
696        # Load user - user attribute look-up plugin
697        self.initAttributeInterface()
698       
699        attCertFilePath = os.path.join(self.attCertDir, self.attCertFileName)
700               
701        # Rotating file handler used for logging attribute certificates
702        # issued.
703        self.attCertLog = AttCertLog(attCertFilePath,
704                                     backUpCnt=self.attCertFileLogCnt)
705
706    def setProperties(self, **prop):
707        """Set configuration from an input property dictionary
708        @type prop: dict
709        @param prop: properties dictionary containing configuration items
710        to be set
711        """
712        lenPropPrefix = len(self.propPrefix)
713       
714        # '+ 1' allows for the dot separator
715        lenAttributeInterfacePrefix = len(
716                            AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME) + 1
717       
718        for name, val in prop.items():
719            if name.startswith(self.propPrefix):
720                name = name[lenPropPrefix:]
721           
722            if name.startswith(AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME):
723                name = name[lenAttributeInterfacePrefix:]
724                self.attributeInterfaceCfg[name] = val
725                continue
726           
727            if name not in AttributeAuthority.propertyDefaults:
728                raise AttributeError('Invalid attribute name "%s"' % name)
729           
730            if isinstance(val, basestring):
731                val = os.path.expandvars(val)
732           
733            if isinstance(AttributeAuthority.propertyDefaults[name], list):
734                val = AttributeAuthority.CONFIG_LIST_SEP_PAT.split(val)
735               
736            # This makes an implicit call to the appropriate property method
737            try:
738                setattr(self, name, val)
739            except AttributeError:
740                raise AttributeError("Can't set attribute \"%s\"" % name)         
741           
742    def readProperties(self):
743        '''Read the properties files and do some checking/converting of input
744        values
745        '''
746        if not os.path.isfile(self.propFilePath):
747            raise IOError('Error parsing properties file "%s": No such file' % 
748                          self.propFilePath)
749           
750        defaultItems = {'here': os.path.dirname(self.propFilePath)}
751       
752        cfg = CaseSensitiveConfigParser(defaults=defaultItems)
753        cfg.read(self.propFilePath)
754       
755        cfgItems = dict([(name, val) 
756                         for name, val in cfg.items(self.propFileSection)
757                         if name != 'here'])
758        self.setProperties(**cfgItems)
759
760    def initAttributeInterface(self):
761        '''Load host sites custom user roles interface to enable the AA to
762        # assign roles in an attribute certificate on a getAttCert request'''
763        classProperties = {}
764        classProperties.update(self.attributeInterfaceCfg)
765       
766        modName = classProperties.pop('modName')
767        className = classProperties.pop('className') 
768       
769        # file path may be omitted   
770        modFilePath = classProperties.pop('modFilePath', None) 
771                     
772        self.__attributeInterface = instantiateClass(modName,
773                                             className,
774                                             moduleFilePath=modFilePath,
775                                             objectType=AttributeInterface,
776                                             classProperties=classProperties)
777
778    def getAttCert(self,
779                   userId=None,
780                   holderX509Cert=None,
781                   holderX509CertFilePath=None,
782                   userAttCert=None,
783                   userAttCertFilePath=None):
784
785        """Request a new Attribute Certificate for use in authorisation
786
787        getAttCert([userId=uid][holderX509Cert=x509Cert|
788                    holderX509CertFilePath=x509CertFile, ]
789                   [userAttCert=cert|userAttCertFilePath=certFile])
790         
791        @type userId: string
792        @param userId: identifier for the user who is entitled to the roles
793        in the certificate that is issued.  If this keyword is omitted, then
794        the userId will be set to the DN of the holder.
795       
796        holder = the holder of the certificate - an inidividual user or an
797        organisation to which the user belongs who vouches for that user's ID
798       
799        userId = the identifier for the user who is entitled to the roles
800        specified in the Attribute Certificate that is issued.
801                 
802        @type holderX509Cert: string / ndg.security.common.X509.X509Cert type
803        @param holderX509Cert: base64 encoded string containing proxy cert./
804        X.509 cert object corresponding to the ID who will be the HOLDER of
805        the Attribute Certificate that will be issued.  - Normally, using
806        proxy certificates, the holder and user ID are the same but there
807        may be cases where the holder will be an organisation ID.  This is the
808        case for NDG security with the DEWS project
809       
810        @param holderX509CertFilePath: string
811        @param holderX509CertFilePath: file path to proxy/X.509 certificate of
812        candidate holder
813     
814        @type userAttCert: string or AttCert type
815        @param userAttCert: externally provided attribute certificate from
816        another data centre.  This is only necessary if the user is not
817        registered with this attribute authority.
818                       
819        @type userAttCertFilePath: string
820        @param userAttCertFilePath: alternative to userAttCert except pass
821        in as a file path to an attribute certificate instead.
822       
823        @rtype: AttCert
824        @return: new attribute certificate"""
825
826        log.debug("Calling getAttCert ...")
827       
828        # Read candidate Attribute Certificate holder's X.509 certificate
829        try:
830            if holderX509CertFilePath is not None:
831                                   
832                # Certificate input as a file
833                holderX509Cert = X509Cert()
834                holderX509Cert.read(holderX509CertFilePath)
835               
836            elif isinstance(holderX509Cert, basestring):
837
838                # Certificate input as string text
839                holderX509Cert = X509Cert.Parse(holderX509Cert)
840               
841            elif not isinstance(holderX509Cert, (X509Cert, None.__class__)):
842                raise AttributeAuthorityError("Holder X.509 Certificate must "
843                                              "be set to valid type: a file "
844                                              "path, string, X509 object or "
845                                              "None")           
846        except Exception, e:
847            log.error("Holder X.509 certificate: %s" % e)
848            raise
849
850
851        # Check certificate hasn't expired
852        if holderX509Cert:
853            log.debug("Checking candidate holder X.509 certificate ...")
854            try:
855                holderX509Cert.isValidTime(raiseExcep=True)
856               
857            except Exception, e:
858                log.error("User X.509 certificate is invalid: " + e)
859                raise
860
861           
862        # If no user ID is input, set id from holder X.509 certificate DN
863        # instead
864        if not userId:
865            if not holderX509Cert:
866                raise AttributeAuthorityError("If no user ID is set a holder "
867                                              "X.509 certificate must be "
868                                              "present")
869            try:
870                userId = holderX509Cert.dn.serialise(\
871                                         separator=self.dnSeparator) 
872            except Exception, e:
873                log.error("Setting user Id from holder certificate DN: %s" % e)
874                raise
875       
876        # Make a new Attribute Certificate instance passing in certificate
877        # details for later signing
878        attCert = AttCert()
879
880        # First certificate in list contains the public key corresponding to
881        # the private key
882        attCert.certFilePathList = [self.signingCertFilePath] + \
883                                                                self.caCertFilePathList
884             
885        # Check for expiry of each certificate                   
886        for x509Cert in attCert.certFilePathList:
887            X509Cert.Read(x509Cert).isValidTime(raiseExcep=True)
888                                                               
889        attCert.signingKeyFilePath = self.signingPriKeyFilePath
890        attCert.signingKeyPwd = self.signingPriKeyPwd
891       
892       
893        # Set holder's Distinguished Name if a holder X.509 certificate was
894        # input
895        if holderX509Cert:
896            try:
897                attCert['holder'] = holderX509Cert.dn.serialise(
898                                        separator=self.dnSeparator)           
899            except Exception, e:
900                log.error("Holder X.509 Certificate DN: %s" % e)
901                raise
902        else:
903            log.debug("No holder X.509 Certificate set, setting Attribute "
904                      "Certificate holder to userId=%r", userId)
905            attCert['holder'] = userId
906           
907        # Set Issuer details from Attribute Authority
908        issuerDN = self.cert.dn
909        try:
910            attCert['issuer'] = \
911                    issuerDN.serialise(separator=self.dnSeparator)           
912        except Exception, e:
913            log.error("Issuer X.509 Certificate DN: %s" % e)
914            raise 
915           
916        attCert['issuerName'] = self.name
917        attCert['issuerSerialNumber'] = self.issuerSerialNumber
918
919        attCert['userId'] = userId
920       
921        # Set validity time
922        try:
923            attCert.setValidityTime(
924                        lifetime=self.attCertLifetime,
925                        notBeforeOffset=self.attCertNotBeforeOff)
926
927            # Check against the holder X.509 certificate's expiry if set
928            if holderX509Cert:
929                dtHolderCertNotAfter = holderX509Cert.notAfter
930               
931                if attCert.getValidityNotAfter(asDatetime=True) > \
932                   dtHolderCertNotAfter:
933   
934                    # Adjust the attribute certificate's expiry date time
935                    # so that it agrees with that of the certificate
936                    # ... but also make ensure that the not before skew is
937                    # still applied
938                    attCert.setValidityTime(dtNotAfter=dtHolderCertNotAfter,
939                            notBeforeOffset=self.attCertNotBeforeOff)
940           
941        except Exception, e:
942            log.error("Error setting attribute certificate validity time: %s" %
943                      e)
944            raise 
945
946        # Check name is registered with this Attribute Authority - if no
947        # user roles are found, the user is not registered
948        userRoles = self.getRoles(userId)
949        if userRoles:
950            # Set as an Original Certificate
951            #
952            # User roles found - user is registered with this data centre
953            # Add roles for this user for this data centre
954            attCert.addRoles(userRoles)
955
956            # Mark new Attribute Certificate as an original
957            attCert['provenance'] = AttCert.origProvenance
958
959        else:           
960            # Set as a Mapped Certificate
961            #
962            # No roles found - user is not registered with this data centre
963            # Check for an externally provided certificate from another
964            # trusted data centre
965            if userAttCertFilePath:
966               
967                # Read externally provided certificate
968                try:
969                    userAttCert = AttCert.Read(userAttCertFilePath)
970                   
971                except Exception, e:
972                    raise AttributeAuthorityError("Reading external Attribute "
973                                                  "Certificate: %s" % e)                           
974            elif userAttCert:
975                # Allow input as a string but convert to
976                if isinstance(userAttCert, basestring):
977                    userAttCert = AttCert.Parse(userAttCert)
978                   
979                elif not isinstance(userAttCert, AttCert):
980                    raise AttributeAuthorityError(
981                        "Expecting userAttCert as a string or AttCert type")       
982            else:
983                raise AttributeAuthorityAccessDenied('User "%s" is not '
984                    'registered and no external attribute certificate is '
985                    'available to make a mapping.' % userId)
986
987
988            # Check it's an original certificate - mapped certificates can't
989            # be used to make further mappings
990            if userAttCert.isMapped():
991                raise AttributeAuthorityError("External Attribute Certificate "
992                                              "must have an original "
993                                              "provenance in order "
994                                              "to make further mappings.")
995
996
997            # Check it's valid and signed
998            try:
999                # Give path to CA cert to allow check
1000                userAttCert.certFilePathList = self.caCertFilePathList
1001                userAttCert.isValid(raiseExcep=True)
1002               
1003            except Exception, e:
1004                raise AttributeAuthorityError("Invalid Remote Attribute "
1005                                        "Certificate: " + str(e))       
1006
1007
1008            # Check that's it's holder matches the candidate holder
1009            # certificate DN
1010            if holderX509Cert and userAttCert.holderDN != holderX509Cert.dn:
1011                raise AttributeAuthorityError("User certificate and Attribute "
1012                                        'Certificate DNs don\'t match: "%s"'
1013                                        ' and "%s"' % (holderX509Cert.dn, 
1014                                                       userAttCert.holderDN))
1015           
1016 
1017            # Get roles from external Attribute Certificate
1018            trustedHostRoles = userAttCert.roles
1019
1020
1021            # Map external roles to local ones
1022            localRoles = self.mapRemoteRoles2LocalRoles(
1023                                                    userAttCert['issuerName'],
1024                                                    trustedHostRoles)
1025            if not localRoles:
1026                raise AttributeAuthorityAccessDenied("No local roles mapped "
1027                                               "to the %s roles: %s" % 
1028                                               (userAttCert['issuerName'], 
1029                                                ', '.join(trustedHostRoles)))
1030
1031            attCert.addRoles(localRoles)
1032           
1033           
1034            # Mark new Attribute Certificate as mapped
1035            attCert.provenance = AttCert.mappedProvenance
1036
1037            # Copy the user Id from the external AC
1038            attCert.userId = userAttCert.userId
1039           
1040            # End set mapped certificate block
1041
1042        try:
1043            # Digitally sign certificate using Attribute Authority's
1044            # certificate and private key
1045            attCert.applyEnvelopedSignature()
1046           
1047            # Check the certificate is valid
1048            attCert.isValid(raiseExcep=True)
1049           
1050            # Write out certificate to keep a record of it for auditing
1051            #attCert.write()
1052            self.__attCertLog.info(attCert)
1053           
1054            log.info('Issued an Attribute Certificate to "%s" with roles: '
1055                     '"%s"' % (userId, '", "'.join(attCert.roles)))
1056
1057            # Return the cert to caller
1058            return attCert
1059       
1060        except Exception:
1061            raise AttributeAuthorityError('New Attribute Certificate "%s": %s'%
1062                                          (attCert.filePath, 
1063                                           traceback.format_exc()))
1064
1065    def samlAttributeQuery(self, attributeQuery):
1066        """Respond to SAML 2.0 Attribute Query
1067        """
1068        if not isinstance(attributeQuery, AttributeQuery):
1069            raise TypeError('Expecting %r for attribute query; got %r' %
1070                            (AttributeQuery, type(attributeQuery)))
1071           
1072        samlResponse = Response()
1073       
1074        samlResponse.issueInstant = datetime.utcnow()
1075        if self.attCertNotBeforeOff != 0:
1076            samlResponse.issueInstant += timedelta(
1077                                            seconds=self.attCertNotBeforeOff)
1078           
1079        samlResponse.id = str(uuid4())
1080        samlResponse.issuer = Issuer()
1081       
1082        # Initialise to success status but reset on error
1083        samlResponse.status = Status()
1084        samlResponse.status.statusCode = StatusCode()
1085        samlResponse.status.statusMessage = StatusMessage()
1086        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI
1087       
1088        # Nb. SAML 2.0 spec says issuer format must be omitted
1089        samlResponse.issuer.value = self.issuer
1090       
1091        samlResponse.inResponseTo = attributeQuery.id
1092       
1093        # Attribute Query validation ...
1094        utcNow = datetime.utcnow()
1095        if attributeQuery.issueInstant >= utcNow + self.clockSkew:
1096            msg = ('SAML Attribute Query issueInstant [%s] is at or after '
1097                   'the current clock time [%s]') % \
1098                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow))
1099            log.error(msg)
1100                     
1101            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1102            samlResponse.status.statusMessage = StatusMessage()
1103            samlResponse.status.statusMessage.value = msg
1104            return samlResponse
1105           
1106        elif attributeQuery.version < SAMLVersion.VERSION_20:
1107            samlResponse.status.statusCode.value = \
1108                                        StatusCode.REQUEST_VERSION_TOO_LOW_URI
1109            return samlResponse
1110       
1111        elif attributeQuery.version > SAMLVersion.VERSION_20:
1112            samlResponse.status.statusCode.value = \
1113                                        StatusCode.REQUEST_VERSION_TOO_HIGH_URI
1114            return samlResponse
1115       
1116        elif (attributeQuery.subject.nameID.format != 
1117              EsgSamlNamespaces.NAMEID_FORMAT):
1118            log.error('SAML Attribute Query subject format is %r; expecting '
1119                      '%r' % (attributeQuery.subject.nameID.format,
1120                                EsgSamlNamespaces.NAMEID_FORMAT))
1121            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1122            samlResponse.status.statusMessage.value = \
1123                                "Subject Name ID format is not recognised"
1124            return samlResponse
1125       
1126        elif attributeQuery.issuer.format not in Issuer.X509_SUBJECT:
1127            log.error('SAML Attribute Query issuer format is %r; expecting '
1128                      '%r' % (attributeQuery.issuer.format,
1129                                Issuer.X509_SUBJECT))
1130            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1131            samlResponse.status.statusMessage.value = \
1132                                            "Issuer format is not recognised"
1133            return samlResponse
1134       
1135        try:
1136            # Return a dictionary of name, value pairs
1137            self.attributeInterface.getAttributes(attributeQuery, samlResponse)
1138           
1139        except InvalidUserId, e:
1140            log.exception(e)
1141            samlResponse.status.statusCode.value = \
1142                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1143            return samlResponse
1144           
1145        except UserIdNotKnown, e:
1146            log.exception(e)
1147            samlResponse.status.statusCode.value = \
1148                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1149            samlResponse.status.statusMessage.value = str(e)
1150            return samlResponse
1151           
1152        except InvalidRequestorId, e:
1153            log.exception(e)
1154            samlResponse.status.statusCode.value = StatusCode.REQUEST_DENIED_URI
1155            samlResponse.status.statusMessage.value = str(e)
1156            return samlResponse
1157           
1158        except AttributeReleaseDenied, e:
1159            log.exception(e)
1160            samlResponse.status.statusCode.value = \
1161                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1162            samlResponse.status.statusMessage.value = str(e)
1163            return samlResponse
1164           
1165        except AttributeNotKnownError, e:
1166            log.exception(e)
1167            samlResponse.status.statusCode.value = \
1168                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1169            samlResponse.status.statusMessage.value = str(e)
1170            return samlResponse
1171           
1172        except Exception, e:
1173            log.exception("Unexpected error calling Attribute Interface "
1174                          "for subject [%s] and query issuer [%s]" %
1175                          (attributeQuery.subject.nameID.value,
1176                           attributeQuery.issuer.value))
1177           
1178            # SAML spec says application server should set a HTTP 500 Internal
1179            # Server error in this case
1180            raise 
1181
1182        return samlResponse
1183   
1184    def readMapConfig(self):
1185        """Parse Map Configuration file.
1186        """
1187        log.debug("Reading map configuration file ...")
1188       
1189        try:
1190            tree = ElementTree.parse(self.mapConfigFilePath)
1191            rootElem = tree.getroot()
1192           
1193        except IOError, e:
1194            raise AttributeAuthorityConfigError('Error parsing Map '
1195                                                'Configuration file "%s": %s' % 
1196                                                (e.filename, e.strerror))         
1197        except Exception, e:
1198            raise AttributeAuthorityConfigError('Error parsing Map '
1199                                                'Configuration file: "%s": %s'% 
1200                                                (self.mapConfigFilePath, e))
1201       
1202        trustedElem = rootElem.findall('trusted')
1203        if not trustedElem: 
1204            # Make an empty list so that for loop block below is skipped
1205            # without an error 
1206            trustedElem = ()
1207
1208        # Dictionaries:
1209        # 1) to hold all the data
1210        self.__mapConfig = {'thisHost': {}, 'trustedHosts': {}}
1211
1212        # ... look-up
1213        # 2) hosts corresponding to a given role and
1214        # 3) roles of external data centre to this data centre
1215        self.__localRole2TrustedHost = {}
1216        self.__localRole2RemoteRole = {}
1217        self.__remoteRole2LocalRole = {}
1218
1219        # Information about this host
1220        try:
1221            thisHostElem = rootElem.findall('thisHost')[0]
1222           
1223        except Exception, e:
1224            raise AttributeAuthorityConfigError('"thisHost" tag not found in '
1225                                                'Map Configuration file "%s"' % 
1226                                                self.mapConfigFilePath)
1227
1228        try:
1229            hostName = thisHostElem.attrib.values()[0]
1230           
1231        except Exception, e:
1232            raise AttributeAuthorityConfigError('"name" attribute of '
1233                                                '"thisHost" element not found '
1234                                                'in Map Configuration file '
1235                                                '"%s"' % 
1236                                                self.mapConfigFilePath)
1237
1238        # hostname is also stored in the AA's config file in the 'name' tag. 
1239        # Check the two match as the latter is copied into Attribute
1240        # Certificates issued by this AA
1241        #
1242        # TODO: would be better to rationalise this so that the hostname is
1243        # stored in one place only.
1244        #
1245        # P J Kershaw 14/06/06
1246        if hostName != self.name:
1247            raise AttributeAuthorityError('"name" attribute of "thisHost" '
1248                                          'element in Map Configuration file '
1249                                          'doesn\'t match "name" element in '
1250                                          'properties file.')
1251       
1252        # Information for THIS Attribute Authority
1253        self.__mapConfig['thisHost'][hostName] = {}
1254
1255        for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1256            val = thisHostElem.findtext(k)
1257            if val is None and v == NotImplemented:
1258                raise AttributeAuthorityConfigError('<thisHost> option <%s> '
1259                                                    'must be set.' % k)
1260            self.__mapConfig['thisHost'][hostName][k] = val     
1261       
1262        # Information about trusted hosts
1263        for elem in trustedElem:
1264            try:
1265                trustedHost = elem.attrib.values()[0]
1266               
1267            except Exception, e:
1268                raise AttributeAuthorityConfigError('Error reading trusted '
1269                                                    'host name: %s' % e)
1270 
1271            # Add signatureFile and list of roles
1272            #
1273            # (Currently Optional) additional tag allows query of the URI
1274            # where a user would normally login at the trusted host.  Added
1275            # this feature to allow users to be forwarded to their home site
1276            # if they are accessing a secure resource and are not
1277            # authenticated
1278            #
1279            # P J Kershaw 25/05/06
1280            self.__mapConfig['trustedHosts'][trustedHost] = {}
1281            for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1282                val = thisHostElem.findtext(k)
1283                if val is None and v == NotImplemented:
1284                    raise AttributeAuthorityConfigError('<trustedHost> option '
1285                                                        '<%s> must be set.'%k)
1286                   
1287                self.__mapConfig['trustedHosts'][trustedHost][k] = \
1288                                                        elem.findtext(k)   
1289
1290            roleElem = elem.findall('role')
1291            if roleElem:
1292                # Role keyword value requires special parsing before
1293                # assignment
1294                self.__mapConfig['trustedHosts'][trustedHost]['role'] = \
1295                                        [dict(i.items()) for i in roleElem]
1296            else:
1297                # It's possible for trust relationships to not contain any
1298                # role mapping.  e.g. a site's login service trusting other
1299                # sites login requests
1300                self.__mapConfig['trustedHosts'][trustedHost]['role'] = []
1301                       
1302            self.__localRole2RemoteRole[trustedHost] = {}
1303            self.__remoteRole2LocalRole[trustedHost] = {}
1304           
1305            for role in self.__mapConfig['trustedHosts'][trustedHost]['role']:
1306                try:
1307                    localRole = role['local']
1308                    remoteRole = role['remote']
1309                except KeyError, e:
1310                    raise AttributeAuthorityError('Reading map configuration '
1311                                                  ' file "%s": no element '
1312                                                  '"%s" for host "%s"' % 
1313                                                (self.mapConfigFilePath, 
1314                                                 e, 
1315                                                 trustedHost))
1316                   
1317                # Role to host look-up
1318                if localRole in self.__localRole2TrustedHost:
1319                   
1320                    if trustedHost not in \
1321                       self.__localRole2TrustedHost[localRole]:
1322                        self.__localRole2TrustedHost[localRole].\
1323                                                        append(trustedHost)                       
1324                else:
1325                    self.__localRole2TrustedHost[localRole] = [trustedHost]
1326
1327
1328                # Trusted Host to local role and trusted host to trusted role
1329                # map look-ups
1330                try:
1331                    self.__remoteRole2LocalRole[trustedHost][remoteRole].\
1332                                                            append(localRole)                 
1333                except KeyError:
1334                    self.__remoteRole2LocalRole[trustedHost][remoteRole] = \
1335                                                                [localRole]
1336                   
1337                try:
1338                    self.__localRole2RemoteRole[trustedHost][localRole].\
1339                                                            append(remoteRole)                 
1340                except KeyError:
1341                    self.__localRole2RemoteRole[trustedHost][localRole] = \
1342                                                                [remoteRole]
1343       
1344        # Store trusted host info look-up for retrieval by getTrustedHostInfo
1345        # method                                                                         
1346        #
1347        # Nb. {}.fromkeys([...]).keys() is a fudge to get unique elements
1348        # from a list i.e. convert the list elements to a dict eliminating
1349        # duplicated elements and convert the keys back into a list.
1350        self._trustedHostInfo = dict(
1351        [
1352            (
1353                k, 
1354                {
1355                    'siteName':             v['siteName'],
1356                    'aaURI':                v['aaURI'], 
1357                    'aaDN':                 v['aaDN'], 
1358                    'loginURI':             v['loginURI'], 
1359                    'loginServerDN':        v['loginServerDN'], 
1360                    'loginRequestServerDN': v['loginRequestServerDN'], 
1361                    'role':                 {}.fromkeys([role['remote'] 
1362                                                         for role in v['role']]
1363                                                       ).keys()
1364                }
1365            ) for k, v in self.__mapConfig['trustedHosts'].items()
1366        ])
1367
1368        log.info('Loaded map configuration file "%s"' % self.mapConfigFilePath)
1369       
1370       
1371    def getRoles(self, userId):
1372        """Get the roles available to the registered user identified userId.
1373
1374        @type dn: string
1375        @param dn: user identifier - could be a X500 Distinguished Name
1376        @return: list of roles for the given user ID"""
1377
1378        log.debug('Calling getRoles for user "%s" ...' % userId)
1379       
1380        # Call to AttributeInterface derived class.  Each Attribute Authority
1381        # should define it's own roles class derived from AttributeInterface to
1382        # define how roles are accessed
1383        try:
1384            return self.__attributeInterface.getRoles(userId)
1385
1386        except Exception, e:
1387            raise AttributeAuthorityError("Getting user roles: %s" % e)
1388       
1389       
1390    def _getHostInfo(self):
1391        """Return the host that this Attribute Authority represents: its ID,
1392        the user login URI and WSDL address.  Call this method via the
1393        'hostInfo' property
1394       
1395        @rtype: dict
1396        @return: dictionary of host information derived from the map
1397        configuration"""
1398       
1399        return self.__mapConfig['thisHost']
1400       
1401    hostInfo = property(fget=_getHostInfo, 
1402                        doc="Return information about this host")
1403       
1404       
1405    def getTrustedHostInfo(self, role=None):
1406        """Return a dictionary of the hosts that have trust relationships
1407        with this AA.  The dictionary is indexed by the trusted host name
1408        and contains AA service, login URIs and the roles that map to the
1409        given input local role.
1410
1411        @type role: string
1412        @param role: if set, return trusted hosts that having a mapping set
1413        for this role.  If no role is input, return all the AA's trusted hosts
1414        with all their possible roles
1415
1416        @rtype: dict
1417        @return: dictionary of the hosts that have trust relationships
1418        with this AA.  It returns an empty dictionary if role isn't
1419        recognised"""
1420               
1421        log.debug('Calling getTrustedHostInfo with role = "%s" ...' % role) 
1422                                 
1423        if not self.__mapConfig or not self.__localRole2RemoteRole:
1424            # This Attribute Authority has no trusted hosts
1425            raise AttributeAuthorityNoTrustedHosts("The %s Attribute "
1426                                                   "Authority has no trusted "
1427                                                   "hosts" % 
1428                                                   self.name)
1429
1430
1431        if role is None:
1432            # No role input - return all trusted hosts with their service URIs
1433            # and the remote roles they map to
1434            return self._trustedHostInfo
1435
1436        else:           
1437            # Get trusted hosts for given input local role       
1438            try:
1439                trustedHosts = self.__localRole2TrustedHost[role]
1440            except:
1441                raise AttributeAuthorityNoMatchingRoleInTrustedHosts(
1442                    'None of the trusted hosts have a mapping to the '
1443                    'input role "%s"' % role)
1444   
1445   
1446            # Get associated Web service URI and roles for the trusted hosts
1447            # identified and return as a dictionary indexed by host name
1448            trustedHostInfo = dict(
1449       [(
1450            host, 
1451            {
1452                'siteName': self.__mapConfig['trustedHosts'][host]['siteName'],
1453                'aaURI':    self.__mapConfig['trustedHosts'][host]['aaURI'],
1454                'aaDN':     self.__mapConfig['trustedHosts'][host]['aaDN'],
1455                'loginURI': self.__mapConfig['trustedHosts'][host]['loginURI'],
1456                'loginServerDN': 
1457                        self.__mapConfig['trustedHosts'][host]['loginServerDN'],
1458                'loginRequestServerDN': 
1459                self.__mapConfig['trustedHosts'][host]['loginRequestServerDN'],
1460                'role':     self.__localRole2RemoteRole[host][role]
1461            }
1462        ) for host in trustedHosts])
1463                         
1464            return trustedHostInfo
1465       
1466       
1467    def mapRemoteRoles2LocalRoles(self, trustedHost, trustedHostRoles):
1468        """Map roles of trusted hosts to roles for this data centre
1469
1470        @type trustedHost: string
1471        @param trustedHost: name of external trusted data centre
1472        @type trustedHostRoles: list
1473        @param trustedHostRoles:   list of external roles to map
1474        @return: list of mapped roles"""
1475
1476        if not self.__remoteRole2LocalRole:
1477            raise AttributeAuthorityError("Roles map is not set - ensure " 
1478                                    "readMapConfig() has been called.")
1479
1480
1481        # Check the host name is a trusted one recorded in the map
1482        # configuration
1483        if not self.__remoteRole2LocalRole.has_key(trustedHost):
1484            return []
1485
1486        # Add local roles, skipping if no mapping is found
1487        localRoles = []
1488        for trustedRole in trustedHostRoles:
1489            if trustedRole in self.__remoteRole2LocalRole[trustedHost]:
1490                localRoles.extend(
1491                        self.__remoteRole2LocalRole[trustedHost][trustedRole])
1492               
1493        return localRoles
1494
1495    def getAttCertFactory(self):
1496        """Factory method to create SAML Attribute Qeury wrapper function
1497        @rtype: function
1498        @return getAttCert method function wrapper
1499        """
1500        def getAttCertWrapper(*arg, **kw):
1501            """
1502            @type *arg: tuple
1503            @param *arg: getAttCert arguments
1504            @type **kw: dict
1505            @param **kw: getAttCert keyword arguments
1506            @rtype: ndg.security.common.AttCert.AttCert
1507            @return: new attribute certificate
1508            """
1509            return self.getAttCert(*arg, **kw)
1510       
1511        return getAttCertWrapper
1512
1513    def samlAttributeQueryFactory(self):
1514        """Factory method to create SAML Attribute Qeury wrapper function
1515        @rtype: function
1516        @return: samlAttributeQuery method function wrapper
1517        """
1518        def samlAttributeQueryWrapper(attributeQuery):
1519            """
1520            @type attributeQuery: saml.saml2.core.AttributeQuery
1521            @param attributeQuery: SAML Attribute Query
1522            @rtype: saml.saml2.core.Response
1523            @return: SAML response
1524            """
1525            return self.samlAttributeQuery(attributeQuery)
1526       
1527        return samlAttributeQueryWrapper
1528   
1529
1530from logging.handlers import RotatingFileHandler
1531
1532# Inherit directly from Logger
1533_loggerClass = logging.getLoggerClass()
1534class AttCertLog(_loggerClass, object):
1535    """Log each Attribute Certificate issued using a rotating file handler
1536    so that the number of files held can be managed"""
1537   
1538    def __init__(self, attCertFilePath, backUpCnt=1024):
1539        """Set up a rotating file handler to log ACs issued.
1540        @type attCertFilePath: string
1541        @param attCertFilePath: set where to store ACs.  Set from
1542        AttributeAuthority properties file.
1543       
1544        @type backUpCnt: int
1545        @param backUpCnt: set the number of files to store before rotating
1546        and overwriting old files."""
1547       
1548        if not isinstance(backUpCnt, int):
1549            raise TypeError('Expecting int type for "backUpCnt" keyword')
1550       
1551        # Inherit from Logger class
1552        super(AttCertLog, self).__init__(name='', level=logging.INFO)
1553                           
1554        # Set a format for messages so that only the content of the AC is
1555        # logged, nothing else.
1556        formatter = logging.Formatter(fmt="", datefmt="")
1557
1558        # maxBytes is set to one so that only one AC will be written before
1559        # rotation to the next file
1560        fileLog = RotatingFileHandler(attCertFilePath, 
1561                                      maxBytes=1, 
1562                                      backupCount=backUpCnt)
1563        fileLog.setFormatter(formatter)           
1564        self.addHandler(fileLog)
1565 
1566                     
1567class AttributeInterfaceError(Exception):
1568    """Exception handling for NDG Attribute Authority User Roles interface
1569    class."""
1570 
1571                     
1572class AttributeInterfaceConfigError(AttributeInterfaceError):
1573    """Invalid configuration set for Attribute interface"""
1574 
1575                     
1576class AttributeInterfaceRetrieveError(AttributeInterfaceError):
1577    """Error retrieving attributes for Attribute interface class"""
1578
1579                       
1580class AttributeReleaseDenied(AttributeInterfaceError):
1581    """Requestor was denied release of the requested attributes"""
1582
1583                       
1584class AttributeNotKnownError(AttributeInterfaceError):
1585    """Requested attribute names are not known to this authority"""
1586
1587
1588class InvalidRequestorId(AttributeInterfaceError):
1589    """Requestor is not known or not allowed to request attributes"""
1590   
1591
1592class UserIdNotKnown(AttributeInterfaceError): 
1593    """User ID passed to getAttributes is not known to the authority"""
1594   
1595   
1596class InvalidUserId(AttributeInterfaceError):
1597    """User Id passed to getAttributes is invalid"""
1598   
1599   
1600class InvalidAttributeFormat(AttributeInterfaceError):
1601    """Format for Attribute requested is invalid or not supported"""
1602   
1603     
1604class AttributeInterface(object):
1605    """An abstract base class to define the user roles interface to an
1606    Attribute Authority.
1607
1608    Each NDG data centre should implement a derived class which implements
1609    the way user roles are provided to its representative Attribute Authority.
1610   
1611    Roles are expected to indexed by user Distinguished Name (DN).  They
1612    could be stored in a database or file."""
1613   
1614    # Enable derived classes to use slots if desired
1615    __slots__ = ()
1616   
1617    # User defined class may wish to specify a URI for a database interface or
1618    # path for a user roles configuration file
1619    def __init__(self, **prop):
1620        """User Roles base class - derive from this class to define
1621        roles interface to Attribute Authority
1622       
1623        @type prop: dict
1624        @param prop: custom properties to pass to this class
1625        """
1626
1627    def getRoles(self, userId):
1628        """Virtual method - Derived method should return the roles for the
1629        given user's Id or else raise an exception
1630       
1631        @type userId: string
1632        @param userId: user identity e.g. user Distinguished Name
1633        @rtype: list
1634        @return: list of roles for the given user ID
1635        @raise AttributeInterfaceError: an error occured requesting
1636        attributes
1637        """
1638        raise NotImplementedError(self.getRoles.__doc__)
1639 
1640    def getAttributes(self, attributeQuery, response):
1641        """Virtual method should be implemented in a derived class to enable
1642        AttributeAuthority.samlAttributeQuery - The derived method should
1643        return the attributes requested for the given user's Id or else raise
1644        an exception
1645       
1646        @type attributeQuery: saml.saml2.core.AttributeQuery
1647        @param userId: query containing requested attributes
1648        @type: saml.saml2.core.Response
1649        @param: Response - add an assertion with the list of attributes
1650        for the given subject ID in the query or set an error Status code and
1651        message
1652        @raise AttributeInterfaceError: an error occured requesting
1653        attributes
1654        @raise AttributeReleaseDeniedError: Requestor was denied release of the
1655        requested attributes
1656        @raise AttributeNotKnownError: Requested attribute names are not known
1657        to this authority
1658        """
1659        raise NotImplementedError(self.getAttributes.__doc__)
1660
1661
1662class CSVFileAttributeInterface(AttributeInterface):
1663    """Attribute Interface based on a Comma Separated Variable file containing
1664    user identities and associated attributes.  For test/development purposes
1665    only.  The SAML getAttributes method is NOT implemented here
1666   
1667    The expected file format is:
1668   
1669    <userID>, <role1>, <role2>, ... <roleN>
1670    """
1671    def __init__(self, propertiesFilePath=None):
1672        """
1673        @param propertiesFilePath: file path to Comma Separated file
1674        containing user ids and roles
1675        @type propertiesFilePath: basestring
1676        """
1677        if propertiesFilePath is None:
1678            raise AttributeError("Expecting propertiesFilePath setting")
1679       
1680        propertiesFile = open(propertiesFilePath)
1681        lines = propertiesFile.readlines()
1682       
1683        self.attributeMap = {}
1684        for line in lines:
1685            fields = re.split(',\s*', line.strip())
1686            self.attributeMap[fields[0]] = fields[1:]
1687   
1688    def getRoles(self, userId):
1689        """
1690        @param userId: user identity to key into attributeMap
1691        @type userId: basestring
1692        """ 
1693        log.debug('CSVFileAttributeInterface.getRoles for user "%s" ...', 
1694                  userId)
1695        return self.attributeMap.get(userId, [])
1696
1697
1698# Properties file
1699from ConfigParser import SafeConfigParser, NoOptionError
1700
1701try:
1702    # PostgreSQL interface
1703    from psycopg2 import connect
1704except ImportError:
1705    pass
1706
1707class PostgresAttributeInterface(AttributeInterface):
1708    """User Roles interface to Postgres database
1709   
1710    The SAML getAttributes method is NOT implemented
1711   
1712    The configuration file follows the form,
1713   
1714    [Connection]
1715    # name of database
1716    dbName: user.db
1717   
1718    # database host machine
1719    host: mydbhost.ac.uk
1720   
1721    # database account username
1722    username: mydbaccount
1723   
1724    # Password - comment out to prompt from stdin instead
1725    pwd: mydbpassword
1726   
1727    [getRoles]
1728    query0: select distinct grp from users_table, where user = '%%s'
1729    defaultRoles = publicRole
1730    """
1731
1732    CONNECTION_SECTION_NAME = "Connection"
1733    GETROLES_SECTION_NAME = "getRoles"
1734    HOST_OPTION_NAME = "host"
1735    DBNAME_OPTION_NAME = "dbName"
1736    USERNAME_OPTION_NAME = "username"
1737    PWD_OPTION_NAME = "pwd"
1738    QUERYN_OPTION_NAME = "query%d"
1739    DEFAULT_ROLES_OPTION_NAME = "defaultRoles"
1740   
1741    def __init__(self, propertiesFilePath=None):
1742        """Connect to Postgres database"""
1743        self.__con = None
1744        self.__host = None
1745        self.__dbName = None
1746        self.__username = None
1747        self.__pwd = None
1748
1749        if propertiesFilePath is None:
1750            raise AttributeError("No Configuration file was set")
1751
1752        self.readConfigFile(propertiesFilePath)
1753
1754    def __del__(self):
1755        """Close database connection"""
1756        self.close()
1757
1758    def readConfigFile(self, propertiesFilePath):
1759        """Read the configuration for the database connection
1760
1761        @type propertiesFilePath: string
1762        @param propertiesFilePath: file path to config file"""
1763
1764        if not isinstance(propertiesFilePath, basestring):
1765            raise TypeError("Input Properties file path must be a valid "
1766                            "string; got %r" % type(propertiesFilePath))
1767
1768        cfg = SafeConfigParser()
1769        cfg.read(propertiesFilePath)
1770
1771        self.__host = cfg.get(
1772                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1773                        PostgresAttributeInterface.HOST_OPTION_NAME)
1774        self.__dbName = cfg.get(
1775                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1776                        PostgresAttributeInterface.DBNAME_OPTION_NAME)
1777        self.__username = cfg.get(
1778                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1779                        PostgresAttributeInterface.USERNAME_OPTION_NAME)
1780        self.__pwd = cfg.get(
1781                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1782                        PostgresAttributeInterface.PWD_OPTION_NAME)
1783
1784        try:
1785            self.__getRolesQuery = []
1786            for i in range(10):
1787                queryStr = cfg.get(
1788                        PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1789                        PostgresAttributeInterface.QUERYN_OPTION_NAME % i)
1790                self.__getRolesQuery += [queryStr]
1791        except NoOptionError:
1792             # Continue until no more query<n> items left
1793             pass
1794
1795        # This option may be omitted in the config file
1796        try:
1797            self.__defaultRoles = cfg.get(
1798                PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1799                PostgresAttributeInterface.DEFAULT_ROLES_OPTION_NAME).split()
1800        except NoOptionError:
1801            self.__defaultRoles = []
1802
1803    def connect(self,
1804                username=None,
1805                dbName=None,
1806                host=None,
1807                pwd=None,
1808                prompt="Database password: "):
1809        """Connect to database
1810
1811        Values for keywords omitted are derived from the config file.  If pwd
1812        is not in the config file it will be prompted for from stdin
1813
1814        @type username: string
1815        @keyword username: database account username
1816        @type dbName: string
1817        @keyword dbName: name of database
1818        @type host: string
1819        @keyword host: database host machine
1820        @type pwd: string
1821        @keyword pwd: password for database account.  If omitted and not in
1822        the config file it will be prompted for from stdin
1823        @type prompt: string
1824        @keyword prompt: override default password prompt"""
1825
1826        if not host:
1827            host = self.__host
1828
1829        if not dbName:
1830            dbName = self.__dbName
1831
1832        if not username:
1833            username = self.__username
1834
1835        if not pwd:
1836            pwd = self.__pwd
1837
1838            if not pwd:
1839                import getpass
1840                pwd = getpass.getpass(prompt=prompt)
1841
1842        try:
1843            self.__db = connect("host=%s dbname=%s user=%s password=%s" % \
1844                                (host, dbName, username, pwd))
1845            self.__cursor = self.__db.cursor()
1846
1847        except NameError, e:
1848            raise AttributeInterfaceError("psycopg2 Postgres package not "
1849                                          "installed? %s" % e)
1850        except Exception, e:
1851            raise AttributeInterfaceError("Error connecting to database "
1852                                          "\"%s\": %s" % (dbName, e))
1853
1854    def close(self):
1855        """Close database connection"""
1856        if self.__con:
1857            self.__con.close()
1858
1859    def getRoles(self, userId):
1860        """Return valid roles for the given userId
1861
1862        @type userId: basestring
1863        @param userId: user identity"""
1864
1865        try:
1866            self.connect()
1867
1868            # Process each query in turn appending role names
1869            roles = self.__defaultRoles[:]
1870            for query in self.__getRolesQuery:
1871                try:
1872                    self.__cursor.execute(query % userId)
1873                    queryRes = self.__cursor.fetchall()
1874
1875                except Exception, e:
1876                    raise AttributeInterfaceError("Query for %s: %s" %
1877                                                  (userId, e))
1878
1879                roles += [res[0] for res in queryRes if res[0]]
1880        finally:
1881            self.close()
1882
1883        return roles
1884
1885    def __getCursor(self):
1886        """Return a database cursor instance"""
1887        return self.__cursor
1888
1889    cursor = property(fget=__getCursor, doc="database cursor")
1890
1891
1892import traceback
1893from string import Template
1894try:
1895    from sqlalchemy import create_engine, exc
1896    sqlAlchemyInstalled = True
1897except ImportError:
1898    sqlAlchemyInstalled = False
1899   
1900
1901class SQLAlchemyAttributeInterface(AttributeInterface):
1902    '''SQLAlchemy based Attribute interface enables the Attribute Authority
1903    to interface to any database type supported by it
1904   
1905    @type SQLQUERY_USERID_KEYNAME: basestring
1906    @cvar SQLQUERY_USERID_KEYNAME: key corresponding to string to be
1907    substituted into attribute query for user identifier e.g.
1908   
1909    select attr from user_table where username = $userId
1910   
1911    @type SAML_VALID_REQUESTOR_DNS_PAT: _sre.SRE_Pattern
1912    @param SAML_VALID_REQUESTOR_DNS_PAT: regular expression to split list of
1913    SAML requestor DNs.  These must comma separated.  Each comma may be
1914    separated by any white space including new line characters
1915    ''' 
1916    DEFAULT_SAML_ASSERTION_LIFETIME = timedelta(seconds=60*60*8) 
1917     
1918    SQLQUERY_USERID_KEYNAME = 'userId'
1919   
1920    ISSUER_NAME_FORMAT = Issuer.X509_SUBJECT
1921    ISSUER_NAME_OPTNAME = 'issuerName'
1922    CONNECTION_STRING_OPTNAME = 'connectionString'
1923    ATTRIBUTE_SQLQUERY_OPTNAME = 'attributeSqlQuery'
1924    SAML_SUBJECT_SQLQUERY_OPTNAME = 'samlSubjectSqlQuery'
1925    SAML_VALID_REQUESTOR_DNS_OPTNAME = 'samlValidRequestorDNs'
1926    SAML_ASSERTION_LIFETIME_OPTNAME = 'samlAssertionLifetime'
1927    SAML_ATTRIBUTE2SQLQUERY_OPTNAME = 'samlAttribute2SqlQuery'
1928    SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN = len(SAML_ATTRIBUTE2SQLQUERY_OPTNAME)
1929   
1930    SAML_ATTRIBUTE2SQLQUERY_ATTRNAME_DELIMITERS = ('.', '_')
1931   
1932    __slots__ = (
1933        ISSUER_NAME_OPTNAME,
1934        CONNECTION_STRING_OPTNAME,
1935        ATTRIBUTE_SQLQUERY_OPTNAME,
1936        SAML_SUBJECT_SQLQUERY_OPTNAME,
1937        SAML_VALID_REQUESTOR_DNS_OPTNAME,
1938        SAML_ASSERTION_LIFETIME_OPTNAME,
1939        SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
1940    )
1941    __PRIVATE_ATTR_PREFIX = '_SQLAlchemyAttributeInterface__'
1942    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
1943    del i
1944   
1945#    For Reference - split based on space separated ' or " quoted items
1946#    SAML_VALID_REQUESTOR_DNS_PAT = re.compile("['\"]?\s*['\"]")
1947   
1948    SAML_VALID_REQUESTOR_DNS_PAT = re.compile(',\s*')
1949   
1950    def __init__(self, **properties):
1951        '''Instantiate object taking in settings from the input properties'''
1952        log.debug('Initialising SQLAlchemyAttributeInterface instance ...')
1953       
1954        if not sqlAlchemyInstalled:
1955            raise AttributeInterfaceConfigError("SQLAlchemy is not installed")
1956       
1957        self.__issuerName = None
1958        self.__connectionString = None
1959        self.__attributeSqlQuery = None
1960        self.__samlSubjectSqlQuery = None
1961        self.__samlValidRequestorDNs = []
1962        self.__samlAssertionLifetime = \
1963            SQLAlchemyAttributeInterface.DEFAULT_SAML_ASSERTION_LIFETIME
1964        self.__samlAttribute2SqlQuery = {}
1965       
1966        self.setProperties(**properties)
1967
1968    def __setattr__(self, name, value):
1969        """Provide a way to set the attribute map by dynamically handling
1970        attribute names containing the SAML attribute name as a suffix e.g.
1971       
1972        attributeInterface.samlAttribute2SqlQuery_firstName = 'Philip'
1973       
1974        will update __samlAttribute2SqlQuery with the 'firstName', 'Philip'
1975        key value pair.  Similarly,
1976       
1977        setattr('samlAttribute2SqlQuery.emailAddress', 'pjk@somewhere.ac.uk')
1978       
1979        sets __samlAttribute2SqlQuery with the 'emailAddress',
1980        'pjk@somewhere.ac.uk' key value pair
1981       
1982        This is useful in enabling settings to be made direct from a dict of
1983        option name and values parsed from an ini file.
1984        """
1985        cls = SQLAlchemyAttributeInterface
1986       
1987        if name in cls.__slots__:
1988            object.__setattr__(self, name, value)
1989           
1990        elif (name[cls.SAML_ATTRIBUTE2SQLQUERY_OPTNAME_LEN] in 
1991              cls.SAML_ATTRIBUTE2SQLQUERY_ATTRNAME_DELIMITERS):
1992            # A special 'samlAttribute2SqlQuery[._]+' attribute name has been
1993            # found.  The first item is the attribute name and the second, the
1994            # corresponding SQL query to get the values corresponding to that
1995            # name.           
1996            samlAttributeName, samlAttributeSqlQuery = value.split(None, 1)
1997           
1998            # Items may be quoted with " quotes
1999            self.__samlAttribute2SqlQuery[samlAttributeName.strip('"')
2000                                          ] = samlAttributeSqlQuery.strip('"')
2001        else:
2002            raise AttributeError("'SQLAlchemyAttributeInterface' has no "
2003                                 "attribute %r" % name)
2004
2005    def setProperties(self, prefix='', **properties):
2006        for name, val in properties.items():
2007            if prefix:
2008                if name.startswith(prefix):
2009                    name = name.replace(prefix, '', 1)
2010                    setattr(self, name, val)
2011            else:
2012                setattr(self, name, val)
2013
2014    def _getIssuerName(self):
2015        return self.__issuerName
2016
2017    def _setIssuerName(self, value):
2018        if not isinstance(value, basestring):
2019            raise TypeError('Expecting string type for "%s" attribute; got %r'%
2020                            (SQLAlchemyAttributeInterface.ISSUER_NAME_OPTNAME,
2021                             type(value)))
2022
2023        self.__issuerName = value
2024
2025    issuerName = property(_getIssuerName, 
2026                          _setIssuerName, 
2027                          doc="The name of the issuing organisation.  This is "
2028                              "expected to be an X.509 Distinguished Name")
2029           
2030    def _getSamlAssertionLifetime(self):
2031        return self.__samlAssertionLifetime
2032
2033    def _setSamlAssertionLifetime(self, value):
2034        if isinstance(value, timedelta):
2035            self.__samlAssertionLifetime = value
2036           
2037        if isinstance(value, (float, int, long)):
2038            self.__samlAssertionLifetime = timedelta(seconds=value)
2039           
2040        elif isinstance(value, basestring):
2041            self.__samlAssertionLifetime = timedelta(seconds=float(value))
2042        else:
2043            raise TypeError('Expecting float, int, long, string or timedelta '
2044                'type for "%s"; got %r' % 
2045                (SQLAlchemyAttributeInterface.SAML_ASSERTION_LIFETIME_OPTNAME,
2046                 type(value)))
2047
2048    samlAssertionLifetime = property(_getSamlAssertionLifetime, 
2049                                     _setSamlAssertionLifetime, 
2050                                     doc="Time validity for SAML Assertion "
2051                                         "set in SAML Response returned from "
2052                                         "getAttributes")
2053
2054    def _getSamlSubjectSqlQuery(self):
2055        return self.__samlSubjectSqlQuery
2056
2057    def _setSamlSubjectSqlQuery(self, value):
2058        if not isinstance(value, basestring):
2059            raise TypeError('Expecting string type for "%s" attribute; got %r'%
2060                    (SQLAlchemyAttributeInterface.SAML_SUBJECT_SQLQUERY_OPTNAME,
2061                     type(value)))
2062           
2063        self.__samlSubjectSqlQuery = value
2064
2065    samlSubjectSqlQuery = property(_getSamlSubjectSqlQuery, 
2066                                   _setSamlSubjectSqlQuery, 
2067                                   doc="SAML Subject SQL Query")
2068
2069    def _getSamlAttribute2SqlQuery(self):
2070        return self.__samlAttribute2SqlQuery
2071
2072    def _setSamlAttribute2SqlQuery(self, value):
2073        if isinstance(value, dict):
2074            # Validate string type for keys and values
2075            invalidItems = [(k, v) for k, v in value.items() 
2076                            if (not isinstance(k, basestring) or 
2077                                not isinstance(v, basestring))]
2078            if invalidItems:
2079                raise TypeError('Expecting string type for "%s" dict items; '
2080                                'got these/this invalid item(s) %r' % 
2081                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
2082                 invalidItems))
2083               
2084            self.__samlAttribute2SqlQuery = value
2085           
2086        elif isinstance(value, (tuple, list)):
2087            for query in value:
2088                if not isinstance(query, basestring):
2089                    raise TypeError('Expecting string type for "%s" '
2090                                    'attribute items; got %r' %
2091                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
2092                 type(value)))
2093                   
2094            self.__samlAttribute2SqlQuery = value                 
2095        else:
2096            raise TypeError('Expecting dict type for "%s" attribute; got %r' %
2097                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE2SQLQUERY_OPTNAME,
2098                 type(value)))
2099           
2100    samlAttribute2SqlQuery = property(_getSamlAttribute2SqlQuery, 
2101                                      _setSamlAttribute2SqlQuery, 
2102                                      doc="SQL Query or queries to obtain the "
2103                                          "attribute information to respond "
2104                                          "a SAML attribute query.  The "
2105                                          "attributes returned from each "
2106                                          "query concatenated together, must "
2107                                          "exactly match the SAML attribute "
2108                                          "names set in the samlAttributeNames "
2109                                          "property")
2110
2111    def _getSamlValidRequestorDNs(self):
2112        return self.__samlValidRequestorDNs
2113
2114    def _setSamlValidRequestorDNs(self, value):
2115        if isinstance(value, basestring):
2116           
2117            pat = SQLAlchemyAttributeInterface.SAML_VALID_REQUESTOR_DNS_PAT
2118            self.__samlValidRequestorDNs = [
2119                X500DN.fromString(dn) for dn in pat.split(value)
2120            ]
2121           
2122        elif isinstance(value, (tuple, list)):
2123            self.__samlValidRequestorDNs = [X500DN.fromString(dn) 
2124                                            for dn in value]
2125        else:
2126            raise TypeError('Expecting list/tuple or basestring type for "%s" '
2127                'attribute; got %r' %
2128                (SQLAlchemyAttributeInterface.SAML_VALID_REQUESTOR_DNS_OPTNAME,
2129                 type(value)))
2130   
2131    samlValidRequestorDNs = property(_getSamlValidRequestorDNs, 
2132                                     _setSamlValidRequestorDNs, 
2133                                     doc="list of certificate Distinguished "
2134                                         "Names referring to the client "
2135                                         "identities permitted to query the "
2136                                         "Attribute Authority via the SAML "
2137                                         "Attribute Query interface")
2138   
2139    def _getConnectionString(self):
2140        return self.__connectionString
2141
2142    def _setConnectionString(self, value):
2143        if not isinstance(value, basestring):
2144            raise TypeError('Expecting string type for "%s" attribute; got %r'%
2145                        (SQLAlchemyAttributeInterface.CONNECTION_STRING_OPTNAME,
2146                         type(value)))
2147        self.__connectionString = value
2148
2149    connectionString = property(fget=_getConnectionString, 
2150                                fset=_setConnectionString, 
2151                                doc="Database connection string")
2152
2153    def _getAttributeSqlQuery(self):
2154        return self.__attributeSqlQuery
2155
2156    def _setAttributeSqlQuery(self, value):
2157        if not isinstance(value, basestring):
2158            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
2159                    (SQLAlchemyAttributeInterface.ATTRIBUTE_SQLQUERY_OPTNAME,
2160                     type(value)))
2161        self.__attributeSqlQuery = value
2162
2163    attributeSqlQuery = property(fget=_getAttributeSqlQuery, 
2164                                 fset=_setAttributeSqlQuery, 
2165                                 doc="SQL Query for attribute query")
2166   
2167    def getRoles(self, userId):     
2168        """Return valid roles for the given userId
2169
2170        @type userId: basestring
2171        @param userId: user identity
2172        @rtype: list
2173        @return: list of roles for the given user
2174        """
2175
2176        dbEngine = create_engine(self.connectionString)
2177        connection = dbEngine.connect()
2178       
2179        try:
2180            queryInputs = {
2181                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME:
2182                userId
2183            }
2184            query = Template(self.attributeSqlQuery).substitute(queryInputs)
2185            result = connection.execute(query)
2186
2187        except exc.ProgrammingError:
2188            raise AttributeInterfaceRetrieveError("Error with SQL Syntax: %s" %
2189                                                  traceback.format_exc())
2190        finally:
2191            connection.close()
2192
2193        try:
2194            attributes = [attr for attr in result][0][0]
2195       
2196        except (IndexError, TypeError):
2197            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
2198                                                  traceback.format_exc())
2199       
2200        log.debug('Attributes=%r retrieved for user=%r' % (attributes, 
2201                                                           userId))
2202       
2203        return attributes
2204
2205    def getAttributes(self, attributeQuery, response):
2206        """Attribute Authority SAML AttributeQuery
2207       
2208        @type attributeQuery: saml.saml2.core.AttributeQuery
2209        @param userId: query containing requested attributes
2210        @type: saml.saml2.core.Response
2211        @param: Response - add an assertion with the list of attributes
2212        for the given subject ID in the query or set an error Status code and
2213        message
2214        @raise AttributeInterfaceError: an error occured requesting
2215        attributes
2216        @raise AttributeReleaseDeniedError: Requestor was denied release of the
2217        requested attributes
2218        @raise AttributeNotKnownError: Requested attribute names are not known
2219        to this authority
2220        """
2221        userId = attributeQuery.subject.nameID.value
2222        requestedAttributeNames = [attribute.name
2223                                   for attribute in attributeQuery.attributes]
2224       
2225        requestorDN = X500DN.fromString(attributeQuery.issuer.value)
2226
2227        if not self._queryDbForSamlSubject(userId):
2228            raise UserIdNotKnown('Subject Id "%s" is not known to this '
2229                                 'authority' % userId)
2230
2231        if requestorDN not in self.samlValidRequestorDNs:
2232            raise InvalidRequestorId('Requestor identity "%s" is invalid' %
2233                                     requestorDN)
2234
2235        unknownAttrNames = [attrName for attrName in requestedAttributeNames
2236                            if attrName not in self.samlAttribute2SqlQuery]
2237
2238        if len(unknownAttrNames) > 0:
2239            raise AttributeNotKnownError("Unknown attributes requested: %r" %
2240                                         unknownAttrNames)
2241       
2242        # Create a new assertion to hold the attributes to be returned
2243        assertion = Assertion()
2244
2245        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
2246        assertion.id = str(uuid4())
2247        assertion.issueInstant = response.issueInstant
2248   
2249        assertion.issuer = Issuer()
2250        assertion.issuer.value = self.issuerName
2251        assertion.issuer.format = Issuer.X509_SUBJECT
2252
2253        assertion.conditions = Conditions()
2254        assertion.conditions.notBefore = assertion.issueInstant
2255        assertion.conditions.notOnOrAfter = (assertion.conditions.notBefore + 
2256                                             self.samlAssertionLifetime)
2257
2258        assertion.subject = Subject()
2259        assertion.subject.nameID = NameID()
2260        assertion.subject.nameID.format = attributeQuery.subject.nameID.format
2261        assertion.subject.nameID.value = attributeQuery.subject.nameID.value
2262
2263        attributeStatement = AttributeStatement()
2264
2265        # Query the database for the requested attributes and return them
2266        # mapped to their attribute names as specified by the attributeNames
2267        # property
2268        for requestedAttribute in attributeQuery.attributes:
2269            attributeVals = self._queryDbForSamlAttributes(
2270                                                    requestedAttribute.name, 
2271                                                    userId)
2272
2273            # Make a new SAML attribute object to hold the values obtained
2274            attribute = Attribute()
2275            attribute.name = requestedAttribute.name
2276           
2277            # Check name format requested - only XSString is currently
2278            # supported
2279            if (requestedAttribute.nameFormat != 
2280                XSStringAttributeValue.DEFAULT_FORMAT):
2281                raise InvalidAttributeFormat('Requested attribute type %r but '
2282                                     'only %r type is supported' %
2283                                     (requestedAttribute.nameFormat,
2284                                      XSStringAttributeValue.DEFAULT_FORMAT))
2285           
2286            attribute.nameFormat = requestedAttribute.nameFormat
2287
2288            if requestedAttribute.friendlyName is not None:
2289                attribute.friendlyName = requestedAttribute.friendlyName
2290
2291            for val in attributeVals:
2292                attribute.attributeValues.append(XSStringAttributeValue())
2293                attribute.attributeValues[-1].value = val
2294
2295            attributeStatement.attributes.append(attribute)
2296
2297        assertion.attributeStatements.append(attributeStatement)
2298        response.assertions.append(assertion)
2299       
2300    def _queryDbForSamlSubject(self, userId):     
2301        """Check a given SAML subject (user) is registered in the database.
2302        This method is called from the getAttributes() method
2303
2304        @type userId: basestring
2305        @param userId: user identity
2306        @rtype: bool
2307        @return: True/False is user registered?
2308        """
2309        if self.samlSubjectSqlQuery is None:
2310            log.debug('No "self.samlSubjectSqlQuery" property has been set, '
2311                      'skipping SAML subject query step')
2312            return True
2313       
2314        if self.connectionString is None:
2315            raise AttributeInterfaceConfigError('No "connectionString" setting '
2316                                                'has been made')
2317           
2318        dbEngine = create_engine(self.connectionString)
2319       
2320        try:
2321            queryInputs = {
2322                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME: userId
2323            }
2324            query = Template(self.samlSubjectSqlQuery).substitute(queryInputs)
2325           
2326        except KeyError, e:
2327            raise AttributeInterfaceConfigError("Invalid key for SAML subject "
2328                        "query string.  The valid key is %r" % 
2329                        SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME)   
2330
2331        log.debug('Checking for SAML subject with SQL Query = "%s"', query)
2332        try:
2333            connection = dbEngine.connect()
2334            result = connection.execute(query)
2335
2336        except (exc.ProgrammingError, exc.OperationalError):
2337            raise AttributeInterfaceRetrieveError('SQL error: %s' %
2338                                                  traceback.format_exc()) 
2339        finally:
2340            connection.close()
2341
2342        try:
2343            found = [entry for entry in result][0][0] > 0
2344       
2345        except (IndexError, TypeError):
2346            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
2347                                                  traceback.format_exc())
2348       
2349        log.debug('user=%r found=%r' % (userId, found))
2350       
2351        return found
2352     
2353    def _queryDbForSamlAttributes(self, attributeName, userId):     
2354        """Query the database in response to a SAML attribute query
2355       
2356        This method is called from the getAttributes() method
2357
2358        @type userId: basestring
2359        @param userId: user identity
2360        @rtype: bool
2361        @return: True/False is user registered?
2362        """
2363       
2364        if self.connectionString is None:
2365            raise AttributeInterfaceConfigError('No "connectionString" setting '
2366                                                'has been made')
2367
2368        dbEngine = create_engine(self.connectionString)
2369       
2370        queryTmpl = self.samlAttribute2SqlQuery.get(attributeName)
2371        if queryTmpl is None:
2372            raise AttributeInterfaceConfigError('No SQL query set for '
2373                                                'attribute %r' % attributeName)
2374       
2375        try:
2376            queryInputs = {
2377                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME: userId
2378            }
2379            query = Template(queryTmpl).substitute(queryInputs)
2380           
2381        except KeyError, e:
2382            raise AttributeInterfaceConfigError("Invalid key %s for SAML "
2383                        "attribute query string.  The valid key is %r" % 
2384                        (e,
2385                         SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME))
2386           
2387        log.debug('Checking for SAML attributes with SQL Query = "%s"', query)
2388               
2389        try:
2390            connection = dbEngine.connect()
2391            result = connection.execute(query)
2392           
2393        except (exc.ProgrammingError, exc.OperationalError):
2394            raise AttributeInterfaceRetrieveError('SQL error: %s' %
2395                                                  traceback.format_exc())
2396        finally:
2397            connection.close()
2398
2399        try:
2400            attributeValues = [entry[0] for entry in result]
2401           
2402        except (IndexError, TypeError):
2403            raise AttributeInterfaceRetrieveError("Error with result set: "
2404                                                  "%s" % traceback.format_exc())
2405       
2406        log.debug('Database results for SAML Attribute query user=%r '
2407                  'attribute values=%r' % (userId, attributeValues))
2408       
2409        return attributeValues
2410     
2411    def __getstate__(self):
2412        '''Explicit pickling required with __slots__'''
2413        return dict([(attrName, getattr(self, attrName)) 
2414                      for attrName in SQLAlchemyAttributeInterface.__slots__])
2415       
2416    def __setstate__(self, attrDict):
2417        '''Enable pickling for use with beaker.session'''
2418        for attr, val in attrDict.items():
2419            setattr(self, attr, val)           
2420
2421       
2422   
2423       
Note: See TracBrowser for help on using the repository browser.