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

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

Renamed Attribute Authority classes and reran unittests

  • 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) 2007 STFC & NERC"
10__license__ = \
11"""This software may be distributed under the terms of the Q Public
12License, version 1.0 or later."""
13__contact__ = "P.J.Kershaw@rl.ac.uk"
14__revision__ = '$Id:attributeauthority.py 4367 2008-10-29 09:27:59Z pjkersha $'
15
16import types
17
18
19# Create unique names for attribute certificates
20import tempfile
21import os
22
23# Alter system path for dynamic import of user roles class
24import sys
25
26# For parsing of properties file
27try: # python 2.5
28    from xml.etree import cElementTree as ElementTree
29except ImportError:
30    # if you've installed it yourself it comes this way
31    import cElementTree as ElementTree
32
33import logging
34log = logging.getLogger(__name__)
35
36# X509 Certificate handling
37from ndg.security.common.X509 import *
38
39# NDG Attribute Certificate
40from ndg.security.common.AttCert import *
41
42from ndg.security.common.utils.ConfigFileParsers import \
43    readAndValidateProperties
44from ndg.security.common.utils.ClassFactory import instantiateClass
45
46#_____________________________________________________________________________
47class AttributeAuthorityError(Exception):
48    """Exception handling for NDG Attribute Authority class."""
49    def __init__(self, msg):
50        log.error(msg)
51        Exception.__init__(self, msg)
52
53#_____________________________________________________________________________
54class AttributeAuthorityConfigError(Exception):
55    """NDG Attribute Authority error with configuration. e.g. properties file
56    directory permissions or role mapping file"""
57    def __init__(self, msg):
58        log.error(msg)
59        Exception.__init__(self, msg)
60       
61
62#_____________________________________________________________________________
63class AttributeAuthorityAccessDenied(AttributeAuthorityError):
64    """NDG Attribute Authority - access denied exception.
65
66    Raise from getAttCert method where no roles are available for the user
67    but that the request is otherwise valid.  In all other error cases raise
68    AttributeAuthorityError"""   
69
70class AttributeAuthorityNoTrustedHosts(AttributeAuthorityError):
71    """Raise from getTrustedHosts if there are no trusted hosts defined in
72    the map configuration"""
73
74class AttributeAuthorityNoMatchingRoleInTrustedHosts(AttributeAuthorityError):
75    """Raise from getTrustedHosts if there is no mapping to any of the
76    trusted hosts for the given input role name"""
77
78
79#_____________________________________________________________________________
80class AttributeAuthority(dict):
81    """NDG Attribute Authority - service for allocation of user authorization
82    tokens - attribute certificates.
83   
84    @type propertyDefaults: dict
85    @cvar propertyDefaults: valid configuration property keywords - properties file
86    must contain these
87   
88    @type _confDir: string
89    @cvar _confDir: configuration directory under $NDGSEC_DIR - default
90    location for properties file
91   
92    @type _propFileName: string
93    @cvar _propFileName: default file name for properties file under
94    _confDir
95    """
96
97    # Code designed from NERC Data Grid Enterprise and Information Viewpoint
98    # documents.
99    #
100    # Also, draws from Neil Bennett's ACServer class used in the Java
101    # implementation of NDG Security
102
103    _confDir = "conf"
104    _propFileName = "attAuthorityProperties.xml"
105   
106    # valid configuration property keywords with accepted default values. 
107    # Values set to not NotImplemented here denote keys which must be specified
108    # in the config
109    propertyDefaults = { 
110        'name':                '',
111        'portNum':             -1,
112        'useSSL':              False,
113        'sslCertFile':         '',
114        'sslKeyFile':          '',
115        'sslKeyPwd':           '',
116        'sslCACertDir':        '',
117        'signingCertFilePath': NotImplemented,
118        'signingPriKeyFilePath':NotImplemented,
119        'signingPriKeyPwd':    None,
120        'caCertFilePathList':  [NotImplemented],
121        'attCertLifetime':     -1,
122        'attCertNotBeforeOff': 0,
123        'attCertFileName':     NotImplemented,
124        'attCertFileLogCnt':   0,
125        'mapConfigFile':       NotImplemented,
126        'attCertDir':          NotImplemented,
127        'dnSeparator':         '/',
128        'userRolesModFilePath':'',
129        'userRolesModName':    NotImplemented,
130        'userRolesClassName':  NotImplemented,
131        'userRolesPropFile':   ''
132    }
133   
134    WS_SETTINGS_KEY = 'WS-Security'
135
136    def __init__(self, 
137                 propFilePath=None, 
138                 propFileSection='DEFAULT',
139                 propPrefix='',
140                 bReadMapConfig=True):
141        """Create new NDG Attribute Authority instance
142
143        @type propFilePath: string
144        @param propFilePath: path to file containing Attribute Authority
145        configuration parameters.  It defaults to $NDGSEC_AA_PROPFILEPATH or
146        if not set, $NDGSEC_DIR/conf/attAuthorityProperties.xml
147        - if the filename ends with 'xml', it is assumed to be in the xml
148        format
149        - otherwise it is assumed to be a flat text 'ini' type file
150        @type propFileSection: basestring
151        @param propFileSection: section of properties file to read from.  This
152        applies to ini format files only and is ignored for XML format
153        properties files
154        @type bReadMapConfig: boolean
155        @param bReadMapConfig: by default the Map Configuration file is
156        read.  Set this flag to False to override.
157        """
158        log.info("Initialising service ...")
159       
160        # Base class initialisation
161        dict.__init__(self)
162
163        # Set from input or use defaults based or environment variables
164        self.propFilePath = propFilePath
165       
166        self.propFileSection = propFileSection
167        self.propPrefix = propPrefix
168       
169        # Initialise role mapping look-ups - These are set in readMapConfig()
170        self.__mapConfig = None
171        self.__localRole2RemoteRole = None
172        self.__remoteRole2LocalRole = None
173
174        self.readProperties()
175
176        # Read the Map Configuration file
177        if bReadMapConfig:
178            self.readMapConfig()
179
180        # Instantiate Certificate object
181        log.debug("Reading and checking Attribute Authority X.509 cert. ...")
182        self.__cert = X509Cert(self.__prop['signingCertFilePath'])
183        self.__cert.read()
184
185        # Check it's valid
186        try:
187            self.__cert.isValidTime(raiseExcep=True)
188           
189        except Exception, e:
190            raise AttributeAuthorityError("Attribute Authority's certificate is "
191                                    "invalid: " + str(e))
192       
193        # Check CA certificate
194        log.debug("Reading and checking X.509 CA certificate ...")
195        for caCertFile in self.__prop['caCertFilePathList']:
196            caCert = X509Cert(caCertFile)
197            caCert.read()
198           
199            try:
200                caCert.isValidTime(raiseExcep=True)
201               
202            except Exception, e:
203                raise AttributeAuthorityError('CA certificate "%s" is invalid: %s'%\
204                                        (caCert.dn, e))
205       
206        # Issuer details - serialise using the separator string set in the
207        # properties file
208        self.__issuer = \
209            self.__cert.dn.serialise(separator=self.__prop['dnSeparator'])
210
211        self.__issuerSerialNumber = self.__cert.serialNumber
212       
213        # Load host sites custom user roles interface to enable the AA to
214        # assign roles in an attribute certificate on a getAttCert request
215        self.__userRoles = instantiateClass(self.__prop['userRolesModName'],
216                     self.__prop['userRolesClassName'],
217                     moduleFilePath=self.__prop.get('userRolesModFilePath'),
218                     objectType=AAUserRoles,
219                     classProperties=self.__prop.get('userRolesPropFile'))
220
221        attCertFilePath = os.path.join(self.__prop['attCertDir'],
222                                       self.__prop['attCertFileName'])
223               
224        # Rotating file handler used for logging attribute certificates
225        # issued.
226        self.__attCertLog = AttCertLog(attCertFilePath)
227
228
229    def readProperties(self, section=None, prefix=None):
230        '''Read the properties files and do some checking/converting of input
231        values
232       
233        @type section: basestring
234        @param section: ini file section to read properties from - doesn't
235        apply to XML format properties files.  section setting defaults to
236        current propFileSection attribute
237       
238        @type prefix: basestring
239        @param prefix: apply prefix to ini file properties - doesn't
240        apply to XML format properties files.  This enables filtering of
241        properties so that only those relevant to this class are read in
242        '''
243        if section is None:
244            section = self.propFileSection
245       
246        if prefix is None:
247            prefix = self.propPrefix
248             
249        # Configuration file properties are held together in a dictionary
250        fileProp = readAndValidateProperties(self.propFilePath, 
251                                     validKeys=AttributeAuthority.propertyDefaults,
252                                     prefix=prefix,
253                                     sections=(section,))
254       
255        # Allow for section and prefix names which will nest the Attribute
256        # Authority properties in a hierarchy
257        propBranch = fileProp
258        if section != 'DEFAULT':
259            propBranch = propBranch[section]
260           
261        if prefix:
262            propBranch = propBranch[prefix]
263           
264        self.__prop = propBranch
265       
266        # Ensure Certificate time parameters are converted to numeric type
267        self.__prop['attCertLifetime'] = float(self.__prop['attCertLifetime'])
268        self.__prop['attCertNotBeforeOff'] = \
269                                    float(self.__prop['attCertNotBeforeOff'])
270
271        # Check directory path
272        try:
273            dirList = os.listdir(self.__prop['attCertDir'])
274
275        except OSError, osError:
276            raise AttributeAuthorityConfigError('Invalid directory path Attribute '
277                                    'Certificates store "%s": %s' % \
278                                    (self.__prop['attCertDir'], 
279                                     osError.strerror))
280
281       
282    # Methods for Attribute Authority dictionary like behaviour       
283    def __repr__(self):
284        """Return file properties dictionary as representation"""
285        return repr(self.__prop)
286   
287    def __delitem__(self, key):
288        AttributeAuthority.__name__ + " keys cannot be removed"       
289        raise KeyError('Keys cannot be deleted from '+AttributeAuthority.__name__)
290
291
292    def __getitem__(self, key):
293        AttributeAuthority.__name__ + """ behaves as data dictionary of Attribute
294        Authority properties
295        """
296        if key not in self.__prop:
297            raise KeyError("Invalid key '%s'" % key)
298       
299        return self.__prop[key]
300       
301    def get(self, kw):
302        return self.__prop.get(kw)
303   
304    def clear(self):
305        raise KeyError("Data cannot be cleared from "+AttributeAuthority.__name__)
306   
307    def keys(self):
308        return self.__prop.keys()
309
310    def items(self):
311        return self.__prop.items()
312
313    def values(self):
314        return self.__prop.values()
315
316    def has_key(self, key):
317        return self.__prop.has_key(key)
318
319    # 'in' operator
320    def __contains__(self, key):
321        return key in self.__prop
322
323
324    def setPropFilePath(self, val=None):
325        """Set properties file from input or based on environment variable
326        settings
327       
328        @type val: basestring
329        @param val: properties file path"""
330        log.debug("Setting property file path")
331        if not val:
332            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
333                val = os.environ['NDGSEC_AA_PROPFILEPATH']
334               
335            elif 'NDGSEC_DIR' in os.environ:
336                val = os.path.join(os.environ['NDGSEC_DIR'], 
337                                   AttributeAuthority._confDir,
338                                   AttributeAuthority._propFileName)
339            else:
340                raise AttributeError('Unable to set default Attribute '
341                                     'Authority properties file path: neither '
342                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
343                                     ' environment variables are set')
344               
345        if not isinstance(val, basestring):
346            raise AttributeError("Input Properties file path "
347                                 "must be a valid string.")
348     
349        self._propFilePath = os.path.expandvars(val)
350        log.debug("Path set to: %s" % val)
351       
352    def getPropFilePath(self):
353        '''Get the properties file path
354       
355        @rtype: basestring
356        @return: properties file path'''
357        log.debug("Getting property file path")
358        if hasattr(self, '_propFilePath'):
359            return self._propFilePath
360        else:
361            return ""
362       
363    # Also set up as a property
364    propFilePath = property(fset=setPropFilePath,
365                            fget=getPropFilePath,
366                            doc="Set the path to the properties file")   
367   
368    def setPropFileSection(self, val=None):
369        """Set section name to read properties from ini file.  This is set from
370        input or based on environment variable setting
371        NDGSEC_AA_PROPFILESECTION
372       
373        @type val: basestring
374        @param val: section name"""
375        log.debug("Setting property file section name")
376        if not val:
377            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
378               
379        if not isinstance(val, basestring):
380            raise AttributeError("Input Properties file section name "
381                                 "must be a valid string.")
382     
383        self._propFileSection = val
384        log.debug("Properties file section set to: %s" % val)
385       
386    def getPropFileSection(self):
387        '''Get the section name to extract properties from an ini file -
388        DOES NOT apply to XML file properties
389       
390        @rtype: basestring
391        @return: section name'''
392        log.debug("Getting property file section name")
393        if hasattr(self, '_propFileSection'):
394            return self._propFileSection
395        else:
396            return ""   
397       
398    # Also set up as a property
399    propFileSection = property(fset=setPropFileSection,
400                    fget=getPropFileSection,
401                    doc="Set the file section name for ini file properties")   
402   
403    def setPropPrefix(self, val=None):
404        """Set prefix for properties read from ini file.  This is set from
405        input or based on environment variable setting
406        NDGSEC_AA_PROPFILEPREFIX
407       
408        DOES NOT apply to XML file properties
409       
410        @type val: basestring
411        @param val: section name"""
412        log.debug("Setting property file section name")
413        if val is None:
414            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
415               
416        if not isinstance(val, basestring):
417            raise AttributeError("Input Properties file section name "
418                                 "must be a valid string.")
419     
420        self._propPrefix = val
421        log.debug("Properties file section set to: %s" % val)
422       
423    def getPropPrefix(self):
424        '''Get the prefix name used for properties in an ini file -
425        DOES NOT apply to XML file properties
426       
427        @rtype: basestring
428        @return: section name'''
429        log.debug("Getting property file prefix")
430        if hasattr(self, '_propPrefix'):
431            return self._propPrefix
432        else:
433            return ""   
434       
435    # Also set up as a property
436    propPrefix = property(fset=setPropPrefix,
437                          fget=getPropPrefix,
438                          doc="Set a prefix for ini file properties")   
439
440    def getAttCert(self,
441                   userId=None,
442                   holderCert=None,
443                   holderCertFilePath=None,
444                   userAttCert=None,
445                   userAttCertFilePath=None):
446
447        """Request a new Attribute Certificate for use in authorisation
448
449        getAttCert([userId=uid][holderCert=px|holderCertFilePath=pxFile, ]
450                   [userAttCert=cert|userAttCertFilePath=certFile])
451         
452        @type userId: string
453        @param userId: identifier for the user who is entitled to the roles
454        in the certificate that is issued.  If this keyword is omitted, then
455        the userId will be set to the DN of the holder.
456       
457        holder = the holder of the certificate - an inidividual user or an
458        organisation to which the user belongs who vouches for that user's ID
459       
460        userId = the identifier for the user who is entitled to the roles
461        specified in the Attribute Certificate that is issued.
462                 
463        @type holderCert: string / ndg.security.common.X509.X509Cert type
464        @param holderCert: base64 encoded string containing proxy cert./
465        X.509 cert object corresponding to the ID who will be the HOLDER of
466        the Attribute Certificate that will be issued.  - Normally, using
467        proxy certificates, the holder and user ID are the same but there
468        may be cases where the holder will be an organisation ID.  This is the
469        case for NDG security with the DEWS project
470       
471        @param holderCertFilePath: string
472        @param holderCertFilePath: file path to proxy/X.509 certificate of
473        candidate holder
474     
475        @type userAttCert: string or AttCert type
476        @param userAttCert: externally provided attribute certificate from
477        another data centre.  This is only necessary if the user is not
478        registered with this attribute authority.
479                       
480        @type userAttCertFilePath: string
481        @param userAttCertFilePath: alternative to userAttCert except pass
482        in as a file path to an attribute certificate instead.
483       
484        @rtype: AttCert
485        @return: new attribute certificate"""
486
487        log.debug("Calling getAttCert ...")
488       
489        # Read X.509 certificate
490        try:           
491            if holderCertFilePath is not None:
492                                   
493                # Certificate input as a file
494                holderCert = X509Cert()
495                holderCert.read(holderCertFilePath)
496               
497            elif isinstance(holderCert, basestring):
498
499                # Certificate input as string text
500                holderCert = X509CertParse(holderCert)
501               
502            elif not isinstance(holderCert, X509Cert):
503                raise AttributeAuthorityError("No input file path or cert text/"
504                                        "object set")
505           
506        except Exception, e:
507            raise AttributeAuthorityError("User X.509 certificate: %s" % e)
508
509
510        # Check certificate hasn't expired
511        log.debug("Checking client request X.509 certificate ...")
512        try:
513            holderCert.isValidTime(raiseExcep=True)
514           
515        except Exception, e:
516            raise AttributeAuthorityError("User X.509 certificate is invalid: " + \
517                                    str(e))
518
519           
520        # Get Distinguished name from certificate as an X500DN type
521        if not userId:
522            try:
523                userId = holderCert.dn.serialise(\
524                                         separator=self.__prop['dnSeparator']) 
525            except Exception, e:
526                raise AttributeAuthorityError("Setting user Id from holder "
527                                        "certificate DN: %s" % e)
528       
529        # Make a new Attribute Certificate instance passing in certificate
530        # details for later signing
531        attCert = AttCert()
532
533        # First cert in list corresponds to the private key
534        attCert.certFilePathList = [self.__prop['signingCertFilePath']] + \
535                                                                self.__prop['caCertFilePathList']
536                                                               
537        attCert.signingKeyFilePath = self.__prop['signingPriKeyFilePath']
538        attCert.signingKeyPwd = self.__prop['signingPriKeyPwd']
539       
540       
541        # Set holder's (user's) Distinguished Name
542        try:
543            attCert['holder'] = \
544                holderCert.dn.serialise(separator=self.__prop['dnSeparator'])           
545        except Exception, e:
546            raise AttributeAuthorityError("Holder DN: %s" % e)
547
548       
549        # Set Issuer details from Attribute Authority
550        issuerDN = self.__cert.dn
551        try:
552            attCert['issuer'] = \
553                    issuerDN.serialise(separator=self.__prop['dnSeparator'])           
554        except Exception, e:
555            raise AttributeAuthorityError("Issuer DN: %s" % e)
556       
557        attCert['issuerName'] = self.__prop['name']
558        attCert['issuerSerialNumber'] = self.__issuerSerialNumber
559
560        attCert['userId'] = userId
561       
562        # Set validity time
563        try:
564            attCert.setValidityTime(\
565                        lifetime=self.__prop['attCertLifetime'],
566                        notBeforeOffset=self.__prop['attCertNotBeforeOff'])
567
568            # Check against the certificate's expiry
569            dtHolderCertNotAfter = holderCert.notAfter
570           
571            if attCert.getValidityNotAfter(asDatetime=True) > \
572               dtHolderCertNotAfter:
573
574                # Adjust the attribute certificate's expiry date time
575                # so that it agrees with that of the certificate
576                # ... but also make ensure that the not before skew is still
577                # applied
578                attCert.setValidityTime(dtNotAfter=dtHolderCertNotAfter,
579                        notBeforeOffset=self.__prop['attCertNotBeforeOff'])
580           
581        except Exception, e:
582            raise AttributeAuthorityError("Error setting validity time: %s" % e)
583       
584
585        # Check name is registered with this Attribute Authority - if no
586        # user roles are found, the user is not registered
587        userRoles = self.getRoles(userId)
588        if userRoles:           
589            # Set as an Original Certificate
590            #
591            # User roles found - user is registered with this data centre
592            # Add roles for this user for this data centre
593            attCert.addRoles(userRoles)
594
595            # Mark new Attribute Certificate as an original
596            attCert['provenance'] = AttCert.origProvenance
597
598        else:           
599            # Set as a Mapped Certificate
600            #
601            # No roles found - user is not registered with this data centre
602            # Check for an externally provided certificate from another
603            # trusted data centre
604            if userAttCertFilePath:
605               
606                # Read externally provided certificate
607                try:
608                    userAttCert = AttCertRead(userAttCertFilePath)
609                   
610                except Exception, e:
611                    raise AttributeAuthorityError("Reading external Attribute "
612                                            "Certificate: %s" % e)                           
613            elif userAttCert:
614                # Allow input as a string but convert to
615                if isinstance(userAttCert, basestring):
616                    userAttCert = AttCertParse(userAttCert)
617                   
618                elif not isinstance(userAttCert, AttCert):
619                    raise AttributeAuthorityError(
620                        "Expecting userAttCert as a string or AttCert type")       
621            else:
622                raise AttributeAuthorityAccessDenied("User \"%s\" is not registered "
623                                               "and no external attribute "
624                                               "certificate is available to "
625                                               "make a mapping." % userId)
626
627
628            # Check it's an original certificate - mapped certificates can't
629            # be used to make further mappings
630            if userAttCert.isMapped():
631                raise AttributeAuthorityError("External Attribute Certificate must "
632                                        "have an original provenance in order "
633                                        "to make further mappings.")
634
635
636            # Check it's valid and signed
637            try:
638                # Give path to CA cert to allow check
639                userAttCert.certFilePathList=self.__prop['caCertFilePathList']
640                userAttCert.isValid(raiseExcep=True)
641               
642            except Exception, e:
643                raise AttributeAuthorityError("Invalid Remote Attribute "
644                                        "Certificate: " + str(e))       
645
646
647            # Check that's it's holder matches the candidate holder
648            # certificate DN
649            if userAttCert.holderDN != holderCert.dn:
650                raise AttributeAuthorityError("User certificate and Attribute "
651                                        'Certificate DNs don\'t match: "%s"'
652                                        ' and "%s"' % (holderCert.dn, 
653                                                       userAttCert.holderDN))
654           
655 
656            # Get roles from external Attribute Certificate
657            trustedHostRoles = userAttCert.roles
658
659
660            # Map external roles to local ones
661            localRoles = self.mapRemoteRoles2LocalRoles(\
662                                                    userAttCert['issuerName'],
663                                                    trustedHostRoles)
664            if not localRoles:
665                raise AttributeAuthorityAccessDenied("No local roles mapped to the "
666                                               "%s roles: %s" % \
667                                               (userAttCert['issuerName'], 
668                                                ', '.join(trustedHostRoles)))
669
670            attCert.addRoles(localRoles)
671           
672           
673            # Mark new Attribute Certificate as mapped
674            attCert.provenance = AttCert.mappedProvenance
675
676            # Copy the user Id from the external AC
677            attCert.userId = userAttCert.userId
678           
679            # End set mapped certificate block
680
681        try:
682            # Digitally sign certificate using Attribute Authority's
683            # certificate and private key
684            attCert.applyEnvelopedSignature()
685           
686            # Check the certificate is valid
687            attCert.isValid(raiseExcep=True)
688           
689            # Write out certificate to keep a record of it for auditing
690            #attCert.write()
691            self.__attCertLog.info(attCert)
692           
693            log.info('Issued an Attribute Certificate to "%s" with roles: '
694                     '"%s"' % (userId, '", "'.join(attCert.roles)))
695
696            # Return the cert to caller
697            return attCert
698       
699        except Exception, e:
700            raise AttributeAuthorityError("New Attribute Certificate \"%s\": %s" % \
701                                    (attCert.filePath, e))
702       
703       
704    #_________________________________________________________________________     
705    def readMapConfig(self, mapConfigFilePath=None):
706        """Parse Map Configuration file.
707
708        @type mapConfigFilePath: string
709        @param mapConfigFilePath: file path for map configuration file.  If
710        omitted, it uses member variable __prop['mapConfigFile'].
711        """
712       
713        log.debug("Reading map configuration file ...")
714       
715        if mapConfigFilePath is not None:
716            if not isinstance(mapConfigFilePath, basestring):
717                raise AttributeAuthorityError(
718                "Input Map Configuration file path must be a valid string.")
719           
720            self.__prop['mapConfigFile'] = mapConfigFilePath
721
722
723        try:
724            tree = ElementTree.parse(self.__prop['mapConfigFile'])
725            rootElem = tree.getroot()
726           
727        except IOError, e:
728            raise AttributeAuthorityConfigError('Error parsing properties file '
729                                          '"%s": %s' % (e.filename,e.strerror))         
730        except Exception, e:
731            raise AttributeAuthorityConfigError('Error parsing Map Configuration '
732                                          'file: "%s": %s' % 
733                                          (self.__prop['mapConfigFile'], e))
734
735           
736        trustedElem = rootElem.findall('trusted')
737        if not trustedElem: 
738            # Make an empty list so that for loop block below is skipped
739            # without an error 
740            trustedElem = ()
741
742        # Dictionaries:
743        # 1) to hold all the data
744        self.__mapConfig = {'thisHost': {}, 'trustedHosts': {}}
745
746        # ... look-up
747        # 2) hosts corresponding to a given role and
748        # 3) roles of external data centre to this data centre
749        self.__localRole2TrustedHost = {}
750        self.__localRole2RemoteRole = {}
751        self.__remoteRole2LocalRole = {}
752
753
754        # Information about this host
755        try:
756            thisHostElem = rootElem.findall('thisHost')[0]
757           
758        except Exception, e:
759            raise AttributeAuthorityConfigError('"thisHost" tag not found in Map '
760                                          'Configuration file "%s"' % 
761                                          self.__prop['mapConfigFile'])
762
763        try:
764            hostName = thisHostElem.attrib.values()[0]
765           
766        except Exception, e:
767            raise AttributeAuthorityConfigError('"name" attribute of "thisHost" '
768                                    'element not found in Map Configuration '
769                                    'file "%s"' % self.__prop['mapConfigFile'])
770
771
772        # hostname is also stored in the AA's config file in the 'name' tag. 
773        # Check the two match as the latter is copied into Attribute
774        # Certificates issued by this AA
775        #
776        # TODO: would be better to rationalise this so that the hostname is
777        # stored in one place only.
778        #
779        # P J Kershaw 14/06/06
780        if hostName != self.__prop['name']:
781            raise AttributeAuthorityError('"name" attribute of "thisHost" element in'
782                                    " Map Configuration file doesn't match "
783                                    '"name" element in properties file.')
784       
785        # Information for THIS Attribute Authority
786        hostDict = {}.fromkeys(('aaURI',
787                                'aaDN',
788                                'loginURI',
789                                'loginServerDN',
790                                'loginRequestServerDN'))
791        self.__mapConfig['thisHost'][hostName] = hostDict.copy()
792        for k in self.__mapConfig['thisHost'][hostName]:
793            self.__mapConfig['thisHost'][hostName][k]=thisHostElem.findtext(k)
794       
795        # Information about trusted hosts
796        for elem in trustedElem:
797            try:
798                trustedHost = elem.attrib.values()[0]
799               
800            except Exception, e:
801                raise AttributeAuthorityError('Error reading trusted host name: %s' %
802                                        e)
803
804           
805            # Add signatureFile and list of roles
806            #
807            # (Currently Optional) additional tag allows query of the URI
808            # where a user would normally login at the trusted host.  Added
809            # this feature to allow users to be forwarded to their home site
810            # if they are accessing a secure resource and are not
811            # authenticated
812            #
813            # P J Kershaw 25/05/06
814            self.__mapConfig['trustedHosts'][trustedHost] = hostDict.copy()
815            for k in self.__mapConfig['trustedHosts'][trustedHost]:
816                self.__mapConfig['trustedHosts'][trustedHost][k] = \
817                                                        elem.findtext(k)   
818
819            roleElem = elem.findall('role')
820            if roleElem:
821                # Role keyword value requires special parsing before
822                # assignment
823                self.__mapConfig['trustedHosts'][trustedHost]['role'] = \
824                                        [dict(i.items()) for i in roleElem]
825            else:
826                # It's possible for trust relationships to not contain any
827                # role mapping.  e.g. a site's login service trusting other
828                # sites login requests
829                self.__mapConfig['trustedHosts'][trustedHost]['role'] = []
830                       
831            self.__localRole2RemoteRole[trustedHost] = {}
832            self.__remoteRole2LocalRole[trustedHost] = {}
833           
834            for role in self.__mapConfig['trustedHosts'][trustedHost]['role']:
835                try:
836                    localRole = role['local']
837                    remoteRole = role['remote']
838                except KeyError, e:
839                    raise AttributeAuthorityError('Reading map config file "%s": no '
840                                            'element "%s" for host "%s"' % \
841                                            (self.__prop['mapConfigFile'], 
842                                             e, 
843                                             trustedHost))
844                   
845                # Role to host look-up
846                if localRole in self.__localRole2TrustedHost:
847                   
848                    if trustedHost not in \
849                       self.__localRole2TrustedHost[localRole]:
850                        self.__localRole2TrustedHost[localRole].\
851                                                        append(trustedHost)                       
852                else:
853                    self.__localRole2TrustedHost[localRole] = [trustedHost]
854
855
856                # Trusted Host to local role and trusted host to trusted role
857                # map look-ups
858                try:
859                    self.__remoteRole2LocalRole[trustedHost][remoteRole].\
860                                                            append(localRole)                 
861                except KeyError:
862                    self.__remoteRole2LocalRole[trustedHost][remoteRole] = \
863                                                                [localRole]
864                   
865                try:
866                    self.__localRole2RemoteRole[trustedHost][localRole].\
867                                                            append(remoteRole)                 
868                except KeyError:
869                    self.__localRole2RemoteRole[trustedHost][localRole] = \
870                                                                [remoteRole]                 
871        log.info('Loaded map configuration file "%s"' % \
872                 self.__prop['mapConfigFile'])
873
874       
875    #_________________________________________________________________________     
876    def userIsRegistered(self, userId):
877        """Check a particular user is registered with the Data Centre that the
878        Attribute Authority represents
879       
880        Nb. this method is not used internally by AttributeAuthority class and is
881        not a required part of the AAUserRoles API
882       
883        @type userId: string
884        @param userId: user identity - could be a X500 Distinguished Name
885        @rtype: bool
886        @return: True if user is registered, False otherwise"""
887        log.debug("Calling userIsRegistered ...")
888        return self.__userRoles.userIsRegistered(userId)
889       
890       
891    #_________________________________________________________________________     
892    def getRoles(self, userId):
893        """Get the roles available to the registered user identified userId.
894
895        @type dn: string
896        @param dn: user identifier - could be a X500 Distinguished Name
897        @return: list of roles for the given user ID"""
898
899        log.debug('Calling getRoles for user "%s" ...' % userId)
900       
901        # Call to AAUserRoles derived class.  Each Attribute Authority
902        # should define it's own roles class derived from AAUserRoles to
903        # define how roles are accessed
904        try:
905            return self.__userRoles.getRoles(userId)
906
907        except Exception, e:
908            raise AttributeAuthorityError("Getting user roles: %s" % e)
909       
910       
911    #_________________________________________________________________________     
912    def __getHostInfo(self):
913        """Return the host that this Attribute Authority represents: its ID,
914        the user login URI and WSDL address.  Call this method via the
915        'hostInfo' property
916       
917        @rtype: dict
918        @return: dictionary of host information derived from the map
919        configuration"""
920       
921        return self.__mapConfig['thisHost']
922       
923    hostInfo = property(fget=__getHostInfo, 
924                        doc="Return information about this host")
925       
926       
927    #_________________________________________________________________________     
928    def getTrustedHostInfo(self, role=None):
929        """Return a dictionary of the hosts that have trust relationships
930        with this AA.  The dictionary is indexed by the trusted host name
931        and contains AA service, login URIs and the roles that map to the
932        given input local role.
933
934        @type role: string
935        @param role: if set, return trusted hosts that having a mapping set
936        for this role.  If no role is input, return all the AA's trusted hosts
937        with all their possible roles
938
939        @rtype: dict
940        @return: dictionary of the hosts that have trust relationships
941        with this AA.  It returns an empty dictionary if role isn't
942        recognised"""
943               
944        log.debug('Calling getTrustedHostInfo with role = "%s" ...' % role) 
945                                 
946        if not self.__mapConfig or not self.__localRole2RemoteRole:
947            # This Attribute Authority has no trusted hosts
948            raise AttributeAuthorityNoTrustedHosts("The %s Attribute Authority has "
949                                             "no trusted hosts" % 
950                                             self.__prop['name'])
951
952
953        if role is None:
954            # No role input - return all trusted hosts with their WSDL URIs
955            # and the remote roles they map to
956            #
957            # Nb. {}.fromkeys([...]).keys() is a fudge to get unique elements
958            # from a list i.e. convert the list elements to a dict eliminating
959            # duplicated elements and convert the keys back into a list.
960            trustedHostInfo = dict(
961            [
962                (
963                    k, 
964                    {
965                        'aaURI':                v['aaURI'], 
966                        'aaDN':                 v['aaDN'], 
967                        'loginURI':             v['loginURI'], 
968                        'loginServerDN':        v['loginServerDN'], 
969                        'loginRequestServerDN': v['loginRequestServerDN'], 
970                        'role':        {}.fromkeys(
971                            [role['remote'] for role in v['role']]
972                        ).keys()
973                    }
974                ) for k, v in self.__mapConfig['trustedHosts'].items()
975            ])
976
977        else:           
978            # Get trusted hosts for given input local role       
979            try:
980                trustedHosts = self.__localRole2TrustedHost[role]
981            except:
982                raise AttributeAuthorityNoMatchingRoleInTrustedHosts(
983                    'None of the trusted hosts have a mapping to the '
984                    'input role "%s"' % role)
985   
986   
987            # Get associated WSDL URI and roles for the trusted hosts
988            # identified and return as a dictionary indexed by host name
989            trustedHostInfo = dict(
990   [(
991        host, 
992        {
993            'aaURI': self.__mapConfig['trustedHosts'][host]['aaURI'],
994            'aaDN': self.__mapConfig['trustedHosts'][host]['aaDN'],
995            'loginURI': self.__mapConfig['trustedHosts'][host]['loginURI'],
996            'loginServerDN': 
997            self.__mapConfig['trustedHosts'][host]['loginServerDN'],
998            'loginRequestServerDN': 
999            self.__mapConfig['trustedHosts'][host]['loginRequestServerDN'],
1000            'role': self.__localRole2RemoteRole[host][role]
1001        }
1002    ) for host in trustedHosts])
1003                         
1004        return trustedHostInfo
1005       
1006       
1007    #_________________________________________________________________________     
1008    def mapRemoteRoles2LocalRoles(self, trustedHost, trustedHostRoles):
1009        """Map roles of trusted hosts to roles for this data centre
1010
1011        @type trustedHost: string
1012        @param trustedHost: name of external trusted data centre
1013        @type trustedHostRoles: list
1014        @param trustedHostRoles:   list of external roles to map
1015        @return: list of mapped roles"""
1016
1017        if not self.__remoteRole2LocalRole:
1018            raise AttributeAuthorityError("Roles map is not set - ensure " 
1019                                    "readMapConfig() has been called.")
1020
1021
1022        # Check the host name is a trusted one recorded in the map
1023        # configuration
1024        if not self.__remoteRole2LocalRole.has_key(trustedHost):
1025            return []
1026
1027        # Add local roles, skipping if no mapping is found
1028        localRoles = []
1029        for trustedRole in trustedHostRoles:
1030            if trustedRole in self.__remoteRole2LocalRole[trustedHost]:
1031                localRoles.extend(\
1032                        self.__remoteRole2LocalRole[trustedHost][trustedRole])
1033               
1034        return localRoles
1035
1036
1037#_____________________________________________________________________________
1038from logging.handlers import RotatingFileHandler
1039
1040#_________________________________________________________________________
1041# Inherit directly from Logger
1042_loggerClass = logging.getLoggerClass()
1043class AttCertLog(_loggerClass, object):
1044    """Log each Attribute Certificate issued using a rotating file handler
1045    so that the number of files held can be managed"""
1046   
1047    def __init__(self, attCertFilePath, backUpCnt=1024):
1048        """Set up a rotating file handler to log ACs issued.
1049        @type attCertFilePath: string
1050        @param attCertFilePath: set where to store ACs.  Set from AttributeAuthority
1051        properties file.
1052       
1053        @type backUpCnt: int
1054        @param backUpCnt: set the number of files to store before rotating
1055        and overwriting old files."""
1056       
1057        # Inherit from Logger class
1058        super(AttCertLog, self).__init__(name='', level=logging.INFO)
1059                           
1060        # Set a format for messages so that only the content of the AC is
1061        # logged, nothing else.
1062        formatter = logging.Formatter(fmt="", datefmt="")
1063
1064        # maxBytes is set to one so that only one AC will be written before
1065        # rotation to the next file
1066        fileLog = RotatingFileHandler(attCertFilePath, 
1067                                      maxBytes=1, 
1068                                      backupCount=backUpCnt)
1069        fileLog.setFormatter(formatter)           
1070        self.addHandler(fileLog)
1071                       
1072#_____________________________________________________________________________
1073class AAUserRolesError(Exception):
1074    """Exception handling for NDG Attribute Authority User Roles interface
1075    class."""
1076
1077
1078#_____________________________________________________________________________
1079class AAUserRoles:
1080    """An abstract base class to define the user roles interface to an
1081    Attribute Authority.
1082
1083    Each NDG data centre should implement a derived class which implements
1084    the way user roles are provided to its representative Attribute Authority.
1085   
1086    Roles are expected to indexed by user Distinguished Name (DN).  They
1087    could be stored in a database or file."""
1088
1089    # User defined class may wish to specify a URI for a database interface or
1090    # path for a user roles configuration file
1091    def __init__(self, dbURI=None, filePath=None):
1092        """User Roles base class - derive from this class to define
1093        roles interface to Attribute Authority
1094       
1095        @type dbURI: string
1096        @param dbURI: database connection URI
1097        @type filePath: string
1098        @param filePath: file path for properties file containing settings
1099        """
1100        pass
1101
1102
1103    def userIsRegistered(self, userId):
1104        """Virtual method - Derived method should return True if user is known
1105        otherwise False
1106       
1107        Nb. this method is not used by AttributeAuthority class and so does NOT need
1108        to be implemented in a derived class.
1109       
1110        @type userId: string
1111        @param userId: user Distinguished Name to look up.
1112        @rtype: bool
1113        @return: True if user is registered, False otherwise"""
1114        raise NotImplementedError(
1115            self.userIsRegistered.__doc__.replace('\n       ',''))
1116
1117
1118    def getRoles(self, userId):
1119        """Virtual method - Derived method should return the roles for the
1120        given user's Id or else raise an exception
1121       
1122        @type userId: string
1123        @param userId: user identity e.g. user Distinguished Name
1124        @rtype: list
1125        @return: list of roles for the given user ID"""
1126        raise NotImplementedError(
1127            self.getRoles.__doc__.replace('\n       ',''))
1128                         
Note: See TracBrowser for help on using the repository browser.