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

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@6644
Revision 6644, 96.7 KB checked in by pjkersha, 10 years ago (diff)

Fix to CSV file parsing re pattern.

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