source: TI12-security/trunk/python/NDG/XMLMsg.py @ 1176

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

Changes to incoporate new getHostInfo Attribute Authority WS method.

Tests/AttAuthorityIOtest.py: new unit test test method

Tests/SecurityClientTest?.py: minor changes to test settings

dist/NDG-Security-0.68.tar.gz: new distribution

www/html/attAuthority.wsdl: updated WSDL contains getHostInfo method.

conf/mapConfig.xml: contains new tags for information about the service provider of the AA e.g. loginURI,
service provider name. This is used by the new getHostInfo WS method.

conf/attAuthorityProperties.xml: remove old commented out tags.

NDG/AttAuthorityIO.py: added HostInfo?* classes for handling getHostInfo WS method I/O.

NDG/attAuthority_services_server.py and NDG/attAuthority_services.py: updated inline with WSDL changes.

NDG/AttAuthority.py:

  • readMapConfig updated to include new 'thisHost' tags.
  • self.mapConfig dictionary re-ordered to include top level keys 'thisHost' and 'trustedHosts'
  • New hostInfo property

NDG/AttCert.py: trivial fixes to commenting

NDG/XMLMsg.py: simplify error message for "Invalid keywords set for update..." error

NDG/CredWallet.py:

  • Client public key is now read in at the point where the corresponding pub key file path is set - i.e. in

setClntPubKeyFilePath method. This means the equivalent code in reqAuthorisation is not needed.

  • reqAuthorisation method has a new flag refreshAttCert. If set, the wallet is checked first for an existing

AC issued by the target AA. If found this is returned, and the call to the AA is skipped.

NDG/SecurityClient.py: added AttAuthorityClient?.getHostInfo WS wrapper method.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1"""NDG Web Services helper class for XML I/O between WS client and server
2
3NERC Data Grid Project
4
5P J Kershaw 14/12/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
13# For new line symbol
14import os
15
16# Use _getframe to allow setting of attributes by derived class methods
17import sys
18
19# For XML parsing
20import cElementTree as ElementTree
21
22from XMLSecDoc import *
23
24
25#_____________________________________________________________________________
26class XMLMsgError(Exception):   
27    """Exception handling for NDG WS I/O Message class."""
28   
29    def __init__(self, msg):
30        self.__msg = msg
31         
32    def __str__(self):
33        return self.__msg
34
35
36#_____________________________________________________________________________
37class XMLMsg(dict):
38    """Super class for encrypting arguments in SOAP messages for NDG Security
39    Web services"""
40   
41    # Derived classes should specify XML tags and values as keywords and
42    # values in a dictionary
43    xmlTagTmpl = {}
44
45    # Set Mandatory tags - if any of these are not present raise an exception
46    xmlMandatoryTags = []
47   
48    def __init__(self, 
49                 encrXMLtxt=None,
50                 xmlTxt=None,
51                 encrPubKeyFilePath=None,
52                 encrPriKeyFilePath=None,
53                 encrPriKeyPwd=None,
54                 signingKeyFilePath=None,
55                 signingKeyPwd=None,
56                 signingCertFilePath=None,
57                 signatureChkCertFilePath=None,
58                 xmlVers="1.0",
59                 xmlEncoding="UTF-8",
60                 **xmlTags):
61        """XML for sending/receiving encrypted XML arguments with NDG web
62        services
63       
64        To encrypt message to send:
65            encrXML = XMLMsg(encrPubKeyFilePath=filePath,
66                             [xmlTxt]|[Key1=key1, Key2=key2, ...])
67                             
68        For recipient to decrypt:
69            encrXML = XMLMsg(encrPriKeyFilePath=filePath,
70                             encrPriKeyPwd=pwd,
71                             encrXMLtxt=xmlTxt)
72                             
73        encrXMLtxt:            string containing encrypted text for
74                               decryption
75        encrPubKeyFilePath:    public key to encrypt message - use when
76                               sending a message only
77        encrPriKeyFilePath:    private key used to decrypt message - use
78                               when receiving a message only
79        encrPriKeyPwd:         password for private key
80       
81        xmlVers:               version number in standard XML header
82        xmlEncoding:           encoding type set in XML header
83        **xmlTags:             keywords corresponding to the XML tags to be
84                               encrypted
85        """
86       
87        self.__xmlTags = {}
88        self.__encrXMLtxt = encrXMLtxt
89        self.__xmlTxt = xmlTxt
90       
91        self.__xmlHdr = "<?xml version=\"%s\" encoding=\"%s\"?>" % \
92                        (xmlVers, xmlEncoding)
93       
94        # Allow user credentials to be access like dictionary keys
95        dict.__init__(self)
96       
97       
98        # Initialisation for XML Security class used for encryption
99        try:
100            if signingCertFilePath:
101                # Add cert to signature
102                certFilePathList = signingCertFilePath
103               
104            elif signatureChkCertFilePath:
105                #  Check an existing doc using the input cert file
106                certFilePathList = signatureChkCertFilePath
107            else:
108                certFilePathList = None
109                   
110            self.__xmlSecDoc=XMLSecDoc(encrPriKeyFilePath=encrPriKeyFilePath,
111                                       encrPubKeyFilePath=encrPubKeyFilePath,
112                                       signingKeyFilePath=signingKeyFilePath,
113                                       certFilePathList=certFilePathList)
114        except Exception, e:
115            raise XMLMsgError("Error initialising XML security: %s" % e)     
116 
117                 
118        # Check mode:
119        # - encrypted XML entered for decryption
120        # or
121        # - XML or XML tags entered as keywords to make XML document for
122        # encryption
123        if encrXMLtxt:           
124            # Encrypted text has been input for decryption
125            if not encrPriKeyFilePath:
126                raise XMLMsgError(\
127                    "A private key must be set in order to decrypt the data")
128           
129            try:
130                self.decrypt(encrPriKeyPwd, encrXMLtxt)
131               
132            except Exception, e:
133                raise XMLMsgError("Error decrypting input text: %s" % e)     
134
135            # Parse elements from decrypted result saving as dictionary
136            # self.__xmlTags
137            self.parseXML()
138        else:
139            # XML text or XML tags input
140            if xmlTxt:
141                # Input XML text set - parse tags into dictionary
142                # self.__xmlTags
143                if encrPriKeyFilePath:
144                    # decrypt if a private key was set
145                    try:
146                        self.decrypt(encrPriKeyPwd, xmlTxt)
147                    except:
148                        # Try parse without encryption
149                        self.__xmlTxt = xmlTxt
150                else:
151                    self.__xmlTxt = xmlTxt
152                           
153                try:
154                    self.parseXML()
155                except Exception, e:
156                    raise XMLMsgError("Error parsing text: " + str(e))               
157            else:
158                # XML text will be set from tags set as keywords
159                self.updateXML(**xmlTags)
160               
161            if encrPubKeyFilePath:
162                # Public key set - encrypt the data
163                try:
164                    self.encrypt()
165                   
166                except Exception, e:
167                    raise XMLMsgError("Error encrypting credentials: %s" % e)
168
169        self.chkTags()
170       
171               
172    def __repr__(self):
173        """Print the XML for user credentials"""
174        return repr(self.__xmlTags)
175           
176           
177    def __str__(self):
178        """Print the XML for user credentials"""
179        return self.__xmlTxt
180           
181
182    def __call__(self):
183        """Return text for output - encrypted version if set otherwise plain
184        text"""
185        return self.__encrXMLtxt or self.__xmlTxt
186       
187
188    # There doesn't seem to be an implicit function to allow a dict subclass
189    # to yield a dictionary for dict(obj) - Make a 'xmlTags' property instead
190    def __getXMLtags(self):
191        """Return dictonary of XML tags"""
192        return self.__xmlTags
193   
194    xmlTags = property(fget=__getXMLtags, doc="XML tags as a dictionary")
195 
196                     
197    def __getXMLhdr(self):
198        return self.__xmlHdr
199   
200    xmlHdr = property(fget=__getXMLhdr,
201                      doc="Standard header line for XML document")
202                     
203    def __getXMLtxt(self):
204        """Get the user credentials as XML formatted string"""
205        return self.__xmlTxt
206
207    def __setXMLtxt(self, xmlTxt):
208        """Allow text to be set if class of calling method is XMLMsg
209        derived"""
210       
211        try:
212            # Get previous frame from stack
213            frame = sys._getframe().f_back
214        except Exception, e:
215            raise AttributeError("Error checking calling method")
216       
217        try:
218            # Check the class that the calling method belongs to
219            callerClassName = frame.f_locals['self'].__class__.__name__
220   
221            if callerClassName == self.__class__.__name__:
222                self.__xmlTxt = xmlTxt
223                return
224            else:
225                # Raise KeyError so that execution moves to except block
226                raise KeyError
227           
228        except KeyError:
229            # self variable may not be present
230            AttributeError("Caller must be a method of a %s derived class" % \
231                           self.__class__.__name__)
232
233       
234    # Read-only property
235    xmlTxt = property(fget=__getXMLtxt,
236                      fset=__setXMLtxt,
237                      doc="XML string text for message")                     
238
239   
240    def __getEncrXMLtxt(self):
241        """Get the user credentials as encrypted XML formatted string"""
242        return self.__encrXMLtxt
243   
244    # Read-only property
245    encrXMLtxt = property(fget=__getEncrXMLtxt,
246                          doc="encrypted XML user credentials")                     
247
248                         
249    def __delitem__(self, key):
250        "keys cannot be removed"       
251        raise KeyError('Keys cannot be deleted from ' + \
252                       self.__class__.__name__)
253
254
255    def __getitem__(self, key):
256        """Access user credentials as dictionary keys"""
257       
258        # Check input key
259        if self.__xmlTags.has_key(key):
260            # key recognised
261            return self.__xmlTags[key]               
262        else:
263            # key not recognised
264            raise KeyError('Key "%s" not recognised for %s' % \
265                           (key, self.__class__.__name__))
266
267
268    def __setitem__(self, key, item):
269        """Allows keys to be set methods of *derived* classes only
270       
271        - simulates behaviour like 'protected' data members in C++"""
272       
273        if not key in self.__xmlTags:
274            raise KeyError("Key \"%s\" invalid for %s" % \
275                            (key, self.__class__.__name__))
276                           
277        try:
278            # Get previous frame from stack
279            frame = sys._getframe().f_back
280        except Exception, e:
281            raise keyError("Error checking caller")
282       
283        try:
284            # Check the class that the calling method belongs to
285            callerClassName = frame.f_locals['self'].__class__.__name__
286   
287            if callerClassName == self.__class__.__name__:
288                self.__xmlTags[key] = item
289                return
290           
291        except KeyError:
292            # self variable may not be present
293            pass
294             
295        raise KeyError('Keys may only be set by methods of derived classes')
296
297
298    # 'in' operator
299    def __contains__(self, key):
300        return key in self.__xmlTags
301 
302    def has_key(self, key):
303        return self.__xmlTags.has_key(key)     
304
305    def clear(self):
306        raise KeyError("Data cannot be cleared from " + \
307                       self.__class__.__name__)
308   
309    def copy(self):
310        import copy
311        return copy.copy(self)
312   
313    def keys(self):
314        return self.__xmlTags.keys()
315
316    def items(self):
317        return self.__xmlTags.items()
318
319    def values(self):
320        return self.__xmlTags.values()
321
322
323    def update(self, xmlTagsDict=None, **xmlTags):
324        """override dict.update to carry out validation
325       
326        Nb. unlike dict.update, an iterable can't be passed in"""
327       
328        if xmlTagsDict:
329            if not isinstance(xmlTagsDict, dict):
330                raise TypeError("Input must be dict type")
331           
332            # Dictionary input overrides keywords
333            xmlTags = xmlTagsDict
334           
335        if not len(xmlTags):
336            return         
337             
338        # Check keys are valid - xmlTagTmpl MUST have been altered from the
339        # default otherwise this test will always fail
340        unknownKeys = [key for key in xmlTags if key not in self.xmlTagTmpl]
341        if unknownKeys:
342            raise KeyError, "Invalid keywords set for update: \"%s\"" % \
343                            '", "'.join(unknownKeys)
344               
345        # Copy keywords - but only those that are NOT None
346        self.__xmlTags.update(\
347                dict([(k, v) for k, v in xmlTags.items() if v is not None]))
348
349
350    def updateXML(self, **xmlTags):
351        """Build XML message string from keywords corresponding to
352        tags input
353       
354        Override in a derived class if required"""
355       
356        # Update dictionary
357        self.update(**xmlTags)
358       
359        # Create XML formatted string ready for encryption
360        try:
361            rootNode = ElementTree.Element(self.__class__.__name__)
362            rootNode.tail = os.linesep
363           
364            for tag in self.__xmlTags:
365                # ElementTree tostring doesn't like bool types
366                elem = ElementTree.SubElement(rootNode, tag)
367                elem.tail = os.linesep
368               
369                if self.__xmlTags[tag] is None:
370                    continue
371               
372                elif isinstance(self.__xmlTags[tag], bool):                   
373                    elem.text = "%d" % self.__xmlTags[tag]
374                else:
375                    elem.text = self.__xmlTags[tag]
376                   
377                     
378            self.__xmlTxt = self.__xmlHdr + os.linesep + \
379                                                ElementTree.tostring(rootNode)
380        except Exception, e:
381            raise XMLMsgError("Creating XML: %s" % e)
382
383
384    def parseXML(self, rtnRootElem=False):
385        """Parse unencrypted XML in self.__xmlTxt
386       
387        Assumes single level of nesting of XML tags - override in a derived
388        class if required
389       
390        rtnRootElem:    set to True to return the root element - useful for
391                        derived classes to be able to access"""
392       
393        # Convert strings containing digits to integer type
394        intCast = lambda s: int(str(s).isdigit()) and int(s) or s
395       
396        try:
397            rootElem = ElementTree.XML(self.__xmlTxt)
398           
399            xmlTags = {}
400            for elem in rootElem:
401                text = intCast(elem.text)
402                if text is None:
403                    xmlTags[elem.tag] = os.linesep.join(\
404                        [ElementTree.tostring(subElem) for subElem in elem])
405                else:
406                    xmlTags[elem.tag] = elem.text
407                           
408            self.__xmlTags = xmlTags
409           
410        except Exception, e:
411            raise XMLMsgError("Error parsing XML text: %s" % e)     
412
413        return rootElem
414
415
416    #_________________________________________________________________________
417    def chkTags(self):
418        """Check for any mandatory tags that have not been set"""
419        missingTags=[tag for tag in self.xmlMandatoryTags if not tag in self]
420        if missingTags:
421            raise XMLMsgError(\
422                'The following tag(s) must be set: "%s"' % \
423                                                    '", "'.join(missingTags))
424
425
426    #_________________________________________________________________________
427    def encrypt(self, encrPubKeyFilePath=None):
428        """Encrypt message"""
429        self.__xmlSecDoc.encrypt(xmlTxt=self.__xmlTxt,
430                                 encrPubKeyFilePath=encrPubKeyFilePath)
431        self.__encrXMLtxt = str(self.__xmlSecDoc)
432
433
434    #_________________________________________________________________________
435    def decrypt(self, encrPriKeyPwd, xmlTxt=None):
436        """Decrypt encrypted text"""
437       
438        if xmlTxt is None:
439            xmlTxt = self.__encrXMLtxt
440           
441        self.__xmlSecDoc.decrypt(xmlTxt=xmlTxt, encrPriKeyPwd=encrPriKeyPwd)
442        self.__xmlTxt = str(self.__xmlSecDoc)
443
444   
445    #_________________________________________________________________________
446    def sign(self,
447             signingKeyFilePath=None,
448             signingKeyPwd=None,
449             signingCertFilePath=None):
450        """Digitally sign message"""
451       
452        self.__xmlSecDoc.sign(xmlTxt=self.__xmlTxt,
453                              signingKeyFilePath=signingKeyFilePath,
454                              signingKeyPwd=signingKeyPwd,
455                              certFilePathList=signingCertFilePath)
456        self.__xmlTxt = str(self.__xmlSecDoc)
457
458   
459    #_________________________________________________________________________
460    def isValidSig(self, *certFilePathList):
461        """Check digital signature of message"""
462       
463        if certFilePathList == ():
464            certFilePathList = None
465           
466        return self.__xmlSecDoc.isValidSig(xmlTxt=self.__xmlTxt,
467                                           certFilePathList=certFilePathList)
Note: See TracBrowser for help on using the repository browser.