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

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

Completed tests running Attribute Authority and Session Manager in the same WSGI stack:

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