source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/AttAuthority/__init__.py @ 4129

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/AttAuthority/__init__.py@4129
Revision 4129, 44.3 KB checked in by cbyrom, 11 years ago (diff)

General refactoring and updating of code, including:

Removal of refC14nKw and singnedInfoC14nKw keywords in wsssecurity session manager config
(the refC14nInclNS and signedInfoC14nInclNS keywords are sufficient);
Creation of new DOM signature handler class, dom.py, based on the wsSecurity
class;
Abstraction of common code between dom.py and etree.py into new parent
class, BaseSignatureHandler?.py.
Fixing and extending use of properties in the SignatureHandler? code.
Fixing a few bugs with the original SignatureHandler? code.
Updating of test cases to new code/code structure.

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