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

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

Integrated SOAP SAML Attribute Query interface into Attribute Authority Client unit tests.

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