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

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

Fix problem with search and replace licence not adding a new line.

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