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

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

Further standardise property keywords - consolidating caCertFileList into
caCertFilePathList.

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