source: security/trunk/python/NDG/AttAuthority.py @ 496

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

AttAuthority?.py + AttCert?.py: added NDG prefix to imports to allow for
use of NDG package in python site packages dir.

Session.py: UserSession?.createCookie() - experimented with setting domain
so that cookie is visible across .ac.uk sites.

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