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

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

ConfigFileParsers?.py: update to the way prefix is handled - if set filter out all params without this prefix
ndg.security.common.logService: removed - old code
SessionManager?: near to completion of refactoring for generic AuthN interface

  • 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        self.__prop = propBranch
262       
263        # Ensure Certificate time parameters are converted to numeric type
264        self.__prop['attCertLifetime'] = float(self.__prop['attCertLifetime'])
265        self.__prop['attCertNotBeforeOff'] = \
266                                    float(self.__prop['attCertNotBeforeOff'])
267
268        # Check directory path
269        try:
270            dirList = os.listdir(self.__prop['attCertDir'])
271
272        except OSError, osError:
273            raise AttributeAuthorityConfigError('Invalid directory path Attribute '
274                                    'Certificates store "%s": %s' % \
275                                    (self.__prop['attCertDir'], 
276                                     osError.strerror))
277
278       
279    # Methods for Attribute Authority dictionary like behaviour       
280    def __repr__(self):
281        """Return file properties dictionary as representation"""
282        return repr(self.__prop)
283   
284    def __delitem__(self, key):
285        AttributeAuthority.__name__ + " keys cannot be removed"       
286        raise KeyError('Keys cannot be deleted from '+AttributeAuthority.__name__)
287
288
289    def __getitem__(self, key):
290        AttributeAuthority.__name__ + """ behaves as data dictionary of Attribute
291        Authority properties
292        """
293        if key not in self.__prop:
294            raise KeyError("Invalid key '%s'" % key)
295       
296        return self.__prop[key]
297       
298    def get(self, kw):
299        return self.__prop.get(kw)
300   
301    def clear(self):
302        raise KeyError("Data cannot be cleared from "+AttributeAuthority.__name__)
303   
304    def keys(self):
305        return self.__prop.keys()
306
307    def items(self):
308        return self.__prop.items()
309
310    def values(self):
311        return self.__prop.values()
312
313    def has_key(self, key):
314        return self.__prop.has_key(key)
315
316    # 'in' operator
317    def __contains__(self, key):
318        return key in self.__prop
319
320
321    def setPropFilePath(self, val=None):
322        """Set properties file from input or based on environment variable
323        settings
324       
325        @type val: basestring
326        @param val: properties file path"""
327        log.debug("Setting property file path")
328        if not val:
329            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
330                val = os.environ['NDGSEC_AA_PROPFILEPATH']
331               
332            elif 'NDGSEC_DIR' in os.environ:
333                val = os.path.join(os.environ['NDGSEC_DIR'], 
334                                   AttributeAuthority._confDir,
335                                   AttributeAuthority._propFileName)
336            else:
337                raise AttributeError('Unable to set default Attribute '
338                                     'Authority properties file path: neither '
339                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
340                                     ' environment variables are set')
341               
342        if not isinstance(val, basestring):
343            raise AttributeError("Input Properties file path "
344                                 "must be a valid string.")
345     
346        self._propFilePath = os.path.expandvars(val)
347        log.debug("Path set to: %s" % val)
348       
349    def getPropFilePath(self):
350        '''Get the properties file path
351       
352        @rtype: basestring
353        @return: properties file path'''
354        log.debug("Getting property file path")
355        if hasattr(self, '_propFilePath'):
356            return self._propFilePath
357        else:
358            return ""
359       
360    # Also set up as a property
361    propFilePath = property(fset=setPropFilePath,
362                            fget=getPropFilePath,
363                            doc="Set the path to the properties file")   
364   
365    def setPropFileSection(self, val=None):
366        """Set section name to read properties from ini file.  This is set from
367        input or based on environment variable setting
368        NDGSEC_AA_PROPFILESECTION
369       
370        @type val: basestring
371        @param val: section name"""
372        log.debug("Setting property file section name")
373        if not val:
374            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
375               
376        if not isinstance(val, basestring):
377            raise AttributeError("Input Properties file section name "
378                                 "must be a valid string.")
379     
380        self._propFileSection = val
381        log.debug("Properties file section set to: %s" % val)
382       
383    def getPropFileSection(self):
384        '''Get the section name to extract properties from an ini file -
385        DOES NOT apply to XML file properties
386       
387        @rtype: basestring
388        @return: section name'''
389        log.debug("Getting property file section name")
390        if hasattr(self, '_propFileSection'):
391            return self._propFileSection
392        else:
393            return ""   
394       
395    # Also set up as a property
396    propFileSection = property(fset=setPropFileSection,
397                    fget=getPropFileSection,
398                    doc="Set the file section name for ini file properties")   
399   
400    def setPropPrefix(self, val=None):
401        """Set prefix for properties read from ini file.  This is set from
402        input or based on environment variable setting
403        NDGSEC_AA_PROPFILEPREFIX
404       
405        DOES NOT apply to XML file properties
406       
407        @type val: basestring
408        @param val: section name"""
409        log.debug("Setting property file section name")
410        if val is None:
411            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
412               
413        if not isinstance(val, basestring):
414            raise AttributeError("Input Properties file section name "
415                                 "must be a valid string.")
416     
417        self._propPrefix = val
418        log.debug("Properties file section set to: %s" % val)
419       
420    def getPropPrefix(self):
421        '''Get the prefix name used for properties in an ini file -
422        DOES NOT apply to XML file properties
423       
424        @rtype: basestring
425        @return: section name'''
426        log.debug("Getting property file prefix")
427        if hasattr(self, '_propPrefix'):
428            return self._propPrefix
429        else:
430            return ""   
431       
432    # Also set up as a property
433    propPrefix = property(fset=setPropPrefix,
434                          fget=getPropPrefix,
435                          doc="Set a prefix for ini file properties")   
436
437    def getAttCert(self,
438                   userId=None,
439                   holderCert=None,
440                   holderCertFilePath=None,
441                   userAttCert=None,
442                   userAttCertFilePath=None):
443
444        """Request a new Attribute Certificate for use in authorisation
445
446        getAttCert([userId=uid][holderCert=px|holderCertFilePath=pxFile, ]
447                   [userAttCert=cert|userAttCertFilePath=certFile])
448         
449        @type userId: string
450        @param userId: identifier for the user who is entitled to the roles
451        in the certificate that is issued.  If this keyword is omitted, then
452        the userId will be set to the DN of the holder.
453       
454        holder = the holder of the certificate - an inidividual user or an
455        organisation to which the user belongs who vouches for that user's ID
456       
457        userId = the identifier for the user who is entitled to the roles
458        specified in the Attribute Certificate that is issued.
459                 
460        @type holderCert: string / ndg.security.common.X509.X509Cert type
461        @param holderCert: base64 encoded string containing proxy cert./
462        X.509 cert object corresponding to the ID who will be the HOLDER of
463        the Attribute Certificate that will be issued.  - Normally, using
464        proxy certificates, the holder and user ID are the same but there
465        may be cases where the holder will be an organisation ID.  This is the
466        case for NDG security with the DEWS project
467       
468        @param holderCertFilePath: string
469        @param holderCertFilePath: file path to proxy/X.509 certificate of
470        candidate holder
471     
472        @type userAttCert: string or AttCert type
473        @param userAttCert: externally provided attribute certificate from
474        another data centre.  This is only necessary if the user is not
475        registered with this attribute authority.
476                       
477        @type userAttCertFilePath: string
478        @param userAttCertFilePath: alternative to userAttCert except pass
479        in as a file path to an attribute certificate instead.
480       
481        @rtype: AttCert
482        @return: new attribute certificate"""
483
484        log.debug("Calling getAttCert ...")
485       
486        # Read X.509 certificate
487        try:           
488            if holderCertFilePath is not None:
489                                   
490                # Certificate input as a file
491                holderCert = X509Cert()
492                holderCert.read(holderCertFilePath)
493               
494            elif isinstance(holderCert, basestring):
495
496                # Certificate input as string text
497                holderCert = X509CertParse(holderCert)
498               
499            elif not isinstance(holderCert, X509Cert):
500                raise AttributeAuthorityError("No input file path or cert text/"
501                                        "object set")
502           
503        except Exception, e:
504            raise AttributeAuthorityError("User X.509 certificate: %s" % e)
505
506
507        # Check certificate hasn't expired
508        log.debug("Checking client request X.509 certificate ...")
509        try:
510            holderCert.isValidTime(raiseExcep=True)
511           
512        except Exception, e:
513            raise AttributeAuthorityError("User X.509 certificate is invalid: " + \
514                                    str(e))
515
516           
517        # Get Distinguished name from certificate as an X500DN type
518        if not userId:
519            try:
520                userId = holderCert.dn.serialise(\
521                                         separator=self.__prop['dnSeparator']) 
522            except Exception, e:
523                raise AttributeAuthorityError("Setting user Id from holder "
524                                        "certificate DN: %s" % e)
525       
526        # Make a new Attribute Certificate instance passing in certificate
527        # details for later signing
528        attCert = AttCert()
529
530        # First cert in list corresponds to the private key
531        attCert.certFilePathList = [self.__prop['signingCertFilePath']] + \
532                                                                self.__prop['caCertFilePathList']
533                                                               
534        attCert.signingKeyFilePath = self.__prop['signingPriKeyFilePath']
535        attCert.signingKeyPwd = self.__prop['signingPriKeyPwd']
536       
537       
538        # Set holder's (user's) Distinguished Name
539        try:
540            attCert['holder'] = \
541                holderCert.dn.serialise(separator=self.__prop['dnSeparator'])           
542        except Exception, e:
543            raise AttributeAuthorityError("Holder DN: %s" % e)
544
545       
546        # Set Issuer details from Attribute Authority
547        issuerDN = self.__cert.dn
548        try:
549            attCert['issuer'] = \
550                    issuerDN.serialise(separator=self.__prop['dnSeparator'])           
551        except Exception, e:
552            raise AttributeAuthorityError("Issuer DN: %s" % e)
553       
554        attCert['issuerName'] = self.__prop['name']
555        attCert['issuerSerialNumber'] = self.__issuerSerialNumber
556
557        attCert['userId'] = userId
558       
559        # Set validity time
560        try:
561            attCert.setValidityTime(\
562                        lifetime=self.__prop['attCertLifetime'],
563                        notBeforeOffset=self.__prop['attCertNotBeforeOff'])
564
565            # Check against the certificate's expiry
566            dtHolderCertNotAfter = holderCert.notAfter
567           
568            if attCert.getValidityNotAfter(asDatetime=True) > \
569               dtHolderCertNotAfter:
570
571                # Adjust the attribute certificate's expiry date time
572                # so that it agrees with that of the certificate
573                # ... but also make ensure that the not before skew is still
574                # applied
575                attCert.setValidityTime(dtNotAfter=dtHolderCertNotAfter,
576                        notBeforeOffset=self.__prop['attCertNotBeforeOff'])
577           
578        except Exception, e:
579            raise AttributeAuthorityError("Error setting validity time: %s" % e)
580       
581
582        # Check name is registered with this Attribute Authority - if no
583        # user roles are found, the user is not registered
584        userRoles = self.getRoles(userId)
585        if userRoles:           
586            # Set as an Original Certificate
587            #
588            # User roles found - user is registered with this data centre
589            # Add roles for this user for this data centre
590            attCert.addRoles(userRoles)
591
592            # Mark new Attribute Certificate as an original
593            attCert['provenance'] = AttCert.origProvenance
594
595        else:           
596            # Set as a Mapped Certificate
597            #
598            # No roles found - user is not registered with this data centre
599            # Check for an externally provided certificate from another
600            # trusted data centre
601            if userAttCertFilePath:
602               
603                # Read externally provided certificate
604                try:
605                    userAttCert = AttCertRead(userAttCertFilePath)
606                   
607                except Exception, e:
608                    raise AttributeAuthorityError("Reading external Attribute "
609                                            "Certificate: %s" % e)                           
610            elif userAttCert:
611                # Allow input as a string but convert to
612                if isinstance(userAttCert, basestring):
613                    userAttCert = AttCertParse(userAttCert)
614                   
615                elif not isinstance(userAttCert, AttCert):
616                    raise AttributeAuthorityError(
617                        "Expecting userAttCert as a string or AttCert type")       
618            else:
619                raise AttributeAuthorityAccessDenied("User \"%s\" is not registered "
620                                               "and no external attribute "
621                                               "certificate is available to "
622                                               "make a mapping." % userId)
623
624
625            # Check it's an original certificate - mapped certificates can't
626            # be used to make further mappings
627            if userAttCert.isMapped():
628                raise AttributeAuthorityError("External Attribute Certificate must "
629                                        "have an original provenance in order "
630                                        "to make further mappings.")
631
632
633            # Check it's valid and signed
634            try:
635                # Give path to CA cert to allow check
636                userAttCert.certFilePathList=self.__prop['caCertFilePathList']
637                userAttCert.isValid(raiseExcep=True)
638               
639            except Exception, e:
640                raise AttributeAuthorityError("Invalid Remote Attribute "
641                                        "Certificate: " + str(e))       
642
643
644            # Check that's it's holder matches the candidate holder
645            # certificate DN
646            if userAttCert.holderDN != holderCert.dn:
647                raise AttributeAuthorityError("User certificate and Attribute "
648                                        'Certificate DNs don\'t match: "%s"'
649                                        ' and "%s"' % (holderCert.dn, 
650                                                       userAttCert.holderDN))
651           
652 
653            # Get roles from external Attribute Certificate
654            trustedHostRoles = userAttCert.roles
655
656
657            # Map external roles to local ones
658            localRoles = self.mapRemoteRoles2LocalRoles(\
659                                                    userAttCert['issuerName'],
660                                                    trustedHostRoles)
661            if not localRoles:
662                raise AttributeAuthorityAccessDenied("No local roles mapped to the "
663                                               "%s roles: %s" % \
664                                               (userAttCert['issuerName'], 
665                                                ', '.join(trustedHostRoles)))
666
667            attCert.addRoles(localRoles)
668           
669           
670            # Mark new Attribute Certificate as mapped
671            attCert.provenance = AttCert.mappedProvenance
672
673            # Copy the user Id from the external AC
674            attCert.userId = userAttCert.userId
675           
676            # End set mapped certificate block
677
678        try:
679            # Digitally sign certificate using Attribute Authority's
680            # certificate and private key
681            attCert.applyEnvelopedSignature()
682           
683            # Check the certificate is valid
684            attCert.isValid(raiseExcep=True)
685           
686            # Write out certificate to keep a record of it for auditing
687            #attCert.write()
688            self.__attCertLog.info(attCert)
689           
690            log.info('Issued an Attribute Certificate to "%s" with roles: '
691                     '"%s"' % (userId, '", "'.join(attCert.roles)))
692
693            # Return the cert to caller
694            return attCert
695       
696        except Exception, e:
697            raise AttributeAuthorityError("New Attribute Certificate \"%s\": %s" % \
698                                    (attCert.filePath, e))
699       
700       
701    #_________________________________________________________________________     
702    def readMapConfig(self, mapConfigFilePath=None):
703        """Parse Map Configuration file.
704
705        @type mapConfigFilePath: string
706        @param mapConfigFilePath: file path for map configuration file.  If
707        omitted, it uses member variable __prop['mapConfigFile'].
708        """
709       
710        log.debug("Reading map configuration file ...")
711       
712        if mapConfigFilePath is not None:
713            if not isinstance(mapConfigFilePath, basestring):
714                raise AttributeAuthorityError(
715                "Input Map Configuration file path must be a valid string.")
716           
717            self.__prop['mapConfigFile'] = mapConfigFilePath
718
719
720        try:
721            tree = ElementTree.parse(self.__prop['mapConfigFile'])
722            rootElem = tree.getroot()
723           
724        except IOError, e:
725            raise AttributeAuthorityConfigError('Error parsing properties file '
726                                          '"%s": %s' % (e.filename,e.strerror))         
727        except Exception, e:
728            raise AttributeAuthorityConfigError('Error parsing Map Configuration '
729                                          'file: "%s": %s' % 
730                                          (self.__prop['mapConfigFile'], e))
731
732           
733        trustedElem = rootElem.findall('trusted')
734        if not trustedElem: 
735            # Make an empty list so that for loop block below is skipped
736            # without an error 
737            trustedElem = ()
738
739        # Dictionaries:
740        # 1) to hold all the data
741        self.__mapConfig = {'thisHost': {}, 'trustedHosts': {}}
742
743        # ... look-up
744        # 2) hosts corresponding to a given role and
745        # 3) roles of external data centre to this data centre
746        self.__localRole2TrustedHost = {}
747        self.__localRole2RemoteRole = {}
748        self.__remoteRole2LocalRole = {}
749
750
751        # Information about this host
752        try:
753            thisHostElem = rootElem.findall('thisHost')[0]
754           
755        except Exception, e:
756            raise AttributeAuthorityConfigError('"thisHost" tag not found in Map '
757                                          'Configuration file "%s"' % 
758                                          self.__prop['mapConfigFile'])
759
760        try:
761            hostName = thisHostElem.attrib.values()[0]
762           
763        except Exception, e:
764            raise AttributeAuthorityConfigError('"name" attribute of "thisHost" '
765                                    'element not found in Map Configuration '
766                                    'file "%s"' % self.__prop['mapConfigFile'])
767
768
769        # hostname is also stored in the AA's config file in the 'name' tag. 
770        # Check the two match as the latter is copied into Attribute
771        # Certificates issued by this AA
772        #
773        # TODO: would be better to rationalise this so that the hostname is
774        # stored in one place only.
775        #
776        # P J Kershaw 14/06/06
777        if hostName != self.__prop['name']:
778            raise AttributeAuthorityError('"name" attribute of "thisHost" element in'
779                                    " Map Configuration file doesn't match "
780                                    '"name" element in properties file.')
781       
782        # Information for THIS Attribute Authority
783        hostDict = {}.fromkeys(('aaURI',
784                                'aaDN',
785                                'loginURI',
786                                'loginServerDN',
787                                'loginRequestServerDN'))
788        self.__mapConfig['thisHost'][hostName] = hostDict.copy()
789        for k in self.__mapConfig['thisHost'][hostName]:
790            self.__mapConfig['thisHost'][hostName][k]=thisHostElem.findtext(k)
791       
792        # Information about trusted hosts
793        for elem in trustedElem:
794            try:
795                trustedHost = elem.attrib.values()[0]
796               
797            except Exception, e:
798                raise AttributeAuthorityError('Error reading trusted host name: %s' %
799                                        e)
800
801           
802            # Add signatureFile and list of roles
803            #
804            # (Currently Optional) additional tag allows query of the URI
805            # where a user would normally login at the trusted host.  Added
806            # this feature to allow users to be forwarded to their home site
807            # if they are accessing a secure resource and are not
808            # authenticated
809            #
810            # P J Kershaw 25/05/06
811            self.__mapConfig['trustedHosts'][trustedHost] = hostDict.copy()
812            for k in self.__mapConfig['trustedHosts'][trustedHost]:
813                self.__mapConfig['trustedHosts'][trustedHost][k] = \
814                                                        elem.findtext(k)   
815
816            roleElem = elem.findall('role')
817            if roleElem:
818                # Role keyword value requires special parsing before
819                # assignment
820                self.__mapConfig['trustedHosts'][trustedHost]['role'] = \
821                                        [dict(i.items()) for i in roleElem]
822            else:
823                # It's possible for trust relationships to not contain any
824                # role mapping.  e.g. a site's login service trusting other
825                # sites login requests
826                self.__mapConfig['trustedHosts'][trustedHost]['role'] = []
827                       
828            self.__localRole2RemoteRole[trustedHost] = {}
829            self.__remoteRole2LocalRole[trustedHost] = {}
830           
831            for role in self.__mapConfig['trustedHosts'][trustedHost]['role']:
832                try:
833                    localRole = role['local']
834                    remoteRole = role['remote']
835                except KeyError, e:
836                    raise AttributeAuthorityError('Reading map config file "%s": no '
837                                            'element "%s" for host "%s"' % \
838                                            (self.__prop['mapConfigFile'], 
839                                             e, 
840                                             trustedHost))
841                   
842                # Role to host look-up
843                if localRole in self.__localRole2TrustedHost:
844                   
845                    if trustedHost not in \
846                       self.__localRole2TrustedHost[localRole]:
847                        self.__localRole2TrustedHost[localRole].\
848                                                        append(trustedHost)                       
849                else:
850                    self.__localRole2TrustedHost[localRole] = [trustedHost]
851
852
853                # Trusted Host to local role and trusted host to trusted role
854                # map look-ups
855                try:
856                    self.__remoteRole2LocalRole[trustedHost][remoteRole].\
857                                                            append(localRole)                 
858                except KeyError:
859                    self.__remoteRole2LocalRole[trustedHost][remoteRole] = \
860                                                                [localRole]
861                   
862                try:
863                    self.__localRole2RemoteRole[trustedHost][localRole].\
864                                                            append(remoteRole)                 
865                except KeyError:
866                    self.__localRole2RemoteRole[trustedHost][localRole] = \
867                                                                [remoteRole]                 
868        log.info('Loaded map configuration file "%s"' % \
869                 self.__prop['mapConfigFile'])
870
871       
872    #_________________________________________________________________________     
873    def userIsRegistered(self, userId):
874        """Check a particular user is registered with the Data Centre that the
875        Attribute Authority represents
876       
877        Nb. this method is not used internally by AttributeAuthority class and is
878        not a required part of the AAUserRoles API
879       
880        @type userId: string
881        @param userId: user identity - could be a X500 Distinguished Name
882        @rtype: bool
883        @return: True if user is registered, False otherwise"""
884        log.debug("Calling userIsRegistered ...")
885        return self.__userRoles.userIsRegistered(userId)
886       
887       
888    #_________________________________________________________________________     
889    def getRoles(self, userId):
890        """Get the roles available to the registered user identified userId.
891
892        @type dn: string
893        @param dn: user identifier - could be a X500 Distinguished Name
894        @return: list of roles for the given user ID"""
895
896        log.debug('Calling getRoles for user "%s" ...' % userId)
897       
898        # Call to AAUserRoles derived class.  Each Attribute Authority
899        # should define it's own roles class derived from AAUserRoles to
900        # define how roles are accessed
901        try:
902            return self.__userRoles.getRoles(userId)
903
904        except Exception, e:
905            raise AttributeAuthorityError("Getting user roles: %s" % e)
906       
907       
908    #_________________________________________________________________________     
909    def __getHostInfo(self):
910        """Return the host that this Attribute Authority represents: its ID,
911        the user login URI and WSDL address.  Call this method via the
912        'hostInfo' property
913       
914        @rtype: dict
915        @return: dictionary of host information derived from the map
916        configuration"""
917       
918        return self.__mapConfig['thisHost']
919       
920    hostInfo = property(fget=__getHostInfo, 
921                        doc="Return information about this host")
922       
923       
924    #_________________________________________________________________________     
925    def getTrustedHostInfo(self, role=None):
926        """Return a dictionary of the hosts that have trust relationships
927        with this AA.  The dictionary is indexed by the trusted host name
928        and contains AA service, login URIs and the roles that map to the
929        given input local role.
930
931        @type role: string
932        @param role: if set, return trusted hosts that having a mapping set
933        for this role.  If no role is input, return all the AA's trusted hosts
934        with all their possible roles
935
936        @rtype: dict
937        @return: dictionary of the hosts that have trust relationships
938        with this AA.  It returns an empty dictionary if role isn't
939        recognised"""
940               
941        log.debug('Calling getTrustedHostInfo with role = "%s" ...' % role) 
942                                 
943        if not self.__mapConfig or not self.__localRole2RemoteRole:
944            # This Attribute Authority has no trusted hosts
945            raise AttributeAuthorityNoTrustedHosts("The %s Attribute Authority has "
946                                             "no trusted hosts" % 
947                                             self.__prop['name'])
948
949
950        if role is None:
951            # No role input - return all trusted hosts with their WSDL URIs
952            # and the remote roles they map to
953            #
954            # Nb. {}.fromkeys([...]).keys() is a fudge to get unique elements
955            # from a list i.e. convert the list elements to a dict eliminating
956            # duplicated elements and convert the keys back into a list.
957            trustedHostInfo = dict(
958            [
959                (
960                    k, 
961                    {
962                        'aaURI':                v['aaURI'], 
963                        'aaDN':                 v['aaDN'], 
964                        'loginURI':             v['loginURI'], 
965                        'loginServerDN':        v['loginServerDN'], 
966                        'loginRequestServerDN': v['loginRequestServerDN'], 
967                        'role':        {}.fromkeys(
968                            [role['remote'] for role in v['role']]
969                        ).keys()
970                    }
971                ) for k, v in self.__mapConfig['trustedHosts'].items()
972            ])
973
974        else:           
975            # Get trusted hosts for given input local role       
976            try:
977                trustedHosts = self.__localRole2TrustedHost[role]
978            except:
979                raise AttributeAuthorityNoMatchingRoleInTrustedHosts(
980                    'None of the trusted hosts have a mapping to the '
981                    'input role "%s"' % role)
982   
983   
984            # Get associated WSDL URI and roles for the trusted hosts
985            # identified and return as a dictionary indexed by host name
986            trustedHostInfo = dict(
987   [(
988        host, 
989        {
990            'aaURI': self.__mapConfig['trustedHosts'][host]['aaURI'],
991            'aaDN': self.__mapConfig['trustedHosts'][host]['aaDN'],
992            'loginURI': self.__mapConfig['trustedHosts'][host]['loginURI'],
993            'loginServerDN': 
994            self.__mapConfig['trustedHosts'][host]['loginServerDN'],
995            'loginRequestServerDN': 
996            self.__mapConfig['trustedHosts'][host]['loginRequestServerDN'],
997            'role': self.__localRole2RemoteRole[host][role]
998        }
999    ) for host in trustedHosts])
1000                         
1001        return trustedHostInfo
1002       
1003       
1004    #_________________________________________________________________________     
1005    def mapRemoteRoles2LocalRoles(self, trustedHost, trustedHostRoles):
1006        """Map roles of trusted hosts to roles for this data centre
1007
1008        @type trustedHost: string
1009        @param trustedHost: name of external trusted data centre
1010        @type trustedHostRoles: list
1011        @param trustedHostRoles:   list of external roles to map
1012        @return: list of mapped roles"""
1013
1014        if not self.__remoteRole2LocalRole:
1015            raise AttributeAuthorityError("Roles map is not set - ensure " 
1016                                    "readMapConfig() has been called.")
1017
1018
1019        # Check the host name is a trusted one recorded in the map
1020        # configuration
1021        if not self.__remoteRole2LocalRole.has_key(trustedHost):
1022            return []
1023
1024        # Add local roles, skipping if no mapping is found
1025        localRoles = []
1026        for trustedRole in trustedHostRoles:
1027            if trustedRole in self.__remoteRole2LocalRole[trustedHost]:
1028                localRoles.extend(\
1029                        self.__remoteRole2LocalRole[trustedHost][trustedRole])
1030               
1031        return localRoles
1032
1033
1034#_____________________________________________________________________________
1035from logging.handlers import RotatingFileHandler
1036
1037#_________________________________________________________________________
1038# Inherit directly from Logger
1039_loggerClass = logging.getLoggerClass()
1040class AttCertLog(_loggerClass, object):
1041    """Log each Attribute Certificate issued using a rotating file handler
1042    so that the number of files held can be managed"""
1043   
1044    def __init__(self, attCertFilePath, backUpCnt=1024):
1045        """Set up a rotating file handler to log ACs issued.
1046        @type attCertFilePath: string
1047        @param attCertFilePath: set where to store ACs.  Set from AttributeAuthority
1048        properties file.
1049       
1050        @type backUpCnt: int
1051        @param backUpCnt: set the number of files to store before rotating
1052        and overwriting old files."""
1053       
1054        # Inherit from Logger class
1055        super(AttCertLog, self).__init__(name='', level=logging.INFO)
1056                           
1057        # Set a format for messages so that only the content of the AC is
1058        # logged, nothing else.
1059        formatter = logging.Formatter(fmt="", datefmt="")
1060
1061        # maxBytes is set to one so that only one AC will be written before
1062        # rotation to the next file
1063        fileLog = RotatingFileHandler(attCertFilePath, 
1064                                      maxBytes=1, 
1065                                      backupCount=backUpCnt)
1066        fileLog.setFormatter(formatter)           
1067        self.addHandler(fileLog)
1068                       
1069#_____________________________________________________________________________
1070class AAUserRolesError(Exception):
1071    """Exception handling for NDG Attribute Authority User Roles interface
1072    class."""
1073
1074
1075#_____________________________________________________________________________
1076class AAUserRoles:
1077    """An abstract base class to define the user roles interface to an
1078    Attribute Authority.
1079
1080    Each NDG data centre should implement a derived class which implements
1081    the way user roles are provided to its representative Attribute Authority.
1082   
1083    Roles are expected to indexed by user Distinguished Name (DN).  They
1084    could be stored in a database or file."""
1085
1086    # User defined class may wish to specify a URI for a database interface or
1087    # path for a user roles configuration file
1088    def __init__(self, dbURI=None, filePath=None):
1089        """User Roles base class - derive from this class to define
1090        roles interface to Attribute Authority
1091       
1092        @type dbURI: string
1093        @param dbURI: database connection URI
1094        @type filePath: string
1095        @param filePath: file path for properties file containing settings
1096        """
1097        pass
1098
1099
1100    def userIsRegistered(self, userId):
1101        """Virtual method - Derived method should return True if user is known
1102        otherwise False
1103       
1104        Nb. this method is not used by AttributeAuthority class and so does NOT need
1105        to be implemented in a derived class.
1106       
1107        @type userId: string
1108        @param userId: user Distinguished Name to look up.
1109        @rtype: bool
1110        @return: True if user is registered, False otherwise"""
1111        raise NotImplementedError(
1112            self.userIsRegistered.__doc__.replace('\n       ',''))
1113
1114
1115    def getRoles(self, userId):
1116        """Virtual method - Derived method should return the roles for the
1117        given user's Id or else raise an exception
1118       
1119        @type userId: string
1120        @param userId: user identity e.g. user Distinguished Name
1121        @rtype: list
1122        @return: list of roles for the given user ID"""
1123        raise NotImplementedError(
1124            self.getRoles.__doc__.replace('\n       ',''))
1125                         
Note: See TracBrowser for help on using the repository browser.