source: TI12-security/trunk/python/NDG/AttAuthority.py @ 661

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/NDG/AttAuthority.py@661
Revision 661, 27.1 KB checked in by pjkersha, 14 years ago (diff)

AttAuthority?.py: fixed refs userAttCertTxt -> userAttCert in get mapped certificate
block of code - not tested for some time

SessionMgrIO.py, SessionClient?.py, CredWallet?.py:
Renamed keyword/XML tag "setExtAttCertList" -> "rtnExtAttCertList"

Session.py: UserSession?.setCookie - set domain to .rl.ac.uk - will need to change to make
configurable.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1"""NDG Attribute Authority handles security authentication and authorization
2
3NERC Data Grid Project
4
5P J Kershaw 15/04/05
6
7Copyright (C) 2005 CCLRC & NERC
8
9This software may be distributed under the terms of the Q Public License,
10version 1.0 or later.
11"""
12
13cvsID = '$Id$'
14
15import types
16
17
18# Create unique names for attribute certificates
19import tempfile
20import os
21
22# Alter system path for dynamic import of user roles class
23import sys
24
25# For parsing of properties file
26import cElementTree as ElementTree
27
28# X509 Certificate handling
29from X509 import *
30
31# NDG Attribute Certificate
32from AttCert import *
33
34# Format for XML messages passed over WS
35from AttAuthorityIO import *
36
37
38#_____________________________________________________________________________
39class AttAuthorityError(Exception):
40    """Exception handling for NDG Attribute Authority class."""
41   
42    def __init__(self, msg):
43        self.__msg = msg
44         
45    def __str__(self):
46        return self.__msg
47
48
49
50
51#_____________________________________________________________________________
52class AttAuthorityAccessDenied(AttAuthorityError):
53    """NDG Attribute Authority - access denied exception.
54
55    Raise from authorise method where no roles are available for the user
56    but that the request is otherwise valid.  In all other error cases raise
57    AttAuthorityError"""   
58    pass
59
60
61
62#_____________________________________________________________________________
63class AttAuthority:
64
65    """NDG Attribute Authority - server for user authentication/authorization.
66    """
67
68    # Code designed from NERC Data Grid Enterprise and Information Viewpoint
69    # documents.
70    #
71    # Also, draws from Neil Bennett's ACServer class used in the Java
72    # implementation of NDG Security
73
74    # valid configuration property keywords
75    __validKeys = [ 'name',
76                    'keyFile',
77                    'keyPwd',
78                    'certFile',
79                    'caCertFile',
80                    'attCertLifeTime',
81                    'attCertNotBeforeOff',
82                    'attCertFilePfx',
83                    'attCertFileSfx',
84                    'mapConfigFile',
85                    'attCertDir',
86                    'dnSeparator',
87                    'usrRolesModFilePath',
88                    'usrRolesModName',
89                    'usrRolesClassName',
90                    'usrRolesPropFile']
91   
92    def __init__(self, propFilePath, bReadMapConfig=True):
93        """Create new NDG Attribute Authority instance
94
95        propFilePath:   path to file containing Attribute Authority
96                        configuration parameters.
97        bReadMapConfig: by default the Map Configuration file is read.  Set
98                        this flag to False to override.
99        """
100       
101        if not isinstance(propFilePath, basestring):
102            raise AttAuthorityError("Input Properties file path " + \
103                                    "must be a valid string.")
104
105
106        # Initialise role mapping look-ups - These are set in readMapConfig()
107        self.__mapConfig = None
108        self.__localRole2Trusted = None
109        self.__trusted2LocalRole = None
110
111
112        # Configuration file properties are held together in a dictionary
113        self.__prop = {}
114
115        # Read Attribute Authority Properties file
116        self.readProperties(propFilePath)
117
118        # Read the Map Configuration file
119        if bReadMapConfig:
120            self.readMapConfig()
121
122        # Instantiate Certificate object
123        self.__cert = X509Cert(self.__prop['certFile'])
124        self.__cert.read()
125
126        # Check it's valid
127        if not self.__cert.isValidTime():
128            raise AttAuthorityError(\
129                "Attribute Authority's certificate has expired!")
130       
131        # Check CA certificate
132        caCert = X509Cert(self.__prop['caCertFile'])
133        caCert.read()
134       
135        if not caCert.isValidTime():
136            raise AttAuthorityError("CA certificate has expired!")
137
138       
139        # Issuer details - serialise using the separator string set in the
140        # properties file
141        self.__issuer = \
142            self.__cert.dn.serialise(separator=self.__prop['dnSeparator'])
143
144        self.__issuerSerialNumber = self.__cert.serialNumber
145
146       
147        # Set-up user roles interface
148        try:
149            # Temporarily extend system path ready for import
150            sysPathBak = sys.path
151            sys.path.append(self.__prop['usrRolesModFilePath'])
152           
153            # Import module name specified in properties file
154            usrRolesMod = __import__(self.__prop['usrRolesModName'],
155                                     globals(),
156                                     locals(),
157                                     [self.__prop['usrRolesClassName']])
158
159            sys.path = sysPathBak
160           
161            usrRolesClass = eval('usrRolesMod.' + \
162                                 self.__prop['usrRolesClassName'])
163
164        except Exception, e:
165            raise AttAuthorityError('Importing User Roles module: %s' % e)
166
167        # Check class inherits from AAUserRoles abstract base class
168        if not issubclass(usrRolesClass, AAUserRoles):
169            raise AttAuthorityError(\
170                "User Roles class %s must be derived from AAUserRoles" % \
171                self.__prop['usrRolesClassName'])
172
173
174        # Instantiate custom class
175        try:
176            self.__usrRoles = usrRolesClass(self.__prop['usrRolesPropFile'])
177           
178        except Exception, e:
179            raise AttAuthorityError(\
180                "Error instantiating User Roles interface: " + str(e))
181
182       
183    #_________________________________________________________________________
184    def authorise(self,
185                  reqXMLtxt=None, 
186                  proxyCertFilePath=None,
187                  userAttCertFilePath=None,
188                  **reqKeys):
189
190        """Request a new Attribute Certificate for authorisation
191
192        proxyCertFilePath|proxyCert:
193
194                                user's proxy certificate use appropriate
195                                keyword for input as a file path or as the
196                                text content respectively.
197                               
198                                Nb. proxyCert is set via reqKeys
199                               
200        userAttCertFilePath|userAttCert:
201       
202                                externally provided attribute certificate
203                                from another data centre.  This is only
204                                necessary if the user is not registered with
205                                this attribute authority.
206
207                                Pass in either the file path or a string
208                                containing the certificate XML content.
209                               
210                                Nb. userAttCert is set via reqKeys
211                                """
212
213        if reqXMLtxt is not None:
214            # Parse XML text into keywords corresponding to the input
215            # parameters
216            if not isinstance(reqXMLtxt, basestring):
217                raise SessionMgrError(\
218                            "XML Authorisation request must be a string")
219                                       
220            # Parse and decrypt as necessary
221            try:
222                # 1st assume that the request was encrypted
223                reqKeys = AuthorisationReq(encrXMLtxt=reqXMLtxt,
224                                    encrPriKeyFilePath=self.__prop['keyFile'],
225                                    encrPriKeyPwd=self.__prop['keyPwd'])
226            except Exception, e:
227               
228                # Error occured decrypting - Trying parsing again, but this
229                # time assuming non-encrypted
230                try:
231                    reqKeys = AuthorisationReq(xmlTxt=reqXMLtxt)
232                   
233                except Exception, e:
234                    raise SessionMgrError(\
235                        "Error parsing authorisation request: %s" % e)
236
237
238        # Read proxy certificate
239        try:
240            usrProxyCert = X509Cert()
241           
242            if proxyCertFilePath is not None and \
243               isinstance(proxyCertFilePath, basestring):
244
245                # Proxy Certificate input as a file
246                usrProxyCert.read(proxyCertFilePath)
247               
248            elif reqKeys['proxyCert'] is not None:
249
250                # Proxy Certificate input as string text
251                usrProxyCert.parse(reqKeys['proxyCert'])
252
253            else:
254                raise AttAuthorityError(\
255                    "no input proxy certificate file path or file text")
256           
257        except Exception, e:
258            raise AttAuthorityError("User Proxy Certificate: %s" % e)
259
260
261        # Check proxy certificate hasn't expired
262        if not usrProxyCert.isValidTime():
263            raise AttAuthorityError("User Proxy Certificate has expired")
264
265           
266        # Get Distinguished name from certificate as an X500DN type
267        usrDN = usrProxyCert.dn
268       
269       
270        # Make a new Attribute Certificate instance passing in certificate
271        # details for later signing
272        #
273        # Nb. new attribute certificate file path is created from the
274        # Credentials Repository
275        certFilePathList = [self.__prop['certFile'],self.__prop['caCertFile']]
276        attCert = AttCert(self.__newAttCertFilePath(),
277                          signingKeyFilePath=self.__prop['keyFile'],
278                          certFilePathList=certFilePathList)
279
280
281        # Set holder's (user's) Distinguished Name
282        try:
283            attCert['holder'] = \
284                        usrDN.serialise(separator=self.__prop['dnSeparator'])
285           
286        except Exception, e:
287            raise AttAuthorityError("User DN: %s" % e)
288
289       
290        # Set Issuer details from Attribute Authority
291        issuerDN = self.__cert.dn
292        try:
293            attCert['issuer'] = \
294                    issuerDN.serialise(separator=self.__prop['dnSeparator'])
295           
296        except Exception, e:
297            raise AttAuthorityError("Issuer DN: %s" % e)
298       
299        attCert['issuerName'] = self.__prop['name']
300        attCert['issuerSerialNumber'] = self.__issuerSerialNumber
301
302
303        # Set validity time
304        try:
305            attCert.setValidityTime(\
306                        lifeTime=self.__prop['attCertLifeTime'],
307                        notBeforeOffset=self.__prop['attCertNotBeforeOff'])
308
309            # Check against the proxy certificate's expiry
310            dtUsrProxyNotAfter = usrProxyCert.notAfter
311           
312            if attCert.getValidityNotAfter(asDatetime=True) > \
313               dtUsrProxyNotAfter:
314
315                # Adjust the attribute certificate's expiry date time
316                # so that it agrees with that of the proxy certificate
317                attCert.setValidityTime(dtNotAfter=dtUsrProxyNotAfter)
318           
319        except Exception, e:
320            raise AttAuthorityError("Error setting validity time: %s" % e)
321       
322
323        # Check name is registered with this Attribute Authority - if no
324        # user roles are found, the user is not registered
325        usrRoles = self.getRoles(str(usrDN))
326        if usrRoles:
327           
328            # Set as an Original Certificate
329            #
330            # User roles found - user is registered with this data centre
331            # Add roles for this user for this data centre
332            attCert.addRoles(usrRoles)
333
334            # Mark new Attribute Certificate as an original
335            attCert['provenance'] = 'original'
336
337        else:
338           
339            # Set as a Mapped Certificate
340            #
341            # No roles found - user is not registered with this data centre
342            # Check for an externally provided certificate from another
343            # trusted data centre
344            extAttCert = AttCert(certFilePathList=self.__prop['caCertFile'])
345           
346            if userAttCertFilePath is None:
347                if not reqKeys['userAttCert']:
348                    raise AttAuthorityAccessDenied(\
349                    "User \"%s\" is not registered " % attCert['holder'] + \
350                    "and no external attribute certificate is available " + \
351                    "to make a mapping.")
352
353                else:
354                    # Parse externally provided certificate
355                    try:
356                        extAttCert.parse(reqKeys['userAttCert'])
357                       
358                    except Exception, e:
359                        raise AttAuthorityError(\
360                                "External Attribute Certificate: %s" + e)                 
361            else:
362                # Read externally provided certificate
363                try:
364                    extAttCert.read(userAttCertFilePath)
365                   
366                except Exception, e:
367                    raise AttAuthorityError(\
368                                "External Attribute Certificate: %s" + e)
369
370
371            # Check it's an original certificate - mapped certificates can't
372            # be used to make further mappings
373            if extAttCert.isMapped():
374                raise AttAuthorityError(\
375                    "External Attribute Certificate must have an " + \
376                    "original provenance in order to make further mappings.")
377
378
379            # Check it's valid and signed
380            try:
381                extAttCert.isValid(raiseExcep=True)
382               
383            except Exception, e:
384                raise AttAuthorityError(\
385                            "Invalid Remote Attribute Certificate: %s" + e)       
386
387
388            # Check that's it's holder matches the user certificate DN
389            try:
390                holderDN = X500DN(dn=extAttCert['holder'])
391               
392            except Exception, e:
393                raise AttAuthorityError(\
394                    "Error creating X500DN for holder: %s" + e)
395           
396            if holderDN != usrDN:
397                raise AttAuthorityError(\
398                    "User certificate and Attribute Certificate DNs " + \
399                    "don't match: " + str(usrDN) + " and " + str(holderDN))
400           
401 
402            # Get roles from external Attribute Certificate
403            trustedHostRoles = extAttCert.getRoles()
404
405
406            # Map external roles to local ones
407            localRoles = self.mapTrusted2LocalRoles(extAttCert['issuerName'],
408                                                    trustedHostRoles)
409            if not localRoles:
410                raise AttAuthorityAccessDenied(\
411                    "No local roles mapped to the %s roles: %s" % \
412                    (extAttCert['issuerName'], str(trustedHostRoles)))
413
414            attCert.addRoles(localRoles)
415           
416           
417            # Mark new Attribute Certificate as mapped
418            attCert['provenance'] = 'mapped'
419
420            # End if mapped certificate block
421           
422
423        try:
424            # Digitally sign certificate using Attribute Authority's
425            # certificate and private key
426            attCert.sign(signingKeyPwd=self.__prop['keyPwd'])
427           
428            # Check the certificate is valid
429            attCert.isValid(raiseExcep=True)
430           
431            # Write out certificate to keep a record of it for auditing
432            attCert.write()
433
434            # Return the cert to caller
435            return attCert
436       
437        except Exception, e:
438            raise AttAuthorityError("New Attribute Certificate \"%s\": %s" % \
439                                    (attCert.filePath, e))
440
441   
442
443
444    def readProperties(self, propFilePath=None):
445
446        """Read the configuration properties for the Attribute Authority
447
448        propFilePath: file path to properties file
449        """
450       
451        if propFilePath is not None:
452            if not isinstance(propFilePath, basestring):
453                raise AttAuthorityError("Input Properties file path " + \
454                                        "must be a valid string.")
455           
456            self.__propFilePath = propFilePath
457
458
459        try:
460            tree = ElementTree.parse(self.__propFilePath)
461           
462        except IOError, ioErr:
463            raise AttAuthorityError(\
464                                "Error parsing properties file \"%s\": %s" % \
465                                (ioErr.filename, ioErr.strerror))
466
467       
468        aaProp = tree.getroot()
469
470        # Copy properties from file as member variables
471        prop = dict([(elem.tag, elem.text) for elem in aaProp])
472
473
474        # Check for missing properties
475        propKeys = prop.keys()
476        missingKeys = [key for key in AttAuthority.__validKeys \
477                       if key not in propKeys]
478        if missingKeys != []:
479            raise AttAuthorityError("The following properties are " + \
480                                    "missing from the properties file: " + \
481                                    ', '.join(missingKeys))
482
483        # Strip white space - apart from fields where may be required
484        for key in prop:
485            if key != 'keyPwd' and prop[key]: 
486                prop[key] = prop[key].strip()
487               
488            # Check for environment variables in file paths
489            tagCaps = key.upper()
490            if 'FILE' in tagCaps or 'PATH' in tagCaps or 'DIR' in tagCaps:
491                prop[key] = os.path.expandvars(prop[key])
492 
493 
494        # Ensure Certificate time parameters are converted to numeric type
495        prop['attCertLifeTime'] = float(prop['attCertLifeTime'])
496        prop['attCertNotBeforeOff'] = float(prop['attCertNotBeforeOff'])
497         
498        self.__prop = prop
499
500       
501        # Check directory path
502        try:
503            dirList = os.listdir(self.__prop['attCertDir'])
504
505        except OSError, osError:
506            raise AttAuthorityError(\
507                "Invalid directory path Attribute Certificates store: " + \
508                osError.strerror)
509
510       
511       
512       
513    def readMapConfig(self, mapConfigFilePath=None):
514        """Parse Map Configuration file.
515
516        mapConfigFilePath:  file path for map configuration file.  If omitted,
517                            use member variable __mapConfigFilePath.
518        """
519       
520        if mapConfigFilePath is not None:
521            if not isinstance(mapConfigFilePath, basestring):
522                raise AttAuthorityError("Input Map Configuration file path "+\
523                                        "must be a valid string.")
524           
525            self.__prop['mapConfigFile'] = mapConfigFilePath
526
527       
528        tree = ElementTree.parse(self.__prop['mapConfigFile'])
529        rootElem = tree.getroot()
530
531        trustedElem = rootElem.findall('trusted')
532
533        # Dictionaries:
534        # 1) to hold all the data
535        self.__mapConfig = {}
536
537        # ... look-up
538        # 2) hosts corresponding to a given role and
539        # 3) roles of external data centre to this data centre
540        self.__localRole2TrustedHost = {}
541        self.__localRole2Trusted = {}
542        self.__trusted2LocalRole = {}
543       
544        for elem in trustedElem:
545
546            roleElem = elem.findall('role')
547            if not roleElem:
548                raise AttAuthorityError("\"role\" tag not found in \"%s\"" % \
549                                        self.__prop['mapConfigFile'])
550
551            try:
552                trustedHost = elem.attrib.values()[0]
553               
554            except Exception, e:
555                raise AttAuthorityError(\
556                                    "Error setting trusted host name: %s" % e)
557
558           
559            # Add signatureFile and list of roles
560            self.__mapConfig[trustedHost] = \
561            {
562                'wsdl': elem.findtext('wsdl'),
563                'role': [dict(i.items()) for i in roleElem]
564            }
565
566                   
567            self.__localRole2Trusted[trustedHost] = {}
568            self.__trusted2LocalRole[trustedHost] = {}
569           
570            for role in self.__mapConfig[trustedHost]['role']:
571
572                localRole = role['local']
573                remoteRole = role['remote']
574               
575                # Role to host look-up
576                if localRole in self.__localRole2TrustedHost:
577                   
578                    if trustedHost not in \
579                       self.__localRole2TrustedHost[localRole]:
580                        self.__localRole2TrustedHost[localRole].\
581                                                        append(trustedHost)                       
582                else:
583                    self.__localRole2TrustedHost[localRole] = [trustedHost]
584
585
586                # Trusted Host to local role and trusted host to trusted role
587                # map look-ups
588                try:
589                    self.__trusted2LocalRole[trustedHost][remoteRole].append(\
590                                                                localRole)                 
591                except KeyError:
592                    self.__trusted2LocalRole[trustedHost][remoteRole] = \
593                                                                [localRole]
594                   
595                try:
596                    self.__localRole2Trusted[trustedHost][localRole].append(\
597                                                                remoteRole)                 
598                except KeyError:
599                    self.__localRole2Trusted[trustedHost][localRole] = \
600                                                                [remoteRole]                 
601 
602
603           
604    def usrIsRegistered(self, usrDN):
605        """Check a particular user is registered with the Data Centre that the
606        Attribute Authority represents"""
607        return self.__usrRoles.usrIsRegistered(usrDN)
608       
609
610
611
612
613    def getRoles(self, dn):
614        """Get the roles available to the registered user identified usrDN.
615        """
616
617        # Call to AAUserRoles derived class.  Each Attribute Authority
618        # should define it's own roles class derived from AAUserRoles to
619        # define how roles are accessed
620        try:
621            return self.__usrRoles.getRoles(dn)
622
623        except Exception, e:
624            raise AttAuthorityError("Getting user roles: %s" % e)
625
626
627
628   
629    def getTrustedHostInfo(self, localRole=None):
630        """Return a dictionary of the hosts that have trust relationships
631        with this AA.  The dictionary is indexed by the trusted host name
632        and contains WSDL URIs and the roles that map to the
633        given input local role.
634
635        If no role is input, return all the AA's trusted hosts with all
636        their possible roles
637
638        Returns None if localRole isn't recognised"""
639
640        if not self.__localRole2Trusted:
641            raise AttAuthorityError("Roles to host look-up is not set - " + \
642                                    "ensure readMapConfig() has been called.")
643
644
645        if localRole is None:
646            # No role input - return all trusted hosts with their WSDL URIs
647            # and roles
648            trustedHostInfo = dict([(i[0], \
649                        {'wsdl': i[1]['wsdl'], \
650                         'role': [role['remote'] for role in i[1]['role']]}) \
651                         for i in self.__mapConfig.items()])
652                   
653            return trustedHostInfo
654
655
656        # Get trusted hosts for given input local role       
657        try:
658            trustedHosts = self.__localRole2TrustedHost[localRole]
659        except:
660            return None
661
662
663        # Get associated WSDL URI and roles for the trusted hosts identified
664        # and return as a dictionary indexed by host name
665        trustedHostInfo = dict([(host, \
666                        {'wsdl': self.__mapConfig[host]['wsdl'], \
667                         'role': self.__localRole2Trusted[host][localRole]}) \
668                         for host in trustedHosts])
669                         
670        return trustedHostInfo
671
672
673
674   
675    def mapTrusted2LocalRoles(self,trustedHost,trustedHostRoles):
676        """Map roles of trusted hosts to roles for this data centre
677
678        trustedHost:        name of external trusted data centre
679        trustedHostRoles:   list of external roles to map"""
680
681        if not self.__trusted2LocalRole:
682            raise AttAuthorityError("Roles map is not set - ensure " + \
683                                    "readMapConfig() has been called.")
684
685
686        # Check the host name is a trusted one recorded in the map
687        # configuration
688        if not self.__trusted2LocalRole.has_key(trustedHost):
689            return []
690
691        # Add local roles, skipping if no mapping is found
692        localRoles = []
693        for trustedRole in trustedHostRoles:
694            if trustedRole in self.__trusted2LocalRole[trustedHost]:
695                localRoles.extend(\
696                        self.__trusted2LocalRole[trustedHost][trustedRole])
697               
698        return localRoles
699
700
701
702
703    def __newAttCertFilePath(self):
704        """Create a new unique attribute certificate file path"""
705       
706        attCertFd, attCertFilePath = \
707                   tempfile.mkstemp(suffix=self.__prop['attCertFileSfx'],
708                                    prefix=self.__prop['attCertFilePfx'],
709                                    dir=self.__prop['attCertDir'],
710                                    text=True)
711
712        # The file is opened - close using the file descriptor returned in the
713        # first element of the tuple
714        os.close(attCertFd)
715
716        # The file path is the 2nd element
717        return attCertFilePath
718
719
720
721
722#_____________________________________________________________________________
723class AAUserRolesError(Exception):
724
725    """Exception handling for NDG Attribute Authority User Roles interface
726    class."""
727   
728    def __init__(self, msg):
729        self.__msg = msg
730         
731    def __str__(self):
732        return self.__msg
733
734
735
736#_____________________________________________________________________________
737class AAUserRoles:
738
739    """An abstract base class to define the user roles interface to an
740    Attribute Authority.
741
742    Each NDG data centre should implement a derived class which implements
743    the way user roles are provided to its representative Attribute Authority.
744   
745    Roles are expected to indexed by user Distinguished Name (DN).  They
746    could be stored in a database or file."""
747
748    # User defined class may wish to specify a URI for a database interface or
749    # path for a user roles configuration file
750    def __init__(self, dbURI=None, filePath=None):
751        """User Roles abstract base class - derive from this class to define
752        roles interface to Attribute Authority"""
753        raise NotImplementedError(\
754            self.__init__.__doc__.replace('\n       ',''))
755
756
757    def usrIsRegistered(self, dn):
758        """Derived method should return True if user is known otherwise
759        False"""
760        raise NotImplementedError(
761            self.UserIsRegistered.__doc__.replace('\n       ',''))
762
763
764    def getRoles(self, dn):
765        """Derived method should return the roles for the given user's
766        DN or else raise an exception"""
767        raise NotImplementedError(
768            self.getRoles.__doc__.replace('\n       ',''))
769
770
771#_____________________________________________________________________________
772# Test routines
773def testGetTrustedHostInfo(role=None,
774                           propFilePath='./attAuthorityProperties.xml'):
775    "Test getTrustedHosts AttAuthority method"
776    import pdb
777    pdb.set_trace()
778   
779    try:
780        aa = AttAuthority(propFilePath)
781        return aa.getTrustedHostInfo(role)
782   
783    except Exception, e:
784        print e
Note: See TracBrowser for help on using the repository browser.