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