source: TI12-security/trunk/ndg_saml/ndg/saml/saml2/binding/soap/client/__init__.py @ 7165

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/ndg_saml/ndg/saml/saml2/binding/soap/client/__init__.py@7165
Revision 7165, 11.6 KB checked in by pjkersha, 9 years ago (diff)

SAML SOAP binding code forked from ndg.security

  • Property svn:keywords set to Id
Line 
1"""SAML 2.0 bindings module implements SOAP binding for attribute query
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "02/09/09"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "http://www.apache.org/licenses/LICENSE-2.0"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id$'
11import logging
12log = logging.getLogger(__name__)
13
14from os import path
15from ConfigParser import ConfigParser, SafeConfigParser
16
17from ndg.saml.common import SAMLObject
18
19from ndg.saml.utils.factory import importModuleObject
20from ndg.soap import SOAPEnvelopeBase
21from ndg.soap.etree import SOAPEnvelope
22from ndg.soap.client import (UrlLib2SOAPClient, UrlLib2SOAPRequest)
23
24from ndg.saml.saml2.binding.soap import SOAPBindingInvalidResponse
25   
26   
27_isIterable = lambda obj: getattr(obj, '__iter__', False) 
28   
29
30class SOAPBinding(object):
31    '''Client SAML SOAP Binding'''
32    SOAP_ACTION = 'http://www.oasis-open.org/committees/security'
33   
34    REQUEST_ENVELOPE_CLASS_OPTNAME = 'requestEnvelopeClass'
35    RESPONSE_ENVELOPE_CLASS_OPTNAME = 'responseEnvelopeClass'
36    SERIALISE_OPTNAME = 'serialise'
37    DESERIALISE_OPTNAME = 'deserialise' 
38   
39    CONFIG_FILE_OPTNAMES = (
40        REQUEST_ENVELOPE_CLASS_OPTNAME,
41        RESPONSE_ENVELOPE_CLASS_OPTNAME,
42        SERIALISE_OPTNAME,
43        DESERIALISE_OPTNAME
44    )
45   
46    __PRIVATE_ATTR_PREFIX = "__"
47    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
48                       for i in CONFIG_FILE_OPTNAMES + ("client",)])
49    del i
50   
51    isIterable = staticmethod(_isIterable)
52   
53    def __init__(self, 
54                 requestEnvelopeClass=SOAPEnvelope,
55                 responseEnvelopeClass=SOAPEnvelope,
56                 serialise=None,
57                 deserialise=None,
58                 handlers=()):
59        '''Create SAML SOAP Client - Nb. serialisation functions must be set
60        before send()ing the request'''
61        self.__client = None
62        self.__serialise = None
63        self.__deserialise = None
64       
65        if serialise is not None:
66            self.serialise = serialise
67           
68        if deserialise is not None:
69            self.deserialise = deserialise
70       
71        self.client = UrlLib2SOAPClient()
72        self.client.httpHeader['SOAPAction'] = SOAPBinding.SOAP_ACTION
73       
74        # Configurable envelope classes
75        self.requestEnvelopeClass = requestEnvelopeClass
76        self.client.responseEnvelopeClass = responseEnvelopeClass
77
78        if not SOAPBinding.isIterable(handlers):
79            raise TypeError('Expecting iterable for "handlers" keyword; got %r'
80                            % type(handlers))
81           
82        for handler in handlers:
83            self.client.openerDirector.add_handler(handler())
84
85    def _getSerialise(self):
86        return self.__serialise
87
88    def _setSerialise(self, value):
89        if isinstance(value, basestring):
90            self.__serialise = importModuleObject(value)
91           
92        elif callable(value):
93            self.__serialise = value
94        else:
95            raise TypeError('Expecting callable for "serialise"; got %r' % 
96                            value)
97
98    serialise = property(_getSerialise, _setSerialise, 
99                         doc="callable to serialise request into XML type")
100
101    def _getDeserialise(self):
102        return self.__deserialise
103
104    def _setDeserialise(self, value):
105        if isinstance(value, basestring):
106            self.__deserialise = importModuleObject(value)
107           
108        elif callable(value):
109            self.__deserialise = value
110        else:
111            raise TypeError('Expecting callable for "deserialise"; got %r' % 
112                            value)
113       
114
115    deserialise = property(_getDeserialise, 
116                           _setDeserialise, 
117                           doc="callable to de-serialise response from XML "
118                               "type")
119
120    def _getRequestEnvelopeClass(self):
121        return self.__requestEnvelopeClass
122
123    def _setRequestEnvelopeClass(self, value):
124        if isinstance(value, basestring):
125            self.client.responseEnvelopeClass = importClass(value)
126           
127        elif issubclass(value, SOAPEnvelopeBase):
128            self.client.responseEnvelopeClass = value
129        else:
130            raise TypeError('Expecting %r derived type or string for '
131                            '"requestEnvelopeClass" attribute; got %r' % 
132                            (SOAPEnvelopeBase, value))
133       
134        self.__requestEnvelopeClass = value
135
136    requestEnvelopeClass = property(_getRequestEnvelopeClass, 
137                                    _setRequestEnvelopeClass, 
138                                    doc="SOAP Envelope Request Class")
139
140    def _getClient(self):
141        return self.__client
142
143    def _setClient(self, value):     
144        if not isinstance(value, UrlLib2SOAPClient):
145            raise TypeError('Expecting %r for "client"; got %r' % 
146                            (UrlLib2SOAPClient, type(value)))
147        self.__client = value
148
149    client = property(_getClient, _setClient, 
150                      doc="SOAP Client object")   
151
152    def send(self, samlObj, uri=None, request=None):
153        '''Make an request/query to a remote SAML service
154       
155        @type samlObj: saml.common.SAMLObject
156        @param samlObj: SAML query/request object
157        @type uri: basestring
158        @param uri: uri of service.  May be omitted if set from request.url
159        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
160        @param request: SOAP request object to which query will be attached
161        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
162        '''
163        if self.serialise is None:
164            raise AttributeError('No "serialise" method set to serialise the '
165                                 'request')
166
167        if self.deserialise is None:
168            raise AttributeError('No "deserialise" method set to deserialise '
169                                 'the response')
170           
171        if not isinstance(samlObj, SAMLObject):
172            raise TypeError('Expecting %r for input attribute query; got %r'
173                            % (SAMLObject, type(samlObj)))
174           
175        if request is None:
176            request = UrlLib2SOAPRequest()
177            request.envelope = self.requestEnvelopeClass()
178            request.envelope.create()
179           
180        if uri is not None:
181            request.url = uri
182       
183        samlElem = self.serialise(samlObj)
184           
185        # Attach query to SOAP body
186        request.envelope.body.elem.append(samlElem)
187           
188        response = self.client.send(request)
189       
190        if len(response.envelope.body.elem) != 1:
191            raise SOAPBindingInvalidResponse("Expecting single child element "
192                                             "is SOAP body")
193           
194        response = self.deserialise(response.envelope.body.elem[0])
195       
196        return response
197
198    @classmethod
199    def fromConfig(cls, cfg, **kw):
200        '''Alternative constructor makes object from config file settings
201        @type cfg: basestring / ConfigParser derived type
202        @param cfg: configuration file path or ConfigParser type object
203        @rtype: ndg.saml.saml2.binding.soap.client.SOAPBinding or derived type
204        @return: new instance of this class
205        '''
206        obj = cls()
207        obj.parseConfig(cfg, **kw)
208       
209        return obj
210
211    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
212        '''Read config file settings
213        @type cfg: basestring /ConfigParser derived type
214        @param cfg: configuration file path or ConfigParser type object
215        @type prefix: basestring
216        @param prefix: prefix for option names e.g. "attributeQuery."
217        @type section: baestring
218        @param section: configuration file section from which to extract
219        parameters.
220        ''' 
221        if isinstance(cfg, basestring):
222            cfgFilePath = path.expandvars(cfg)
223            _cfg = SafeConfigParser()
224            _cfg.optionxform = str
225
226            _cfg.read(cfgFilePath)
227           
228        elif isinstance(cfg, ConfigParser):
229            _cfg = cfg   
230        else:
231            raise AttributeError('Expecting basestring or ConfigParser type '
232                                 'for "cfg" attribute; got %r type' % type(cfg))
233       
234        # Get items for this section as a dictionary so that parseKeywords can
235        # used to update the object
236        kw = dict(_cfg.items(section))
237        if 'prefix' not in kw and prefix:
238            kw['prefix'] = prefix
239           
240        self.parseKeywords(**kw)
241       
242    def parseKeywords(self, prefix='', **kw):
243        """Update object from input keywords
244       
245        @type prefix: basestring
246        @param prefix: if a prefix is given, only update self from kw items
247        where keyword starts with this prefix
248        @type kw: dict
249        @param kw: items corresponding to class instance variables to
250        update.  Keyword names must match their equivalent class instance
251        variable names.  However, they may prefixed with <prefix>
252        """
253        prefixLen = len(prefix)
254        for optName, val in kw.items():
255            if prefix:
256                # Filter attributes based on prefix
257                if optName.startswith(prefix):
258                    setattr(self, optName[prefixLen:], val)
259            else:
260                # No prefix set - attempt to set all attributes   
261                setattr(self, optName, val)
262               
263    @classmethod
264    def fromKeywords(cls, prefix='', **kw):
265        """Create a new instance initialising instance variables from the
266        keyword inputs
267        @type prefix: basestring
268        @param prefix: if a prefix is given, only update self from kw items
269        where keyword starts with this prefix
270        @type kw: dict
271        @param kw: items corresponding to class instance variables to
272        update.  Keyword names must match their equivalent class instance
273        variable names.  However, they may prefixed with <prefix>
274        @return: new instance of this class
275        @rtype: ndg.saml.saml2.binding.soap.client.SOAPBinding or derived type
276        """
277        obj = cls()
278        obj.fromKeywords(prefix=prefix, **kw)
279       
280        return obj
281       
282    def __setattr__(self, name, value):
283        """Enable setting of SOAPBinding.client.responseEnvelopeClass as if it
284        were an attribute of self
285        """
286        try:
287            super(SOAPBinding, self).__setattr__(name, value)
288           
289        except AttributeError:
290            if 'name' == SOAPBinding.RESPONSE_ENVELOPE_CLASS_OPTNAME:
291                if isinstance(value, basestring):
292                    self.client.responseEnvelopeClass = importClass(value)
293                elif issubclass(value, SOAPEnvelopeBase):
294                    self.client.responseEnvelopeClass = value
295                else:
296                    raise TypeError('Expecting string or type instance for %r; '
297                                    'got %r instead.' % (name, type(value)))
298            else:
299                raise
300                   
301    def __getstate__(self):
302        '''Explicit implementation needed with __slots__'''
303        _dict = {}
304        for attrName in SOAPBinding.__slots__:
305            # Ugly hack to allow for derived classes setting private member
306            # variables
307            if attrName.startswith('__'):
308                attrName = "_SOAPBinding" + attrName
309               
310            _dict[attrName] = getattr(self, attrName)
311           
312        return _dict
313       
314    def __setstate__(self, attrDict):
315        '''Explicit implementation needed with __slots__'''
316        for attr, val in attrDict.items():
317            setattr(self, attr, val)
Note: See TracBrowser for help on using the repository browser.