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

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

saml.xml.etree: important fixes to ElementTree based Status element serialisation and de-serialisation
ndg.security.server.attributeauthority: added clockSkew parameter to provide some leeway in SAML attribute query clock checks. Also added StatusMessage? element for additional error info in responses.
ndg.security.common.soap.client: added check of HTTP Content-type in SOAP responses.

  • 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
29from saml.utils import SAMLDateTime
30from saml.saml2.core import Response, Assertion, Attribute, AttributeValue, \
31    AttributeStatement, SAMLVersion, Subject, NameID, Issuer, AttributeQuery, \
32    XSStringAttributeValue, XSGroupRoleAttributeValue, Conditions, Status, \
33    StatusCode, StatusMessage
34   
35from saml.common.xml import SAMLConstants
36from saml.xml.etree import AssertionElementTree, AttributeQueryElementTree, \
37    ResponseElementTree, XSGroupRoleAttributeValueElementTree
38
39from ndg.security.common.utils import TypedList
40from ndg.security.common.utils.classfactory import instantiateClass
41from ndg.security.common.utils.configfileparsers import \
42    CaseSensitiveConfigParser
43   
44# X.509 Certificate handling
45from ndg.security.common.X509 import X509Cert
46
47# NDG Attribute Certificate
48from ndg.security.common.AttCert import AttCert
49
50
51class AttributeAuthorityError(Exception):
52    """Exception handling for NDG Attribute Authority class."""
53    def __init__(self, msg):
54        log.error(msg)
55        Exception.__init__(self, msg)
56
57
58class AttributeAuthorityConfigError(Exception):
59    """NDG Attribute Authority error with configuration. e.g. properties file
60    directory permissions or role mapping file"""
61    def __init__(self, msg):
62        log.error(msg)
63        Exception.__init__(self, msg)
64       
65       
66class AttributeAuthorityAccessDenied(AttributeAuthorityError):
67    """NDG Attribute Authority - access denied exception.
68
69    Raise from getAttCert method where no roles are available for the user
70    but that the request is otherwise valid.  In all other error cases raise
71    AttributeAuthorityError"""   
72   
73   
74class AttributeAuthorityNoTrustedHosts(AttributeAuthorityError):
75    """Raise from getTrustedHosts if there are no trusted hosts defined in
76    the map configuration"""
77
78
79class AttributeAuthorityNoMatchingRoleInTrustedHosts(AttributeAuthorityError):
80    """Raise from getTrustedHosts if there is no mapping to any of the
81    trusted hosts for the given input role name"""
82
83
84class AttributeAuthority(object):
85    """NDG Attribute Authority - service for allocation of user authorization
86    tokens - attribute certificates.
87   
88    @type propertyDefaults: dict
89    @cvar propertyDefaults: valid configuration property keywords
90   
91    @type attributeInterfacePropertyDefaults: dict
92    @cvar attributeInterfacePropertyDefaults: valid configuration property
93    keywords for the Attribute Interface plugin
94   
95    @type mapConfigHostDefaults: dict
96    @cvar mapConfigHostDefaults: valid configuration property
97    keywords for the Map Configuration XML Host element
98   
99    @type DEFAULT_CONFIG_DIRNAME: string
100    @cvar DEFAULT_CONFIG_DIRNAME: configuration directory under $NDGSEC_DIR -
101    default location for properties file
102   
103    @type DEFAULT_PROPERTY_FILENAME: string
104    @cvar DEFAULT_PROPERTY_FILENAME: default file name for properties file
105    under DEFAULT_CONFIG_DIRNAME
106   
107    @type ATTRIBUTE_INTERFACE_KEYNAME: basestring
108    @param ATTRIBUTE_INTERFACE_KEYNAME: attribute interface parameters key
109    name - see initAttributeInterface for details
110    """
111
112    # Code designed from NERC Data Grid Enterprise and Information Viewpoint
113    # documents.
114    #
115    # Also, draws from Neil Bennett's ACServer class used in the Java
116    # implementation of NDG Security
117
118    DEFAULT_CONFIG_DIRNAME = "conf"
119    DEFAULT_PROPERTY_FILENAME = "attributeAuthority.cfg"
120    ATTRIBUTE_INTERFACE_KEYNAME = 'attributeInterface'
121    CONFIG_LIST_SEP_PAT = re.compile(',\W*')
122   
123    attributeInterfacePropertyDefaults = {
124        'modFilePath':  '',
125        'modName':      '',
126        'className':    ''
127    }
128   
129    # valid configuration property keywords with accepted default values. 
130    # Values set to not NotImplemented here denote keys which must be specified
131    # in the config
132    propertyDefaults = { 
133        'name':                         '',
134        'signingCertFilePath':          '',
135        'signingPriKeyFilePath':        '',
136        'signingPriKeyPwd':             None,
137        'caCertFilePathList':           [],
138        'attCertLifetime':              -1,
139        'attCertNotBeforeOff':          0.,
140        'clockSkew':                    0.,
141        'attCertFileName':              '',
142        'attCertFileLogCnt':            0,
143        'mapConfigFilePath':            '',
144        'attCertDir':                   '',
145        'dnSeparator':                  '/',
146        ATTRIBUTE_INTERFACE_KEYNAME:    attributeInterfacePropertyDefaults
147    }
148   
149    mapConfigHostDefaults = {
150        'siteName':                 None,
151        'aaURI':                    NotImplemented,
152        'aaDN':                     NotImplemented,
153        'loginURI':                 NotImplemented,
154        'loginServerDN':            NotImplemented,
155        'loginRequestServerDN':     NotImplemented
156    }
157
158    def __init__(self):
159        """Create new Attribute Authority instance"""
160        log.info("Initialising service ...")
161       
162        # Initial config file property based attributes
163        for name, val in AttributeAuthority.propertyDefaults.items():
164            setattr(self, '_AttributeAuthority__%s' % name, val)
165       
166        self.__caCertFilePathList = TypedList(basestring)
167       
168        self.__propFilePath = None       
169        self.__propFileSection = 'DEFAULT'
170        self.__propPrefix = ''
171       
172        # Initialise role mapping look-ups - These are set in readMapConfig()
173        self.__mapConfig = None
174        self.__localRole2RemoteRole = None
175        self.__remoteRole2LocalRole = None
176       
177        self.__cert = None
178       
179        # Issuer details - serialise using the separator string set in the
180        # properties file
181        self.__issuer = None
182        self.__issuerSerialNumber = None
183        self.__attCertLog = None
184        self.__name = None
185       
186        self.__attributeInterfaceCfg = {}
187
188    def _getMapConfig(self):
189        return self.__mapConfig
190
191    def _getCert(self):
192        return self.__cert
193
194    def _getIssuer(self):
195        return self.__issuer
196
197    def _getIssuerSerialNumber(self):
198        return self.__issuerSerialNumber
199
200    def _getAttCertLog(self):
201        return self.__attCertLog
202
203    def _getName(self):
204        return self.__name
205
206    def _getAttCertLifetime(self):
207        return self.__attCertLifetime
208
209    def _getAttCertNotBeforeOff(self):
210        return self.__attCertNotBeforeOff
211
212    def _getClockSkew(self):
213        return self.__clockSkew
214
215    def _getAttCertDir(self):
216        return self.__attCertDir
217
218    def _getAttributeInterface(self):
219        return self.__attributeInterface
220
221    def _getTrustedHostInfo(self):
222        return self.__trustedHostInfo
223
224    def _setCert(self, value):
225        if not isinstance(value, X509Cert):
226            raise TypeError('Expecting %r type for "cert"; got %r' %
227                            (X509Cert, type(value)))
228           
229        self.__cert = value
230
231    def _setIssuer(self, value):
232        self.__issuer = value
233
234    def _setIssuerSerialNumber(self, value):
235        if not isinstance(value, (long, int)):
236            raise TypeError('Expecting long or int type for "name"; got %r' %
237                            type(value))
238        self.__issuerSerialNumber = value
239
240    def _setAttCertLog(self, value):
241        if not isinstance(value, AttCertLog):
242            raise TypeError('Expecting %r type for "attCertLog"; got %r' %
243                            (AttCertLog, type(value)))
244        self.__attCertLog = value
245
246    def _setName(self, value):
247        if not isinstance(value, basestring):
248            raise TypeError('Expecting string type for "name"; got %r' %
249                            type(value))
250        self.__name = value
251
252    def _setAttCertLifetime(self, value):
253        if isinstance(value, float):
254            self.__attCertLifetime = value
255           
256        elif isinstance(value, (basestring, int, long)):
257            self.__attCertLifetime = float(value)
258        else:
259            raise TypeError('Expecting float, int, long or string type for '
260                            '"attCertLifetime"; got %r' % type(value))
261
262    def _setAttCertNotBeforeOff(self, value):
263        if isinstance(value, float):
264            self.__attCertNotBeforeOff = value
265           
266        elif isinstance(value, (basestring, int, long)):
267            self.__attCertNotBeforeOff = float(value)
268        else:
269            raise TypeError('Expecting float, int, long or string type for '
270                            '"attCertNotBeforeOff"; got %r' % type(value))
271
272    def _setClockSkew(self, value):
273        if isinstance(value, (float, int, long)):
274            self.__clockSkew = timedelta(seconds=value)
275           
276        elif isinstance(value, basestring):
277            self.__clockSkew = timedelta(seconds=float(value))
278        else:
279            raise TypeError('Expecting float, int, long or string type for '
280                            '"clockSkew"; got %r' % type(value))
281
282    def _setAttCertDir(self, value):
283        if not isinstance(value, basestring):
284            raise TypeError('Expecting string type for "attCertDir"; got %r' % 
285                            type(value))
286
287        # Check directory path
288        try:
289            dirList = os.listdir(value)
290
291        except OSError, osError:
292            raise AttributeAuthorityConfigError('Invalid directory path for '
293                                                'Attribute Certificates store '
294                                                '"%s": %s' % 
295                                                (value, osError.strerror))
296        self.__attCertDir = value
297
298    def _setTrustedHostInfo(self, value):
299        self.__trustedHostInfo = value
300
301    def _get_caCertFilePathList(self):
302        return self.__caCertFilePathList
303
304    def _set_caCertFilePathList(self, val):
305        if not isinstance(val, (list, tuple)):
306            raise TypeError('Expecting list or tuple type for '
307                            '"caCertFilePathList"; got %r' % type(val))
308           
309        # Overwrite any original settings
310        self.__caCertFilePathList = TypedList(basestring)
311       
312        # Update with new items
313        self.__caCertFilePathList += val
314   
315    caCertFilePathList = property(fget=_get_caCertFilePathList,
316                                  fset=_set_caCertFilePathList,
317                                  doc="list of file paths for CA certificates "
318                                      "used to validate an Attribute "
319                                      "Certificate")
320   
321    def _get_signingCertFilePath(self):
322        return self.__signingCertFilePath
323   
324    def _set_signingCertFilePath(self, value):
325        if not isinstance(value, basestring):
326            raise TypeError('Expecting string type for "signingCertFilePath"; '
327                            'got %r' % type(value))
328        self.__signingCertFilePath = value
329         
330    signingCertFilePath = property(fget=_get_signingCertFilePath, 
331                                   fset=_set_signingCertFilePath,
332                                   doc="X.509 certificate used for Attribute "
333                                       "certificate signature")
334   
335    def _get_signingPriKeyFilePath(self):
336        return self.__signingPriKeyFilePath
337   
338    def _set_signingPriKeyFilePath(self, value):
339        if not isinstance(value, basestring):
340            raise TypeError('Expecting string type for '
341                            '"signingPriKeyFilePath"; got %r' % type(value))
342        self.__signingPriKeyFilePath = value
343         
344    signingPriKeyFilePath = property(fget=_get_signingPriKeyFilePath, 
345                                     fset=_set_signingPriKeyFilePath,
346                                     doc="File Path for private key used to "
347                                         "sign Attribute certificate")
348   
349    def _get_signingPriKeyPwd(self):
350        return self.__signingPriKeyPwd
351   
352    def _set_signingPriKeyPwd(self, value):
353        if not isinstance(value, (type(None), basestring)):
354            raise TypeError('Expecting string or None type for '
355                            '"signingPriKeyPwd"; got %r' % type(value))
356        self.__signingPriKeyPwd = value
357         
358    signingPriKeyPwd = property(fget=_get_signingPriKeyPwd, 
359                                fset=_set_signingPriKeyPwd,
360                                doc="Password for private key file used to "
361                                    "for Attribute certificate signature")
362
363    def _get_attributeInterfaceCfg(self):
364        return self.__attributeInterfaceCfg
365   
366    attributeInterfaceCfg = property(fget=_get_attributeInterfaceCfg,
367                                     doc="Settings for Attribute Interface "
368                                         "initialisation")
369   
370    def _get_attCertFileName(self):
371        return self.__attCertFileName
372   
373    def _set_attCertFileName(self, value):
374        if not isinstance(value, basestring):
375            raise TypeError('Expecting string type for "attCertFileName"; got '
376                            '%r' % type(value))
377           
378        self.__attCertFileName = value
379         
380    attCertFileName = property(fget=_get_attCertFileName, 
381                                fset=_set_attCertFileName,
382                                doc="Attribute certificate file name for log "
383                                    "initialisation")
384   
385    def _get_attCertFileLogCnt(self):
386        return self.__attCertFileLogCnt
387   
388    def _set_attCertFileLogCnt(self, value):
389        if isinstance(value, int):
390            self.__attCertFileLogCnt = value
391        elif isinstance(value, basestring):
392            self.__attCertFileLogCnt = int(value)
393        else:
394            raise TypeError('Expecting int or string type for '
395                            '"attCertFileLogCnt"; got %r' % type(value))
396         
397    attCertFileLogCnt = property(fget=_get_attCertFileLogCnt, 
398                                 fset=_set_attCertFileLogCnt,
399                                 doc="Counter for Attribute Certificate log "
400                                     "rotating file handler")
401   
402    def _get_dnSeparator(self):
403        return self.__dnSeparator
404   
405    def _set_dnSeparator(self, value):
406        if not isinstance(value, basestring):
407            raise TypeError('Expecting string type for "dnSeparator"; got '
408                            '%r' % type(value))
409        self.__dnSeparator = value
410         
411    dnSeparator = property(fget=_get_dnSeparator, 
412                           fset=_set_dnSeparator,
413                           doc="Distinguished Name separator character used "
414                               "with X.509 Certificate issuer certificate")
415           
416    def _getMapConfigFilePath(self):
417        return self.__mapConfigFilePath
418   
419    def _setMapConfigFilePath(self, val):
420        if not isinstance(val, basestring):
421            raise AttributeAuthorityConfigError("Input Map Configuration "
422                                                "file path must be a "
423                                                "valid string.")
424        self.__mapConfigFilePath = val
425         
426    mapConfigFilePath = property(fget=_getMapConfigFilePath,
427                                 fset=_setMapConfigFilePath,
428                                 doc="File path for Role Mapping "
429                                     "configuration") 
430
431    def setPropFilePath(self, val=None):
432        """Set properties file from input or based on environment variable
433        settings
434       
435        @type val: basestring
436        @param val: properties file path"""
437        log.debug("Setting property file path")
438        if not val:
439            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
440                val = os.environ['NDGSEC_AA_PROPFILEPATH']
441               
442            elif 'NDGSEC_DIR' in os.environ:
443                val = os.path.join(os.environ['NDGSEC_DIR'], 
444                                   AttributeAuthority.DEFAULT_CONFIG_DIRNAME,
445                                   AttributeAuthority.DEFAULT_PROPERTY_FILENAME)
446            else:
447                raise AttributeError('Unable to set default Attribute '
448                                     'Authority properties file path: neither '
449                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
450                                     ' environment variables are set')
451               
452        if not isinstance(val, basestring):
453            raise AttributeError("Input Properties file path "
454                                 "must be a valid string.")
455     
456        self.__propFilePath = os.path.expandvars(val)
457        log.debug("Path set to: %s" % val)
458       
459    def getPropFilePath(self):
460        '''Get the properties file path
461       
462        @rtype: basestring
463        @return: properties file path'''
464        return self.__propFilePath
465       
466    # Also set up as a property
467    propFilePath = property(fset=setPropFilePath,
468                            fget=getPropFilePath,
469                            doc="path to file containing Attribute Authority "
470                                "configuration parameters.  It defaults to "
471                                "$NDGSEC_AA_PROPFILEPATH or if not set, "
472                                "$NDGSEC_DIR/conf/attributeAuthority.cfg")   
473   
474    def setPropFileSection(self, val=None):
475        """Set section name to read properties from ini file.  This is set from
476        input or based on environment variable setting
477        NDGSEC_AA_PROPFILESECTION
478       
479        @type val: basestring
480        @param val: section name"""
481        if not val:
482            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
483               
484        if not isinstance(val, basestring):
485            raise AttributeError("Input Properties file section name "
486                                 "must be a valid string.")
487     
488        self.__propFileSection = val
489        log.debug("Properties file section set to: \"%s\"" % val)
490       
491    def getPropFileSection(self):
492        '''Get the section name to extract properties from an ini file -
493        DOES NOT apply to XML file properties
494       
495        @rtype: basestring
496        @return: section name'''
497        return self.__propFileSection
498       
499    # Also set up as a property
500    propFileSection = property(fset=setPropFileSection,
501                               fget=getPropFileSection,
502                               doc="Set the file section name for ini file "
503                                   "properties")   
504   
505    def setPropPrefix(self, val=None):
506        """Set prefix for properties read from ini file.  This is set from
507        input or based on environment variable setting
508        NDGSEC_AA_PROPFILEPREFIX
509       
510        DOES NOT apply to XML file properties
511       
512        @type val: basestring
513        @param val: section name"""
514        log.debug("Setting property file section name")
515        if val is None:
516            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
517               
518        if not isinstance(val, basestring):
519            raise AttributeError("Input Properties file section name "
520                                 "must be a valid string.")
521     
522        self.__propPrefix = val
523        log.debug("Properties file section set to: %s" % val)
524       
525    def getPropPrefix(self):
526        '''Get the prefix name used for properties in an ini file -
527        DOES NOT apply to XML file properties
528       
529        @rtype: basestring
530        @return: section name'''
531        return self.__propPrefix
532   
533       
534    # Also set up as a property
535    propPrefix = property(fset=setPropPrefix,
536                          fget=getPropPrefix,
537                          doc="Set a prefix for ini file properties")   
538   
539    mapConfig = property(fget=_getMapConfig, 
540                         doc="MapConfig object")
541
542    cert = property(fget=_getCert, 
543                    fset=_setCert, 
544                    doc="X.509 Issuer Certificate")
545
546    issuer = property(fget=_getIssuer, 
547                      fset=_setIssuer, 
548                      doc="Issuer name")
549
550    issuerSerialNumber = property(fget=_getIssuerSerialNumber, 
551                                  fset=_setIssuerSerialNumber, 
552                                  doc="Issuer Serial Number")
553
554    attCertLog = property(fget=_getAttCertLog,
555                          fset=_setAttCertLog, 
556                          doc="Attribute certificate logging object")
557
558    name = property(fget=_getName, 
559                    fset=_setName, 
560                    doc="Issuer organisation name")
561
562    attCertLifetime = property(fget=_getAttCertLifetime, 
563                               fset=_setAttCertLifetime, 
564                               doc="Attribute certificate lifetime")
565
566    attCertNotBeforeOff = property(fget=_getAttCertNotBeforeOff, 
567                                   fset=_setAttCertNotBeforeOff, 
568                                   doc="Attribute certificate clock skew in "
569                                       "seconds")
570
571    clockSkew = property(fget=_getClockSkew, 
572                         fset=_setClockSkew, 
573                         doc="Allow a clock skew in seconds for SAML Attribute"
574                             " Query issueInstant parameter check")
575
576    attCertDir = property(fget=_getAttCertDir, 
577                          fset=_setAttCertDir, 
578                          doc="Attribute certificate log directory")
579
580    attributeInterface = property(fget=_getAttributeInterface, 
581                                  doc="Attribute Interface object")
582
583    name = property(fget=_getName, fset=_setName, doc="Organisation Name")
584
585    trustedHostInfo = property(fget=_getTrustedHostInfo, 
586                               fset=_setTrustedHostInfo, 
587                               doc="Dictionary of trusted organisations")
588       
589    @classmethod
590    def fromPropertyFile(cls, propFilePath=None, propFileSection='DEFAULT',
591                         propPrefix='attributeauthority.', 
592                         bReadMapConfig=True):
593        """Create new NDG Attribute Authority instance from the property file
594        settings
595
596        @type propFilePath: string
597        @param propFilePath: path to file containing Attribute Authority
598        configuration parameters.  It defaults to $NDGSEC_AA_PROPFILEPATH or
599        if not set, $NDGSEC_DIR/conf/attributeAuthority.cfg
600        @type propFileSection: basestring
601        @param propFileSection: section of properties file to read from.
602        properties files
603        @type propPrefix: basestring
604        @param propPrefix: set a prefix for filtering attribute authority
605        property names - useful where properties are being parsed from a file
606        section containing parameter names for more than one application
607        @type bReadMapConfig: boolean
608        @param bReadMapConfig: by default the Map Configuration file is
609        read.  Set this flag to False to override.
610        """
611           
612        attributeAuthority = AttributeAuthority()
613        if propFileSection:
614            attributeAuthority.propFileSection = propFileSection
615           
616        if propPrefix:
617            attributeAuthority.propPrefix = propPrefix
618
619        attributeAuthority.propFilePath = propFilePath           
620        attributeAuthority.readProperties()
621        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
622   
623        return attributeAuthority
624
625       
626    @classmethod
627    def fromProperties(cls, propPrefix='attributeauthority.', 
628                       bReadMapConfig=True, **prop):
629        """Create new NDG Attribute Authority instance from input property
630        keywords
631
632        @type propPrefix: basestring
633        @param propPrefix: set a prefix for filtering attribute authority
634        property names - useful where properties are being parsed from a file
635        section containing parameter names for more than one application
636        @type bReadMapConfig: boolean
637        @param bReadMapConfig: by default the Map Configuration file is
638        read.  Set this flag to False to override.
639        """
640        attributeAuthority = AttributeAuthority()
641        if propPrefix:
642            attributeAuthority.propPrefix = propPrefix
643               
644        attributeAuthority.setProperties(**prop)
645        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
646       
647        return attributeAuthority
648   
649    def initialise(self, bReadMapConfig=True):
650        """Convenience method for set up of Attribute Interface, map
651        configuration and PKI"""
652       
653        # Read the Map Configuration file
654        if bReadMapConfig:
655            self.readMapConfig()
656
657        # Instantiate Certificate object
658        log.debug("Reading and checking Attribute Authority X.509 cert. ...")
659        self.cert = X509Cert.Read(self.signingCertFilePath)
660
661        # Check it's valid
662        try:
663            self.cert.isValidTime(raiseExcep=True)
664           
665        except Exception, e:
666            raise AttributeAuthorityError("Attribute Authority's certificate "
667                                          "is invalid: %s" % e)
668       
669        # Check CA certificate
670        log.debug("Reading and checking X.509 CA certificate ...")
671        for caCertFile in self.caCertFilePathList:
672            caCert = X509Cert(caCertFile)
673            caCert.read()
674           
675            try:
676                caCert.isValidTime(raiseExcep=True)
677               
678            except Exception, e:
679                raise AttributeAuthorityError('CA certificate "%s" is '
680                                              'invalid: %s'% (caCert.dn, e))
681       
682        # Issuer details - serialise using the separator string set in the
683        # properties file
684        self.issuer = self.cert.dn.serialise(separator=self.dnSeparator)
685
686        self.issuerSerialNumber = self.cert.serialNumber
687       
688        # Load user - user attribute look-up plugin
689        self.initAttributeInterface()
690       
691        attCertFilePath = os.path.join(self.attCertDir, self.attCertFileName)
692               
693        # Rotating file handler used for logging attribute certificates
694        # issued.
695        self.attCertLog = AttCertLog(attCertFilePath,
696                                     backUpCnt=self.attCertFileLogCnt)
697
698    def setProperties(self, **prop):
699        """Set configuration from an input property dictionary
700        @type prop: dict
701        @param prop: properties dictionary containing configuration items
702        to be set
703        """
704        lenPropPrefix = len(self.propPrefix)
705       
706        # '+ 1' allows for the dot separator
707        lenAttributeInterfacePrefix = len(
708                            AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME) + 1
709       
710        for name, val in prop.items():
711            if name.startswith(self.propPrefix):
712                name = name[lenPropPrefix:]
713           
714            if name.startswith(AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME):
715                name = name[lenAttributeInterfacePrefix:]
716                self.attributeInterfaceCfg[name] = val
717                continue
718           
719            if name not in AttributeAuthority.propertyDefaults:
720                raise AttributeError('Invalid attribute name "%s"' % name)
721           
722            if isinstance(val, basestring):
723                val = os.path.expandvars(val)
724           
725            if isinstance(AttributeAuthority.propertyDefaults[name], list):
726                val = AttributeAuthority.CONFIG_LIST_SEP_PAT.split(val)
727               
728            # This makes an implicit call to the appropriate property method
729            try:
730                setattr(self, name, val)
731            except AttributeError:
732                raise AttributeError("Can't set attribute \"%s\"" % name)         
733           
734    def readProperties(self):
735        '''Read the properties files and do some checking/converting of input
736        values
737        '''
738        if not os.path.isfile(self.propFilePath):
739            raise IOError('Error parsing properties file "%s": No such file' % 
740                          self.propFilePath)
741           
742        defaultItems = {'here': os.path.dirname(self.propFilePath)}
743       
744        cfg = CaseSensitiveConfigParser(defaults=defaultItems)
745        cfg.read(self.propFilePath)
746       
747        cfgItems = dict([(name, val) 
748                         for name, val in cfg.items(self.propFileSection)
749                         if name != 'here'])
750        self.setProperties(**cfgItems)
751
752    def initAttributeInterface(self):
753        '''Load host sites custom user roles interface to enable the AA to
754        # assign roles in an attribute certificate on a getAttCert request'''
755        classProperties = {}
756        classProperties.update(self.attributeInterfaceCfg)
757       
758        modName = classProperties.pop('modName')
759        className = classProperties.pop('className') 
760       
761        # file path may be omitted   
762        modFilePath = classProperties.pop('modFilePath', None) 
763                     
764        self.__attributeInterface = instantiateClass(modName,
765                                             className,
766                                             moduleFilePath=modFilePath,
767                                             objectType=AttributeInterface,
768                                             classProperties=classProperties)
769
770    def getAttCert(self,
771                   userId=None,
772                   holderX509Cert=None,
773                   holderX509CertFilePath=None,
774                   userAttCert=None,
775                   userAttCertFilePath=None):
776
777        """Request a new Attribute Certificate for use in authorisation
778
779        getAttCert([userId=uid][holderX509Cert=x509Cert|
780                    holderX509CertFilePath=x509CertFile, ]
781                   [userAttCert=cert|userAttCertFilePath=certFile])
782         
783        @type userId: string
784        @param userId: identifier for the user who is entitled to the roles
785        in the certificate that is issued.  If this keyword is omitted, then
786        the userId will be set to the DN of the holder.
787       
788        holder = the holder of the certificate - an inidividual user or an
789        organisation to which the user belongs who vouches for that user's ID
790       
791        userId = the identifier for the user who is entitled to the roles
792        specified in the Attribute Certificate that is issued.
793                 
794        @type holderX509Cert: string / ndg.security.common.X509.X509Cert type
795        @param holderX509Cert: base64 encoded string containing proxy cert./
796        X.509 cert object corresponding to the ID who will be the HOLDER of
797        the Attribute Certificate that will be issued.  - Normally, using
798        proxy certificates, the holder and user ID are the same but there
799        may be cases where the holder will be an organisation ID.  This is the
800        case for NDG security with the DEWS project
801       
802        @param holderX509CertFilePath: string
803        @param holderX509CertFilePath: file path to proxy/X.509 certificate of
804        candidate holder
805     
806        @type userAttCert: string or AttCert type
807        @param userAttCert: externally provided attribute certificate from
808        another data centre.  This is only necessary if the user is not
809        registered with this attribute authority.
810                       
811        @type userAttCertFilePath: string
812        @param userAttCertFilePath: alternative to userAttCert except pass
813        in as a file path to an attribute certificate instead.
814       
815        @rtype: AttCert
816        @return: new attribute certificate"""
817
818        log.debug("Calling getAttCert ...")
819       
820        # Read candidate Attribute Certificate holder's X.509 certificate
821        try:
822            if holderX509CertFilePath is not None:
823                                   
824                # Certificate input as a file
825                holderX509Cert = X509Cert()
826                holderX509Cert.read(holderX509CertFilePath)
827               
828            elif isinstance(holderX509Cert, basestring):
829
830                # Certificate input as string text
831                holderX509Cert = X509Cert.Parse(holderX509Cert)
832               
833            elif not isinstance(holderX509Cert, (X509Cert, None.__class__)):
834                raise AttributeAuthorityError("Holder X.509 Certificate must "
835                                              "be set to valid type: a file "
836                                              "path, string, X509 object or "
837                                              "None")           
838        except Exception, e:
839            log.error("Holder X.509 certificate: %s" % e)
840            raise
841
842
843        # Check certificate hasn't expired
844        if holderX509Cert:
845            log.debug("Checking candidate holder X.509 certificate ...")
846            try:
847                holderX509Cert.isValidTime(raiseExcep=True)
848               
849            except Exception, e:
850                log.error("User X.509 certificate is invalid: " + e)
851                raise
852
853           
854        # If no user ID is input, set id from holder X.509 certificate DN
855        # instead
856        if not userId:
857            if not holderX509Cert:
858                raise AttributeAuthorityError("If no user ID is set a holder "
859                                              "X.509 certificate must be "
860                                              "present")
861            try:
862                userId = holderX509Cert.dn.serialise(\
863                                         separator=self.dnSeparator) 
864            except Exception, e:
865                log.error("Setting user Id from holder certificate DN: %s" % e)
866                raise
867       
868        # Make a new Attribute Certificate instance passing in certificate
869        # details for later signing
870        attCert = AttCert()
871
872        # First certificate in list contains the public key corresponding to
873        # the private key
874        attCert.certFilePathList = [self.signingCertFilePath] + \
875                                                                self.caCertFilePathList
876             
877        # Check for expiry of each certificate                   
878        for x509Cert in attCert.certFilePathList:
879            X509Cert.Read(x509Cert).isValidTime(raiseExcep=True)
880                                                               
881        attCert.signingKeyFilePath = self.signingPriKeyFilePath
882        attCert.signingKeyPwd = self.signingPriKeyPwd
883       
884       
885        # Set holder's Distinguished Name if a holder X.509 certificate was
886        # input
887        if holderX509Cert:
888            try:
889                attCert['holder'] = holderX509Cert.dn.serialise(
890                                        separator=self.dnSeparator)           
891            except Exception, e:
892                 log.error("Holder X.509 Certificate DN: %s" % e)
893                 raise
894           
895        # Set Issuer details from Attribute Authority
896        issuerDN = self.cert.dn
897        try:
898            attCert['issuer'] = \
899                    issuerDN.serialise(separator=self.dnSeparator)           
900        except Exception, e:
901            log.error("Issuer X.509 Certificate DN: %s" % e)
902            raise 
903           
904        attCert['issuerName'] = self.name
905        attCert['issuerSerialNumber'] = self.issuerSerialNumber
906
907        attCert['userId'] = userId
908       
909        # Set validity time
910        try:
911            attCert.setValidityTime(
912                        lifetime=self.attCertLifetime,
913                        notBeforeOffset=self.attCertNotBeforeOff)
914
915            # Check against the holder X.509 certificate's expiry if set
916            if holderX509Cert:
917                dtHolderCertNotAfter = holderX509Cert.notAfter
918               
919                if attCert.getValidityNotAfter(asDatetime=True) > \
920                   dtHolderCertNotAfter:
921   
922                    # Adjust the attribute certificate's expiry date time
923                    # so that it agrees with that of the certificate
924                    # ... but also make ensure that the not before skew is
925                    # still applied
926                    attCert.setValidityTime(dtNotAfter=dtHolderCertNotAfter,
927                            notBeforeOffset=self.attCertNotBeforeOff)
928           
929        except Exception, e:
930            log.error("Error setting attribute certificate validity time: %s" %
931                      e)
932            raise 
933
934        # Check name is registered with this Attribute Authority - if no
935        # user roles are found, the user is not registered
936        userRoles = self.getRoles(userId)
937        if userRoles:
938            # Set as an Original Certificate
939            #
940            # User roles found - user is registered with this data centre
941            # Add roles for this user for this data centre
942            attCert.addRoles(userRoles)
943
944            # Mark new Attribute Certificate as an original
945            attCert['provenance'] = AttCert.origProvenance
946
947        else:           
948            # Set as a Mapped Certificate
949            #
950            # No roles found - user is not registered with this data centre
951            # Check for an externally provided certificate from another
952            # trusted data centre
953            if userAttCertFilePath:
954               
955                # Read externally provided certificate
956                try:
957                    userAttCert = AttCert.Read(userAttCertFilePath)
958                   
959                except Exception, e:
960                    raise AttributeAuthorityError("Reading external Attribute "
961                                                  "Certificate: %s" % e)                           
962            elif userAttCert:
963                # Allow input as a string but convert to
964                if isinstance(userAttCert, basestring):
965                    userAttCert = AttCert.Parse(userAttCert)
966                   
967                elif not isinstance(userAttCert, AttCert):
968                    raise AttributeAuthorityError(
969                        "Expecting userAttCert as a string or AttCert type")       
970            else:
971                raise AttributeAuthorityAccessDenied('User "%s" is not '
972                    'registered and no external attribute certificate is '
973                    'available to make a mapping.' % userId)
974
975
976            # Check it's an original certificate - mapped certificates can't
977            # be used to make further mappings
978            if userAttCert.isMapped():
979                raise AttributeAuthorityError("External Attribute Certificate "
980                                              "must have an original "
981                                              "provenance in order "
982                                              "to make further mappings.")
983
984
985            # Check it's valid and signed
986            try:
987                # Give path to CA cert to allow check
988                userAttCert.certFilePathList = self.caCertFilePathList
989                userAttCert.isValid(raiseExcep=True)
990               
991            except Exception, e:
992                raise AttributeAuthorityError("Invalid Remote Attribute "
993                                        "Certificate: " + str(e))       
994
995
996            # Check that's it's holder matches the candidate holder
997            # certificate DN
998            if holderX509Cert and userAttCert.holderDN != holderX509Cert.dn:
999                raise AttributeAuthorityError("User certificate and Attribute "
1000                                        'Certificate DNs don\'t match: "%s"'
1001                                        ' and "%s"' % (holderX509Cert.dn, 
1002                                                       userAttCert.holderDN))
1003           
1004 
1005            # Get roles from external Attribute Certificate
1006            trustedHostRoles = userAttCert.roles
1007
1008
1009            # Map external roles to local ones
1010            localRoles = self.mapRemoteRoles2LocalRoles(
1011                                                    userAttCert['issuerName'],
1012                                                    trustedHostRoles)
1013            if not localRoles:
1014                raise AttributeAuthorityAccessDenied("No local roles mapped "
1015                                               "to the %s roles: %s" % 
1016                                               (userAttCert['issuerName'], 
1017                                                ', '.join(trustedHostRoles)))
1018
1019            attCert.addRoles(localRoles)
1020           
1021           
1022            # Mark new Attribute Certificate as mapped
1023            attCert.provenance = AttCert.mappedProvenance
1024
1025            # Copy the user Id from the external AC
1026            attCert.userId = userAttCert.userId
1027           
1028            # End set mapped certificate block
1029
1030        try:
1031            # Digitally sign certificate using Attribute Authority's
1032            # certificate and private key
1033            attCert.applyEnvelopedSignature()
1034           
1035            # Check the certificate is valid
1036            attCert.isValid(raiseExcep=True)
1037           
1038            # Write out certificate to keep a record of it for auditing
1039            #attCert.write()
1040            self.__attCertLog.info(attCert)
1041           
1042            log.info('Issued an Attribute Certificate to "%s" with roles: '
1043                     '"%s"' % (userId, '", "'.join(attCert.roles)))
1044
1045            # Return the cert to caller
1046            return attCert
1047       
1048        except Exception, e:
1049            raise AttributeAuthorityError('New Attribute Certificate "%s": %s'%
1050                                          (attCert.filePath, e))
1051
1052    def samlAttributeQuery(self, attributeQuery):
1053        """Respond to SAML 2.0 Attribute Query
1054        """
1055        if not isinstance(attributeQuery, AttributeQuery):
1056            raise TypeError('Expecting %r for attribute query; got %r' %
1057                            (AttributeQuery, type(attributeQuery)))
1058           
1059        samlResponse = Response()
1060       
1061        samlResponse.issueInstant = datetime.utcnow()
1062        if self.attCertNotBeforeOff != 0:
1063            samlResponse.issueInstant += timedelta(
1064                                            seconds=self.attCertNotBeforeOff)
1065           
1066        samlResponse.id = str(uuid4())
1067        samlResponse.issuer = Issuer()
1068       
1069        # Initialise to success status but reset on error
1070        samlResponse.status = Status()
1071        samlResponse.status.statusCode = StatusCode()
1072        samlResponse.status.statusMessage = StatusMessage()
1073        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI
1074       
1075        # Nb. SAML 2.0 spec says issuer format must be omitted
1076        samlResponse.issuer.value = self.issuer
1077       
1078        samlResponse.inResponseTo = attributeQuery.id
1079       
1080        # Attribute Query validation ...
1081        utcNow = datetime.utcnow()
1082        if attributeQuery.issueInstant >= utcNow + self.clockSkew:
1083            msg = ('SAML Attribute Query issueInstant [%s] is at or after '
1084                   'the current clock time [%s]') % \
1085                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow))
1086            log.error(msg)
1087                     
1088            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1089            samlResponse.status.statusMessage = StatusMessage()
1090            samlResponse.status.statusMessage.value = msg
1091            return samlResponse
1092           
1093        elif attributeQuery.version < SAMLVersion.VERSION_20:
1094            samlResponse.status.statusCode.value = \
1095                                        StatusCode.REQUEST_VERSION_TOO_LOW_URI
1096            return samlResponse
1097       
1098        elif attributeQuery.version > SAMLVersion.VERSION_20:
1099            samlResponse.status.statusCode.value = \
1100                                        StatusCode.REQUEST_VERSION_TOO_HIGH_URI
1101            return samlResponse
1102       
1103        elif attributeQuery.subject.nameID.format != "urn:esg:openid":
1104            log.error('SAML Attribute Query subject format is "%r"; expecting '
1105                      '"%s"' % (attributeQuery.subject.nameID.format,
1106                                "urn:esg:openid"))
1107            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1108            samlResponse.status.statusMessage.value = \
1109                                "Subject Name ID format is not recognised"
1110            return samlResponse
1111       
1112        elif attributeQuery.issuer.format != "urn:esg:issuer":
1113            log.error('SAML Attribute Query issuer format is "%r"; expecting '
1114                      '"%s"' % (attributeQuery.issuer.format,
1115                                "urn:esg:issuer"))
1116            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1117            samlResponse.status.statusMessage.value = \
1118                                            "Issuer format is not recognised"
1119            return samlResponse
1120       
1121        try:
1122            # Return a dictionary of name, value pairs
1123            self.attributeInterface.getAttributes(attributeQuery, samlResponse)
1124           
1125        except InvalidUserId, e:
1126            log.exception(e)
1127            samlResponse.status.statusCode.value = \
1128                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1129            return samlResponse
1130           
1131        except UserIdNotKnown, e:
1132            log.exception(e)
1133            samlResponse.status.statusCode.value = \
1134                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1135            samlResponse.status.statusMessage.value = str(e)
1136            return samlResponse
1137           
1138        except InvalidRequestorId, e:
1139            log.exception(e)
1140            samlResponse.status.statusCode.value = StatusCode.REQUEST_DENIED_URI
1141            samlResponse.status.statusMessage.value = str(e)
1142            return samlResponse
1143           
1144        except AttributeReleaseDenied, e:
1145            log.exception(e)
1146            samlResponse.status.statusCode.value = \
1147                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1148            samlResponse.status.statusMessage.value = str(e)
1149            return samlResponse
1150           
1151        except AttributeNotKnownError, e:
1152            log.exception(e)
1153            samlResponse.status.statusCode.value = \
1154                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1155            samlResponse.status.statusMessage.value = str(e)
1156            return samlResponse
1157           
1158        except Exception, e:
1159            log.exception("Unexpected error calling Attribute Interface "
1160                          "for subject [%s] and query issuer [%s]" %
1161                          (attributeQuery.subject.nameID.value,
1162                           attributeQuery.issuer.value))
1163           
1164            # SAML spec says application server should set a HTTP 500 Internal
1165            # Server error in this case
1166            raise 
1167
1168        return samlResponse
1169   
1170    def readMapConfig(self):
1171        """Parse Map Configuration file.
1172        """
1173       
1174        log.debug("Reading map configuration file ...")
1175       
1176        try:
1177            tree = ElementTree.parse(self.mapConfigFilePath)
1178            rootElem = tree.getroot()
1179           
1180        except IOError, e:
1181            raise AttributeAuthorityConfigError('Error parsing Map '
1182                                                'Configuration file "%s": %s' % 
1183                                                (e.filename, e.strerror))         
1184        except Exception, e:
1185            raise AttributeAuthorityConfigError('Error parsing Map '
1186                                                'Configuration file: "%s": %s'% 
1187                                                (self.mapConfigFilePath, e))
1188       
1189        trustedElem = rootElem.findall('trusted')
1190        if not trustedElem: 
1191            # Make an empty list so that for loop block below is skipped
1192            # without an error 
1193            trustedElem = ()
1194
1195        # Dictionaries:
1196        # 1) to hold all the data
1197        self.__mapConfig = {'thisHost': {}, 'trustedHosts': {}}
1198
1199        # ... look-up
1200        # 2) hosts corresponding to a given role and
1201        # 3) roles of external data centre to this data centre
1202        self.__localRole2TrustedHost = {}
1203        self.__localRole2RemoteRole = {}
1204        self.__remoteRole2LocalRole = {}
1205
1206        # Information about this host
1207        try:
1208            thisHostElem = rootElem.findall('thisHost')[0]
1209           
1210        except Exception, e:
1211            raise AttributeAuthorityConfigError('"thisHost" tag not found in '
1212                                                'Map Configuration file "%s"' % 
1213                                                self.mapConfigFilePath)
1214
1215        try:
1216            hostName = thisHostElem.attrib.values()[0]
1217           
1218        except Exception, e:
1219            raise AttributeAuthorityConfigError('"name" attribute of '
1220                                                '"thisHost" element not found '
1221                                                'in Map Configuration file '
1222                                                '"%s"' % 
1223                                                self.mapConfigFilePath)
1224
1225        # hostname is also stored in the AA's config file in the 'name' tag. 
1226        # Check the two match as the latter is copied into Attribute
1227        # Certificates issued by this AA
1228        #
1229        # TODO: would be better to rationalise this so that the hostname is
1230        # stored in one place only.
1231        #
1232        # P J Kershaw 14/06/06
1233        if hostName != self.name:
1234            raise AttributeAuthorityError('"name" attribute of "thisHost" '
1235                                          'element in Map Configuration file '
1236                                          'doesn\'t match "name" element in '
1237                                          'properties file.')
1238       
1239        # Information for THIS Attribute Authority
1240        self.__mapConfig['thisHost'][hostName] = {}
1241
1242        for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1243            val = thisHostElem.findtext(k)
1244            if val is None and v == NotImplemented:
1245                raise AttributeAuthorityConfigError('<thisHost> option <%s> '
1246                                                    'must be set.' % k)
1247            self.__mapConfig['thisHost'][hostName][k] = val     
1248       
1249        # Information about trusted hosts
1250        for elem in trustedElem:
1251            try:
1252                trustedHost = elem.attrib.values()[0]
1253               
1254            except Exception, e:
1255                raise AttributeAuthorityConfigError('Error reading trusted '
1256                                                    'host name: %s' % e)
1257 
1258            # Add signatureFile and list of roles
1259            #
1260            # (Currently Optional) additional tag allows query of the URI
1261            # where a user would normally login at the trusted host.  Added
1262            # this feature to allow users to be forwarded to their home site
1263            # if they are accessing a secure resource and are not
1264            # authenticated
1265            #
1266            # P J Kershaw 25/05/06
1267            self.__mapConfig['trustedHosts'][trustedHost] = {}
1268            for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1269                val = thisHostElem.findtext(k)
1270                if val is None and v == NotImplemented:
1271                    raise AttributeAuthorityConfigError('<trustedHost> option '
1272                                                        '<%s> must be set.'%k)
1273                   
1274                self.__mapConfig['trustedHosts'][trustedHost][k] = \
1275                                                        elem.findtext(k)   
1276
1277            roleElem = elem.findall('role')
1278            if roleElem:
1279                # Role keyword value requires special parsing before
1280                # assignment
1281                self.__mapConfig['trustedHosts'][trustedHost]['role'] = \
1282                                        [dict(i.items()) for i in roleElem]
1283            else:
1284                # It's possible for trust relationships to not contain any
1285                # role mapping.  e.g. a site's login service trusting other
1286                # sites login requests
1287                self.__mapConfig['trustedHosts'][trustedHost]['role'] = []
1288                       
1289            self.__localRole2RemoteRole[trustedHost] = {}
1290            self.__remoteRole2LocalRole[trustedHost] = {}
1291           
1292            for role in self.__mapConfig['trustedHosts'][trustedHost]['role']:
1293                try:
1294                    localRole = role['local']
1295                    remoteRole = role['remote']
1296                except KeyError, e:
1297                    raise AttributeAuthorityError('Reading map configuration '
1298                                                  ' file "%s": no element '
1299                                                  '"%s" for host "%s"' % 
1300                                                (self.mapConfigFilePath, 
1301                                                 e, 
1302                                                 trustedHost))
1303                   
1304                # Role to host look-up
1305                if localRole in self.__localRole2TrustedHost:
1306                   
1307                    if trustedHost not in \
1308                       self.__localRole2TrustedHost[localRole]:
1309                        self.__localRole2TrustedHost[localRole].\
1310                                                        append(trustedHost)                       
1311                else:
1312                    self.__localRole2TrustedHost[localRole] = [trustedHost]
1313
1314
1315                # Trusted Host to local role and trusted host to trusted role
1316                # map look-ups
1317                try:
1318                    self.__remoteRole2LocalRole[trustedHost][remoteRole].\
1319                                                            append(localRole)                 
1320                except KeyError:
1321                    self.__remoteRole2LocalRole[trustedHost][remoteRole] = \
1322                                                                [localRole]
1323                   
1324                try:
1325                    self.__localRole2RemoteRole[trustedHost][localRole].\
1326                                                            append(remoteRole)                 
1327                except KeyError:
1328                    self.__localRole2RemoteRole[trustedHost][localRole] = \
1329                                                                [remoteRole]
1330       
1331        # Store trusted host info look-up for retrieval by getTrustedHostInfo
1332        # method                                                                         
1333        #
1334        # Nb. {}.fromkeys([...]).keys() is a fudge to get unique elements
1335        # from a list i.e. convert the list elements to a dict eliminating
1336        # duplicated elements and convert the keys back into a list.
1337        self._trustedHostInfo = dict(
1338        [
1339            (
1340                k, 
1341                {
1342                    'siteName':             v['siteName'],
1343                    'aaURI':                v['aaURI'], 
1344                    'aaDN':                 v['aaDN'], 
1345                    'loginURI':             v['loginURI'], 
1346                    'loginServerDN':        v['loginServerDN'], 
1347                    'loginRequestServerDN': v['loginRequestServerDN'], 
1348                    'role':                 {}.fromkeys([role['remote'] 
1349                                                         for role in v['role']]
1350                                                       ).keys()
1351                }
1352            ) for k, v in self.__mapConfig['trustedHosts'].items()
1353        ])
1354
1355        log.info('Loaded map configuration file "%s"' % self.mapConfigFilePath)
1356       
1357       
1358    def getRoles(self, userId):
1359        """Get the roles available to the registered user identified userId.
1360
1361        @type dn: string
1362        @param dn: user identifier - could be a X500 Distinguished Name
1363        @return: list of roles for the given user ID"""
1364
1365        log.debug('Calling getRoles for user "%s" ...' % userId)
1366       
1367        # Call to AttributeInterface derived class.  Each Attribute Authority
1368        # should define it's own roles class derived from AttributeInterface to
1369        # define how roles are accessed
1370        try:
1371            return self.__attributeInterface.getRoles(userId)
1372
1373        except Exception, e:
1374            raise AttributeAuthorityError("Getting user roles: %s" % e)
1375       
1376       
1377    def _getHostInfo(self):
1378        """Return the host that this Attribute Authority represents: its ID,
1379        the user login URI and WSDL address.  Call this method via the
1380        'hostInfo' property
1381       
1382        @rtype: dict
1383        @return: dictionary of host information derived from the map
1384        configuration"""
1385       
1386        return self.__mapConfig['thisHost']
1387       
1388    hostInfo = property(fget=_getHostInfo, 
1389                        doc="Return information about this host")
1390       
1391       
1392    def getTrustedHostInfo(self, role=None):
1393        """Return a dictionary of the hosts that have trust relationships
1394        with this AA.  The dictionary is indexed by the trusted host name
1395        and contains AA service, login URIs and the roles that map to the
1396        given input local role.
1397
1398        @type role: string
1399        @param role: if set, return trusted hosts that having a mapping set
1400        for this role.  If no role is input, return all the AA's trusted hosts
1401        with all their possible roles
1402
1403        @rtype: dict
1404        @return: dictionary of the hosts that have trust relationships
1405        with this AA.  It returns an empty dictionary if role isn't
1406        recognised"""
1407               
1408        log.debug('Calling getTrustedHostInfo with role = "%s" ...' % role) 
1409                                 
1410        if not self.__mapConfig or not self.__localRole2RemoteRole:
1411            # This Attribute Authority has no trusted hosts
1412            raise AttributeAuthorityNoTrustedHosts("The %s Attribute "
1413                                                   "Authority has no trusted "
1414                                                   "hosts" % 
1415                                                   self.name)
1416
1417
1418        if role is None:
1419            # No role input - return all trusted hosts with their service URIs
1420            # and the remote roles they map to
1421            return self._trustedHostInfo
1422
1423        else:           
1424            # Get trusted hosts for given input local role       
1425            try:
1426                trustedHosts = self.__localRole2TrustedHost[role]
1427            except:
1428                raise AttributeAuthorityNoMatchingRoleInTrustedHosts(
1429                    'None of the trusted hosts have a mapping to the '
1430                    'input role "%s"' % role)
1431   
1432   
1433            # Get associated Web service URI and roles for the trusted hosts
1434            # identified and return as a dictionary indexed by host name
1435            trustedHostInfo = dict(
1436       [(
1437            host, 
1438            {
1439                'siteName': self.__mapConfig['trustedHosts'][host]['siteName'],
1440                'aaURI':    self.__mapConfig['trustedHosts'][host]['aaURI'],
1441                'aaDN':     self.__mapConfig['trustedHosts'][host]['aaDN'],
1442                'loginURI': self.__mapConfig['trustedHosts'][host]['loginURI'],
1443                'loginServerDN': 
1444                        self.__mapConfig['trustedHosts'][host]['loginServerDN'],
1445                'loginRequestServerDN': 
1446                self.__mapConfig['trustedHosts'][host]['loginRequestServerDN'],
1447                'role':     self.__localRole2RemoteRole[host][role]
1448            }
1449        ) for host in trustedHosts])
1450                         
1451            return trustedHostInfo
1452       
1453       
1454    def mapRemoteRoles2LocalRoles(self, trustedHost, trustedHostRoles):
1455        """Map roles of trusted hosts to roles for this data centre
1456
1457        @type trustedHost: string
1458        @param trustedHost: name of external trusted data centre
1459        @type trustedHostRoles: list
1460        @param trustedHostRoles:   list of external roles to map
1461        @return: list of mapped roles"""
1462
1463        if not self.__remoteRole2LocalRole:
1464            raise AttributeAuthorityError("Roles map is not set - ensure " 
1465                                    "readMapConfig() has been called.")
1466
1467
1468        # Check the host name is a trusted one recorded in the map
1469        # configuration
1470        if not self.__remoteRole2LocalRole.has_key(trustedHost):
1471            return []
1472
1473        # Add local roles, skipping if no mapping is found
1474        localRoles = []
1475        for trustedRole in trustedHostRoles:
1476            if trustedRole in self.__remoteRole2LocalRole[trustedHost]:
1477                localRoles.extend(
1478                        self.__remoteRole2LocalRole[trustedHost][trustedRole])
1479               
1480        return localRoles
1481
1482    def getAttCertFactory(self):
1483        """Factory method to create SAML Attribute Qeury wrapper function
1484        @rtype: function
1485        @return getAttCert method function wrapper
1486        """
1487        def getAttCertWrapper(*arg, **kw):
1488            """
1489            @type *arg: tuple
1490            @param *arg: getAttCert arguments
1491            @type **kw: dict
1492            @param **kw: getAttCert keyword arguments
1493            @rtype: ndg.security.common.AttCert.AttCert
1494            @return: new attribute certificate
1495            """
1496            return self.getAttCert(*arg, **kw)
1497       
1498        return getAttCertWrapper
1499
1500    def samlAttributeQueryFactory(self):
1501        """Factory method to create SAML Attribute Qeury wrapper function
1502        @rtype: function
1503        @return: samlAttributeQuery method function wrapper
1504        """
1505        def samlAttributeQueryWrapper(attributeQuery):
1506            """
1507            @type attributeQuery: saml.saml2.core.AttributeQuery
1508            @param attributeQuery: SAML Attribute Query
1509            @rtype: saml.saml2.core.Response
1510            @return: SAML response
1511            """
1512            return self.samlAttributeQuery(attributeQuery)
1513       
1514        return samlAttributeQueryWrapper
1515   
1516
1517from logging.handlers import RotatingFileHandler
1518
1519# Inherit directly from Logger
1520_loggerClass = logging.getLoggerClass()
1521class AttCertLog(_loggerClass, object):
1522    """Log each Attribute Certificate issued using a rotating file handler
1523    so that the number of files held can be managed"""
1524   
1525    def __init__(self, attCertFilePath, backUpCnt=1024):
1526        """Set up a rotating file handler to log ACs issued.
1527        @type attCertFilePath: string
1528        @param attCertFilePath: set where to store ACs.  Set from
1529        AttributeAuthority properties file.
1530       
1531        @type backUpCnt: int
1532        @param backUpCnt: set the number of files to store before rotating
1533        and overwriting old files."""
1534       
1535        if not isinstance(backUpCnt, int):
1536            raise TypeError('Expecting int type for "backUpCnt" keyword')
1537       
1538        # Inherit from Logger class
1539        super(AttCertLog, self).__init__(name='', level=logging.INFO)
1540                           
1541        # Set a format for messages so that only the content of the AC is
1542        # logged, nothing else.
1543        formatter = logging.Formatter(fmt="", datefmt="")
1544
1545        # maxBytes is set to one so that only one AC will be written before
1546        # rotation to the next file
1547        fileLog = RotatingFileHandler(attCertFilePath, 
1548                                      maxBytes=1, 
1549                                      backupCount=backUpCnt)
1550        fileLog.setFormatter(formatter)           
1551        self.addHandler(fileLog)
1552 
1553                     
1554class AttributeInterfaceError(Exception):
1555    """Exception handling for NDG Attribute Authority User Roles interface
1556    class."""
1557
1558                       
1559class AttributeReleaseDenied(AttributeInterfaceError):
1560    """Requestor was denied release of the requested attributes"""
1561
1562                       
1563class AttributeNotKnownError(AttributeInterfaceError):
1564    """Requested attribute names are not known to this authority"""
1565
1566
1567class InvalidRequestorId(AttributeInterfaceError):
1568    """Requestor is not known or not allowed to request attributes"""
1569   
1570
1571class UserIdNotKnown(AttributeInterfaceError): 
1572    """User ID passed to getAttributes is not known to the authority"""
1573   
1574   
1575class InvalidUserId(AttributeInterfaceError):
1576    """User Id passed to getAttributes is invalid"""
1577   
1578     
1579class AttributeInterface(object):
1580    """An abstract base class to define the user roles interface to an
1581    Attribute Authority.
1582
1583    Each NDG data centre should implement a derived class which implements
1584    the way user roles are provided to its representative Attribute Authority.
1585   
1586    Roles are expected to indexed by user Distinguished Name (DN).  They
1587    could be stored in a database or file."""
1588
1589    # User defined class may wish to specify a URI for a database interface or
1590    # path for a user roles configuration file
1591    def __init__(self, **prop):
1592        """User Roles base class - derive from this class to define
1593        roles interface to Attribute Authority
1594       
1595        @type prop: dict
1596        @param prop: custom properties to pass to this class
1597        """
1598
1599    def getRoles(self, userId):
1600        """Virtual method - Derived method should return the roles for the
1601        given user's Id or else raise an exception
1602       
1603        @type userId: string
1604        @param userId: user identity e.g. user Distinguished Name
1605        @rtype: list
1606        @return: list of roles for the given user ID
1607        @raise AttributeInterfaceError: an error occured requesting
1608        attributes
1609        """
1610        raise NotImplementedError(self.getRoles.__doc__)
1611 
1612    def getAttributes(self, attributeQuery, response):
1613        """Virtual method should be implemented in a derived class to enable
1614        AttributeAuthority.samlAttributeQuery - The derived method should
1615        return the attributes requested for the given user's Id or else raise
1616        an exception
1617       
1618        @type attributeQuery: saml.saml2.core.AttributeQuery
1619        @param userId: query containing requested attributes
1620        @type: saml.saml2.core.Response
1621        @param: Response - add an assertion with the list of attributes
1622        for the given subject ID in the query or set an error Status code and
1623        message
1624        @raise AttributeInterfaceError: an error occured requesting
1625        attributes
1626        @raise AttributeReleaseDeniedError: Requestor was denied release of the
1627        requested attributes
1628        @raise AttributeNotKnownError: Requested attribute names are not known
1629        to this authority
1630        """
1631        raise NotImplementedError(self.getAttributes.__doc__)
1632
1633
1634class CSVFileAttributeInterface(AttributeInterface):
1635    """Attribute Interface based on a Comma Separated Variable file containing
1636    user identities and associated attributes.  For test/development purposes
1637    only.  The SAML getAttributes method is NOT implemented here
1638   
1639    The expected file format is:
1640   
1641    <userID>, <role1>, <role2>, ... <roleN>
1642    """
1643    def __init__(self, propertiesFilePath=None):
1644        """
1645        @param propertiesFilePath: file path to Comma Separated file
1646        containing user ids and roles
1647        @type propertiesFilePath: basestring
1648        """
1649        if propertiesFilePath is None:
1650            raise AttributeError("Expecting propertiesFilePath setting")
1651       
1652        propertiesFile = open(propertiesFilePath)
1653        lines = propertiesFile.readlines()
1654       
1655        self.attributeMap = {}
1656        for line in lines:
1657            fields = re.split(',\W*', line.strip())
1658            self.attributeMap[fields[0]] = fields[1:]
1659   
1660    def getRoles(self, userId):
1661        """
1662        @param userId: user identity to key into attributeMap
1663        @type userId: basestring
1664        """ 
1665        log.debug('CSVFileAttributeInterface.getRoles for user "%s" ...', 
1666                  userId)
1667        return self.attributeMap.get(userId, [])
1668
1669
1670# Properties file
1671from ConfigParser import SafeConfigParser, NoOptionError
1672
1673try:
1674    # PostgreSQL interface
1675    from psycopg2 import connect
1676except ImportError:
1677    pass
1678
1679class PostgresAttributeInterface(AttributeInterface):
1680    """User Roles interface to Postgres database
1681   
1682    The SAML getAttributes method is NOT implemented"""
1683
1684    CONNECTION_SECTION_NAME = "Connection"
1685    GETROLES_SECTION_NAME = "getRoles"
1686    HOST_OPTION_NAME = "host"
1687    DBNAME_OPTION_NAME = "dbName"
1688    USERNAME_OPTION_NAME = "username"
1689    PWD_OPTION_NAME = "pwd"
1690    QUERYN_OPTION_NAME = "query%d"
1691    DEFAULT_ROLES_OPTION_NAME = "defaultRoles"
1692   
1693    def __init__(self, propertiesFilePath=None):
1694        """Connect to Postgres database"""
1695        self.__con = None
1696        self.__host = None
1697        self.__dbName = None
1698        self.__username = None
1699        self.__pwd = None
1700
1701        if propertiesFilePath is None:
1702            raise AttributeError("No Configuration file was set")
1703
1704        self.readConfigFile(propertiesFilePath)
1705
1706    def __del__(self):
1707        """Close database connection"""
1708        self.close()
1709
1710    def readConfigFile(self, propertiesFilePath):
1711        """Read the configuration for the database connection
1712
1713        @type propertiesFilePath: string
1714        @param propertiesFilePath: file path to config file"""
1715
1716        if not isinstance(propertiesFilePath, basestring):
1717            raise TypeError("Input Properties file path must be a valid "
1718                            "string; got %r" % type(propertiesFilePath))
1719
1720        cfg = SafeConfigParser()
1721        cfg.read(propertiesFilePath)
1722
1723        self.__host = cfg.get(
1724                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1725                        PostgresAttributeInterface.HOST_OPTION_NAME)
1726        self.__dbName = cfg.get(
1727                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1728                        PostgresAttributeInterface.DBNAME_OPTION_NAME)
1729        self.__username = cfg.get(
1730                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1731                        PostgresAttributeInterface.USERNAME_OPTION_NAME)
1732        self.__pwd = cfg.get(
1733                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1734                        PostgresAttributeInterface.PWD_OPTION_NAME)
1735
1736        try:
1737            self.__getRolesQuery = []
1738            for i in range(10):
1739                queryStr = cfg.get(
1740                        PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1741                        PostgresAttributeInterface.QUERYN_OPTION_NAME % i)
1742                self.__getRolesQuery += [queryStr]
1743        except NoOptionError:
1744             # Continue until no more query<n> items left
1745             pass
1746
1747        # This option may be omitted in the config file
1748        try:
1749            self.__defaultRoles = cfg.get(
1750                PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1751                PostgresAttributeInterface.DEFAULT_ROLES_OPTION_NAME).split()
1752        except NoOptionError:
1753            self.__defaultRoles = []
1754
1755    def connect(self,
1756                username=None,
1757                dbName=None,
1758                host=None,
1759                pwd=None,
1760                prompt="Database password: "):
1761        """Connect to database
1762
1763        Values for keywords omitted are derived from the config file.  If pwd
1764        is not in the config file it will be prompted for from stdin
1765
1766        @type username: string
1767        @keyword username: database account username
1768        @type dbName: string
1769        @keyword dbName: name of database
1770        @type host: string
1771        @keyword host: database host machine
1772        @type pwd: string
1773        @keyword pwd: password for database account.  If omitted and not in
1774        the config file it will be prompted for from stdin
1775        @type prompt: string
1776        @keyword prompt: override default password prompt"""
1777
1778        if not host:
1779            host = self.__host
1780
1781        if not dbName:
1782            dbName = self.__dbName
1783
1784        if not username:
1785            username = self.__username
1786
1787        if not pwd:
1788            pwd = self.__pwd
1789
1790            if not pwd:
1791                import getpass
1792                pwd = getpass.getpass(prompt=prompt)
1793
1794        try:
1795            self.__db = connect("host=%s dbname=%s user=%s password=%s" % \
1796                                (host, dbName, username, pwd))
1797            self.__cursor = self.__db.cursor()
1798
1799        except NameError, e:
1800            raise AttributeInterfaceError("psycopg2 Postgres package not "
1801                                          "installed? %s" % e)
1802        except Exception, e:
1803            raise AttributeInterfaceError("Error connecting to database "
1804                                          "\"%s\": %s" % (dbName, e))
1805
1806
1807    def close(self):
1808        """Close database connection"""
1809        if self.__con:
1810            self.__con.close()
1811
1812    def getRoles(self, userId):
1813        """Return valid roles for the given userId
1814
1815        @type userId: basestring
1816        @param userId: user identity"""
1817
1818        try:
1819            self.connect()
1820
1821            # Process each query in turn appending role names
1822            roles = self.__defaultRoles[:]
1823            for query in self.__getRolesQuery:
1824                try:
1825                    self.__cursor.execute(query % userId)
1826                    queryRes = self.__cursor.fetchall()
1827
1828                except Exception, e:
1829                    raise AttributeInterfaceError("Query for %s: %s" %
1830                                                  (userId, e))
1831
1832                roles += [res[0] for res in queryRes if res[0]]
1833        finally:
1834            self.close()
1835
1836        return roles
1837
1838    def __getCursor(self):
1839        """Return a database cursor instance"""
1840        return self.__cursor
1841
1842    cursor = property(fget=__getCursor, doc="database cursor")
Note: See TracBrowser for help on using the repository browser.