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

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

Create new utility package with class, ConfigFileParsers? - a utility
class with parsers for XML and INI style config files. This takes
a filename, together with an optional dictionary of valid keys (to
check for invalid config inputs) + optional section list (to restrict
parsing of INI files to particular sections) and returns a
dictionary of read in properties. NB, if valid keys are specified
and not featured in the prop file, default values are set up in the
returned property dict.
Implemented use of the ConfigFileParsers? in the AttAuthority? service.
Added new testsuite (together with noseTests class to drive tests) to
exercise the new parsers in the context of the AttAuthority? section +
added test config files.

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