Merge lp:~dreamhosters/txaws/920309-fix-ca-certs into lp:txaws

Proposed by Duncan McGreggor
Status: Merged
Approved by: Jamu Kakar
Approved revision: 130
Merge reported by: Duncan McGreggor
Merged at revision: not available
Proposed branch: lp:~dreamhosters/txaws/920309-fix-ca-certs
Merge into: lp:txaws
Diff against target: 541 lines (+263/-146)
7 files modified
Makefile (+1/-1)
txaws/client/ssl.py (+44/-16)
txaws/client/tests/test_base.py (+8/-125)
txaws/client/tests/test_ssl.py (+199/-0)
txaws/exception.py (+6/-0)
txaws/s3/client.py (+1/-2)
txaws/s3/tests/test_acls.py (+4/-2)
To merge this branch: bzr merge lp:~dreamhosters/txaws/920309-fix-ca-certs
Reviewer Review Type Date Requested Status
Arsene Rei Approve
Jamu Kakar Approve
Review via email: mp+90325@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Arsene Rei (arsene-rei) wrote :

+ Retrieve a list of CAs pointed by C{files}.

files is no longer an argument to get_ca_certs.

They had inline imports...?

Revision history for this message
Jamu Kakar (jkakar) wrote :

[1]

+ CERTS_PATH environment variable that should point to a directory containing

Is CERTS_PATH a commonly used environment variable...? If it isn't,
it might be better to use TXAWS_CERTS_PATH.

Nice work, +1!

review: Approve
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

[1] Yeah, the files parameter wasn't used by anything (not even unit tests). Not only that, nothing in the code calls get_ca_certs except for get_global_ca_certs... and that is only ever called inside VerifyingContextFactory. So there's basically no way to pass that files parameter in from anywhere.

[2] Function/method-level imports? Yeah, not sure why those were done like that. There doesn't seem to have been any reason for it, so I removed them.

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

Ah, in a private IRC chat, rei clarified:

"But, regarding C{files} I'm just saying it should be removed from the docstring."

Good catch. I'm on it.

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

Done, and ready for your review.

131. By Duncan McGreggor

Fixed docstring from review feedback (by Stephon Striplin).

Revision history for this message
Arsene Rei (arsene-rei) wrote :

Yup. Looks solid. :)

review: Approve
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

> [1]
>
> + CERTS_PATH environment variable that should point to a directory containing
>
> Is CERTS_PATH a commonly used environment variable...? If it isn't,
> it might be better to use TXAWS_CERTS_PATH.

Good call -- done and thanks!

132. By Duncan McGreggor

Per Jamu's review comment, made the shell env var something distinctly
txAWS-ey.

133. By Duncan McGreggor

Fixed pep8 and pyflakes from some recent commits.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2012-01-25 18:03:05 +0000
3+++ Makefile 2012-01-26 23:08:23 +0000
4@@ -48,7 +48,7 @@
5
6 virtual-pep8: VERSION ?= 2.7
7 virtual-pep8:
8- -. .venv-$(VERSION)/bin/activate && pep8 ./txaws
9+ -. .venv-$(VERSION)/bin/activate && pep8 --repeat ./txaws
10
11
12 virtual-pyflakes: VERSION ?= 2.7
13
14=== modified file 'txaws/client/ssl.py'
15--- txaws/client/ssl.py 2011-11-29 18:42:09 +0000
16+++ txaws/client/ssl.py 2012-01-26 23:08:23 +0000
17@@ -1,16 +1,28 @@
18 from glob import glob
19 import os
20 import re
21+import sys
22
23 from OpenSSL import SSL
24 from OpenSSL.crypto import load_certificate, FILETYPE_PEM
25
26 from twisted.internet.ssl import CertificateOptions
27
28+from txaws import exception
29+
30
31 __all__ = ["VerifyingContextFactory", "get_ca_certs"]
32
33
34+# Multiple defaults are supported; just add more paths, separated by colons.
35+if sys.platform == "darwin":
36+ DEFAULT_CERTS_PATH = "/System/Library/OpenSSL/certs/:"
37+# XXX Windows users can file a bug to add theirs, since we don't know what
38+# the right path is
39+else:
40+ DEFAULT_CERTS_PATH = "/etc/ssl/certs/:"
41+
42+
43 class VerifyingContextFactory(CertificateOptions):
44 """
45 A SSL context factory to pass to C{connectSSL} to check for hostname
46@@ -71,22 +83,38 @@
47 return context
48
49
50-def get_ca_certs(files="/etc/ssl/certs/*.pem"):
51- """Retrieve a list of CAs pointed by C{files}."""
52- certificateAuthorityMap = {}
53- for certFileName in glob(files):
54- # There might be some dead symlinks in there, so let's make sure it's
55- # real.
56- if not os.path.exists(certFileName):
57- continue
58- certFile = open(certFileName)
59- data = certFile.read()
60- certFile.close()
61- x509 = load_certificate(FILETYPE_PEM, data)
62- digest = x509.digest("sha1")
63- # Now, de-duplicate in case the same cert has multiple names.
64- certificateAuthorityMap[digest] = x509
65- return certificateAuthorityMap.values()
66+def get_ca_certs():
67+ """
68+ Retrieve a list of CAs at either the DEFAULT_CERTS_PATH or the env
69+ override, TXAWS_CERTS_PATH.
70+
71+ In order to find .pem files, this function checks first for presence of the
72+ TXAWS_CERTS_PATH environment variable that should point to a directory
73+ containing cert files. In the absense of this variable, the module-level
74+ DEFAULT_CERTS_PATH will be used instead.
75+
76+ Note that both of these variables have have multiple paths in them, just
77+ like the familiar PATH environment variable (separated by colons).
78+ """
79+ cert_paths = os.getenv("TXAWS_CERTS_PATH", DEFAULT_CERTS_PATH).split(":")
80+ certificate_authority_map = {}
81+ for path in cert_paths:
82+ for cert_file_name in glob(os.path.join(path, "*.pem")):
83+ # There might be some dead symlinks in there, so let's make sure
84+ # it's real.
85+ if not os.path.exists(cert_file_name):
86+ continue
87+ cert_file = open(cert_file_name)
88+ data = cert_file.read()
89+ cert_file.close()
90+ x509 = load_certificate(FILETYPE_PEM, data)
91+ digest = x509.digest("sha1")
92+ # Now, de-duplicate in case the same cert has multiple names.
93+ certificate_authority_map[digest] = x509
94+ values = certificate_authority_map.values()
95+ if len(values) == 0:
96+ raise exception.CertsNotFoundError("Could not find any .pem files.")
97+ return values
98
99
100 _ca_certs = None
101
102=== renamed file 'txaws/client/tests/test_client.py' => 'txaws/client/tests/test_base.py'
103--- txaws/client/tests/test_client.py 2011-11-29 18:47:00 +0000
104+++ txaws/client/tests/test_base.py 2012-01-26 23:08:23 +0000
105@@ -1,38 +1,22 @@
106 import os
107
108-from OpenSSL.crypto import load_certificate, FILETYPE_PEM
109-from OpenSSL.SSL import Error as SSLError
110-from OpenSSL.version import __version__ as pyopenssl_version
111-
112 from twisted.internet import reactor
113-from twisted.internet.ssl import DefaultOpenSSLContextFactory
114 from twisted.internet.error import ConnectionRefusedError
115 from twisted.protocols.policies import WrappingFactory
116 from twisted.python import log
117 from twisted.python.filepath import FilePath
118 from twisted.python.failure import Failure
119+from twisted.test.test_sslverify import makeCertificate
120 from twisted.web import server, static
121 from twisted.web.client import HTTPClientFactory
122 from twisted.web.error import Error as TwistedWebError
123
124+from txaws.client import ssl
125 from txaws.client.base import BaseClient, BaseQuery, error_wrapper
126-from txaws.client.ssl import VerifyingContextFactory
127 from txaws.service import AWSServiceEndpoint
128 from txaws.testing.base import TXAWSTestCase
129
130
131-def sibpath(path):
132- return os.path.join(os.path.dirname(__file__), path)
133-
134-
135-PRIVKEY = sibpath("private.ssl")
136-PUBKEY = sibpath("public.ssl")
137-BADPRIVKEY = sibpath("badprivate.ssl")
138-BADPUBKEY = sibpath("badpublic.ssl")
139-PRIVSANKEY = sibpath("private_san.ssl")
140-PUBSANKEY = sibpath("public_san.ssl")
141-
142-
143 class ErrorWrapperTestCase(TXAWSTestCase):
144
145 def test_204_no_content(self):
146@@ -168,6 +152,9 @@
147 d.addCallback(query.get_response_headers)
148 return d.addCallback(check_results)
149
150+ # XXX for systems that don't have certs in the DEFAULT_CERT_PATH, this test
151+ # will fail; instead, let's create some certs in a temp directory and set
152+ # the DEFAULT_CERT_PATH to point there.
153 def test_ssl_hostname_verification(self):
154 """
155 If the endpoint passed to L{BaseQuery} has C{ssl_hostname_verification}
156@@ -183,6 +170,8 @@
157 def connectSSL(self, host, port, client, factory):
158 self.connects.append((host, port, client, factory))
159
160+ certs = makeCertificate(O="Test Certificate", CN="something")[1]
161+ self.patch(ssl, "_ca_certs", certs)
162 fake_reactor = FakeReactor()
163 endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
164 query = BaseQuery("an action", "creds", endpoint, fake_reactor)
165@@ -190,112 +179,6 @@
166 [(host, port, client, factory)] = fake_reactor.connects
167 self.assertEqual("example.com", host)
168 self.assertEqual(443, port)
169- self.assertTrue(isinstance(factory, VerifyingContextFactory))
170+ self.assertTrue(isinstance(factory, ssl.VerifyingContextFactory))
171 self.assertEqual("example.com", factory.host)
172 self.assertNotEqual([], factory.caCerts)
173-
174-
175-class BaseQuerySSLTestCase(TXAWSTestCase):
176-
177- def setUp(self):
178- self.cleanupServerConnections = 0
179- name = self.mktemp()
180- os.mkdir(name)
181- FilePath(name).child("file").setContent("0123456789")
182- r = static.File(name)
183- self.site = server.Site(r, timeout=None)
184- self.wrapper = WrappingFactory(self.site)
185- from txaws.client import ssl
186- pub_key = file(PUBKEY)
187- pub_key_data = pub_key.read()
188- pub_key.close()
189- pub_key_san = file(PUBSANKEY)
190- pub_key_san_data = pub_key_san.read()
191- pub_key_san.close()
192- ssl._ca_certs = [load_certificate(FILETYPE_PEM, pub_key_data),
193- load_certificate(FILETYPE_PEM, pub_key_san_data)]
194-
195- def tearDown(self):
196- from txaws.client import ssl
197- ssl._ca_certs = None
198- # If the test indicated it might leave some server-side connections
199- # around, clean them up.
200- connections = self.wrapper.protocols.keys()
201- # If there are fewer server-side connections than requested,
202- # that's okay. Some might have noticed that the client closed
203- # the connection and cleaned up after themselves.
204- for n in range(min(len(connections), self.cleanupServerConnections)):
205- proto = connections.pop()
206- log.msg("Closing %r" % (proto,))
207- proto.transport.loseConnection()
208- if connections:
209- log.msg("Some left-over connections; this test is probably buggy.")
210- return self.port.stopListening()
211-
212- def _get_url(self, path):
213- return "https://localhost:%d/%s" % (self.portno, path)
214-
215- def test_ssl_verification_positive(self):
216- """
217- The L{VerifyingContextFactory} properly allows to connect to the
218- endpoint if the certificates match.
219- """
220- context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY)
221- self.port = reactor.listenSSL(
222- 0, self.site, context_factory, interface="127.0.0.1")
223- self.portno = self.port.getHost().port
224-
225- endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
226- query = BaseQuery("an action", "creds", endpoint)
227- d = query.get_page(self._get_url("file"))
228- return d.addCallback(self.assertEquals, "0123456789")
229-
230- def test_ssl_verification_negative(self):
231- """
232- The L{VerifyingContextFactory} fails with a SSL error the certificates
233- can't be checked.
234- """
235- context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY)
236- self.port = reactor.listenSSL(
237- 0, self.site, context_factory, interface="127.0.0.1")
238- self.portno = self.port.getHost().port
239-
240- endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
241- query = BaseQuery("an action", "creds", endpoint)
242- d = query.get_page(self._get_url("file"))
243- return self.assertFailure(d, SSLError)
244-
245- def test_ssl_verification_bypassed(self):
246- """
247- L{BaseQuery} doesn't use L{VerifyingContextFactory}
248- if C{ssl_hostname_verification} is C{False}, thus allowing to connect
249- to non-secure endpoints.
250- """
251- context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY)
252- self.port = reactor.listenSSL(
253- 0, self.site, context_factory, interface="127.0.0.1")
254- self.portno = self.port.getHost().port
255-
256- endpoint = AWSServiceEndpoint(ssl_hostname_verification=False)
257- query = BaseQuery("an action", "creds", endpoint)
258- d = query.get_page(self._get_url("file"))
259- return d.addCallback(self.assertEquals, "0123456789")
260-
261- def test_ssl_subject_alt_name(self):
262- """
263- L{VerifyingContextFactory} supports checking C{subjectAltName} in the
264- certificate if it's available.
265- """
266- context_factory = DefaultOpenSSLContextFactory(PRIVSANKEY, PUBSANKEY)
267- self.port = reactor.listenSSL(
268- 0, self.site, context_factory, interface="127.0.0.1")
269- self.portno = self.port.getHost().port
270-
271- endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
272- query = BaseQuery("an action", "creds", endpoint)
273- d = query.get_page("https://127.0.0.1:%d/file" % (self.portno,))
274- return d.addCallback(self.assertEquals, "0123456789")
275-
276- if pyopenssl_version < "0.12":
277- test_ssl_subject_alt_name.skip = (
278- "subjectAltName not supported by older PyOpenSSL")
279
280=== added file 'txaws/client/tests/test_ssl.py'
281--- txaws/client/tests/test_ssl.py 1970-01-01 00:00:00 +0000
282+++ txaws/client/tests/test_ssl.py 2012-01-26 23:08:23 +0000
283@@ -0,0 +1,199 @@
284+import os
285+import tempfile
286+
287+from OpenSSL.crypto import dump_certificate, load_certificate, FILETYPE_PEM
288+from OpenSSL.SSL import Error as SSLError
289+from OpenSSL.version import __version__ as pyopenssl_version
290+
291+from twisted.internet import reactor
292+from twisted.internet.ssl import DefaultOpenSSLContextFactory
293+from twisted.protocols.policies import WrappingFactory
294+from twisted.python import log
295+from twisted.python.filepath import FilePath
296+from twisted.test.test_sslverify import makeCertificate
297+from twisted.web import server, static
298+
299+from txaws import exception
300+from txaws.client import ssl
301+from txaws.client.base import BaseQuery
302+from txaws.service import AWSServiceEndpoint
303+from txaws.testing.base import TXAWSTestCase
304+
305+
306+def sibpath(path):
307+ return os.path.join(os.path.dirname(__file__), path)
308+
309+
310+PRIVKEY = sibpath("private.ssl")
311+PUBKEY = sibpath("public.ssl")
312+BADPRIVKEY = sibpath("badprivate.ssl")
313+BADPUBKEY = sibpath("badpublic.ssl")
314+PRIVSANKEY = sibpath("private_san.ssl")
315+PUBSANKEY = sibpath("public_san.ssl")
316+
317+
318+class BaseQuerySSLTestCase(TXAWSTestCase):
319+
320+ def setUp(self):
321+ self.cleanupServerConnections = 0
322+ name = self.mktemp()
323+ os.mkdir(name)
324+ FilePath(name).child("file").setContent("0123456789")
325+ r = static.File(name)
326+ self.site = server.Site(r, timeout=None)
327+ self.wrapper = WrappingFactory(self.site)
328+ pub_key = file(PUBKEY)
329+ pub_key_data = pub_key.read()
330+ pub_key.close()
331+ pub_key_san = file(PUBSANKEY)
332+ pub_key_san_data = pub_key_san.read()
333+ pub_key_san.close()
334+ ssl._ca_certs = [load_certificate(FILETYPE_PEM, pub_key_data),
335+ load_certificate(FILETYPE_PEM, pub_key_san_data)]
336+
337+ def tearDown(self):
338+ ssl._ca_certs = None
339+ # If the test indicated it might leave some server-side connections
340+ # around, clean them up.
341+ connections = self.wrapper.protocols.keys()
342+ # If there are fewer server-side connections than requested,
343+ # that's okay. Some might have noticed that the client closed
344+ # the connection and cleaned up after themselves.
345+ for n in range(min(len(connections), self.cleanupServerConnections)):
346+ proto = connections.pop()
347+ log.msg("Closing %r" % (proto,))
348+ proto.transport.loseConnection()
349+ if connections:
350+ log.msg("Some left-over connections; this test is probably buggy.")
351+ return self.port.stopListening()
352+
353+ def _get_url(self, path):
354+ return "https://localhost:%d/%s" % (self.portno, path)
355+
356+ def test_ssl_verification_positive(self):
357+ """
358+ The L{VerifyingContextFactory} properly allows to connect to the
359+ endpoint if the certificates match.
360+ """
361+ context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY)
362+ self.port = reactor.listenSSL(
363+ 0, self.site, context_factory, interface="127.0.0.1")
364+ self.portno = self.port.getHost().port
365+
366+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
367+ query = BaseQuery("an action", "creds", endpoint)
368+ d = query.get_page(self._get_url("file"))
369+ return d.addCallback(self.assertEquals, "0123456789")
370+
371+ def test_ssl_verification_negative(self):
372+ """
373+ The L{VerifyingContextFactory} fails with a SSL error the certificates
374+ can't be checked.
375+ """
376+ context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY)
377+ self.port = reactor.listenSSL(
378+ 0, self.site, context_factory, interface="127.0.0.1")
379+ self.portno = self.port.getHost().port
380+
381+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
382+ query = BaseQuery("an action", "creds", endpoint)
383+ d = query.get_page(self._get_url("file"))
384+ return self.assertFailure(d, SSLError)
385+
386+ def test_ssl_verification_bypassed(self):
387+ """
388+ L{BaseQuery} doesn't use L{VerifyingContextFactory}
389+ if C{ssl_hostname_verification} is C{False}, thus allowing to connect
390+ to non-secure endpoints.
391+ """
392+ context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY)
393+ self.port = reactor.listenSSL(
394+ 0, self.site, context_factory, interface="127.0.0.1")
395+ self.portno = self.port.getHost().port
396+
397+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=False)
398+ query = BaseQuery("an action", "creds", endpoint)
399+ d = query.get_page(self._get_url("file"))
400+ return d.addCallback(self.assertEquals, "0123456789")
401+
402+ def test_ssl_subject_alt_name(self):
403+ """
404+ L{VerifyingContextFactory} supports checking C{subjectAltName} in the
405+ certificate if it's available.
406+ """
407+ context_factory = DefaultOpenSSLContextFactory(PRIVSANKEY, PUBSANKEY)
408+ self.port = reactor.listenSSL(
409+ 0, self.site, context_factory, interface="127.0.0.1")
410+ self.portno = self.port.getHost().port
411+
412+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
413+ query = BaseQuery("an action", "creds", endpoint)
414+ d = query.get_page("https://127.0.0.1:%d/file" % (self.portno,))
415+ return d.addCallback(self.assertEquals, "0123456789")
416+
417+ if pyopenssl_version < "0.12":
418+ test_ssl_subject_alt_name.skip = (
419+ "subjectAltName not supported by older PyOpenSSL")
420+
421+
422+class CertsFilesTestCase(TXAWSTestCase):
423+
424+ def setUp(self):
425+ super(CertsFilesTestCase, self).setUp()
426+ # set up temp dir with no certs
427+ self.no_certs_dir = tempfile.mkdtemp()
428+ # create certs
429+ cert1 = makeCertificate(O="Server Certificate 1", CN="cn1")
430+ cert2 = makeCertificate(O="Server Certificate 2", CN="cn2")
431+ cert3 = makeCertificate(O="Server Certificate 3", CN="cn3")
432+ # set up temp dir with one cert
433+ self.one_cert_dir = tempfile.mkdtemp()
434+ self.cert1 = self._write_pem(cert1, self.one_cert_dir, "cert1.pem")
435+ # set up temp dir with two certs
436+ self.two_certs_dir = tempfile.mkdtemp()
437+ self.cert2 = self._write_pem(cert2, self.two_certs_dir, "cert2.pem")
438+ self.cert3 = self._write_pem(cert3, self.two_certs_dir, "cert3.pem")
439+
440+ def tearDown(self):
441+ super(CertsFilesTestCase, self).tearDown()
442+ os.unlink(self.cert1)
443+ os.unlink(self.cert2)
444+ os.unlink(self.cert3)
445+ os.removedirs(self.no_certs_dir)
446+ os.removedirs(self.one_cert_dir)
447+ os.removedirs(self.two_certs_dir)
448+
449+ def _write_pem(self, cert, dir, filename):
450+ data = dump_certificate(FILETYPE_PEM, cert[1])
451+ full_path = os.path.join(dir, filename)
452+ fh = open(full_path, "w")
453+ fh.write(data)
454+ fh.close()
455+ return full_path
456+
457+ def test_get_ca_certs_no_certs(self):
458+ os.environ["TXAWS_CERTS_PATH"] = self.no_certs_dir
459+ self.patch(ssl, "DEFAULT_CERTS_PATH", self.no_certs_dir)
460+ self.assertRaises(exception.CertsNotFoundError, ssl.get_ca_certs)
461+
462+ def test_get_ca_certs_with_default_path(self):
463+ self.patch(ssl, "DEFAULT_CERTS_PATH", self.two_certs_dir)
464+ certs = ssl.get_ca_certs()
465+ self.assertEqual(len(certs), 2)
466+
467+ def test_get_ca_certs_with_env_path(self):
468+ os.environ["TXAWS_CERTS_PATH"] = self.one_cert_dir
469+ certs = ssl.get_ca_certs()
470+ self.assertEqual(len(certs), 1)
471+
472+ def test_get_ca_certs_multiple_paths(self):
473+ os.environ["TXAWS_CERTS_PATH"] = "%s:%s" % (
474+ self.one_cert_dir, self.two_certs_dir)
475+ certs = ssl.get_ca_certs()
476+ self.assertEqual(len(certs), 3)
477+
478+ def test_get_ca_certs_one_empty_path(self):
479+ os.environ["TXAWS_CERTS_PATH"] = "%s:%s" % (
480+ self.no_certs_dir, self.one_cert_dir)
481+ certs = ssl.get_ca_certs()
482+ self.assertEqual(len(certs), 1)
483
484=== modified file 'txaws/exception.py'
485--- txaws/exception.py 2012-01-23 00:48:29 +0000
486+++ txaws/exception.py 2012-01-26 23:08:23 +0000
487@@ -126,3 +126,9 @@
488 """
489 txAWS was unable to parse the server response.
490 """
491+
492+
493+class CertsNotFoundError(Exception):
494+ """
495+ txAWS was not able to find any SSL certificates.
496+ """
497
498=== modified file 'txaws/s3/client.py'
499--- txaws/s3/client.py 2012-01-26 02:19:17 +0000
500+++ txaws/s3/client.py 2012-01-26 23:08:23 +0000
501@@ -198,7 +198,6 @@
502 def _parse_lifecycle_config(self, xml_bytes):
503 """Parse a C{LifecycleConfiguration} XML document."""
504 root = XML(xml_bytes)
505- contents = []
506 rules = []
507
508 for content_data in root.findall("Rule"):
509@@ -209,7 +208,7 @@
510 rules.append(
511 LifecycleConfigurationRule(id, prefix, status, expiration))
512
513- return LifecycleConfiguration(rules)
514+ return LifecycleConfiguration(rules)
515
516 def get_bucket_acl(self, bucket):
517 """
518
519=== modified file 'txaws/s3/tests/test_acls.py'
520--- txaws/s3/tests/test_acls.py 2012-01-24 23:01:13 +0000
521+++ txaws/s3/tests/test_acls.py 2012-01-26 23:08:23 +0000
522@@ -41,7 +41,8 @@
523 grantee = acls.Grantee(email_address="BucketOwnersEmail@amazon.com")
524 xml_bytes = grantee.to_xml()
525 self.assertEquals(xml_bytes, """\
526-<Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="AmazonCustomerByEmail">
527+<Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\
528+ xsi:type="AmazonCustomerByEmail">
529 <EmailAddress>BucketOwnersEmail@amazon.com</EmailAddress>
530 </Grantee>
531 """)
532@@ -51,7 +52,8 @@
533 uri='http://acs.amazonaws.com/groups/global/AuthenticatedUsers')
534 xml_bytes = grantee.to_xml()
535 self.assertEquals(xml_bytes, """\
536-<Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Group">
537+<Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\
538+ xsi:type="Group">
539 <URI>http://acs.amazonaws.com/groups/global/AuthenticatedUsers</URI>
540 </Grantee>
541 """)

Subscribers

People subscribed via source and target branches