~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/web/test/test_httpauth.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2009 Twisted Matrix Laboratories.
 
2
# See LICENSE for details.
 
3
 
 
4
"""
 
5
Tests for L{twisted.web._auth}.
 
6
"""
 
7
 
 
8
 
 
9
from zope.interface import implements
 
10
from zope.interface.verify import verifyObject
 
11
 
 
12
from twisted.trial import unittest
 
13
 
 
14
from twisted.internet.address import IPv4Address
 
15
 
 
16
from twisted.cred import error, portal
 
17
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
 
18
from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess
 
19
from twisted.cred.credentials import IUsernamePassword
 
20
 
 
21
from twisted.web.iweb import ICredentialFactory
 
22
from twisted.web.resource import IResource, Resource, getChildForRequest
 
23
from twisted.web._auth import basic, digest
 
24
from twisted.web._auth.wrapper import HTTPAuthSessionWrapper, UnauthorizedResource
 
25
from twisted.web._auth.basic import BasicCredentialFactory
 
26
 
 
27
from twisted.web.server import NOT_DONE_YET
 
28
from twisted.web.static import Data
 
29
 
 
30
from twisted.web.test.test_web import DummyRequest
 
31
 
 
32
 
 
33
def b64encode(s):
 
34
    return s.encode('base64').strip()
 
35
 
 
36
 
 
37
class BasicAuthTestsMixin:
 
38
    """
 
39
    L{TestCase} mixin class which defines a number of tests for
 
40
    L{basic.BasicCredentialFactory}.  Because this mixin defines C{setUp}, it
 
41
    must be inherited before L{TestCase}.
 
42
    """
 
43
    def setUp(self):
 
44
        self.request = self.makeRequest()
 
45
        self.realm = 'foo'
 
46
        self.username = 'dreid'
 
47
        self.password = 'S3CuR1Ty'
 
48
        self.credentialFactory = basic.BasicCredentialFactory(self.realm)
 
49
 
 
50
 
 
51
    def makeRequest(self, method='GET', clientAddress=None):
 
52
        """
 
53
        Create a request object to be passed to
 
54
        L{basic.BasicCredentialFactory.decode} along with a response value.
 
55
        Override this in a subclass.
 
56
        """
 
57
        raise NotImplementedError("%r did not implement makeRequest" % (
 
58
                self.__class__,))
 
59
 
 
60
 
 
61
    def test_interface(self):
 
62
        """
 
63
        L{BasicCredentialFactory} implements L{ICredentialFactory}.
 
64
        """
 
65
        self.assertTrue(
 
66
            verifyObject(ICredentialFactory, self.credentialFactory))
 
67
 
 
68
 
 
69
    def test_usernamePassword(self):
 
70
        """
 
71
        L{basic.BasicCredentialFactory.decode} turns a base64-encoded response
 
72
        into a L{UsernamePassword} object with a password which reflects the
 
73
        one which was encoded in the response.
 
74
        """
 
75
        response = b64encode('%s:%s' % (self.username, self.password))
 
76
 
 
77
        creds = self.credentialFactory.decode(response, self.request)
 
78
        self.assertTrue(IUsernamePassword.providedBy(creds))
 
79
        self.assertTrue(creds.checkPassword(self.password))
 
80
        self.assertFalse(creds.checkPassword(self.password + 'wrong'))
 
81
 
 
82
 
 
83
    def test_incorrectPadding(self):
 
84
        """
 
85
        L{basic.BasicCredentialFactory.decode} decodes a base64-encoded
 
86
        response with incorrect padding.
 
87
        """
 
88
        response = b64encode('%s:%s' % (self.username, self.password))
 
89
        response = response.strip('=')
 
90
 
 
91
        creds = self.credentialFactory.decode(response, self.request)
 
92
        self.assertTrue(verifyObject(IUsernamePassword, creds))
 
93
        self.assertTrue(creds.checkPassword(self.password))
 
94
 
 
95
 
 
96
    def test_invalidEncoding(self):
 
97
        """
 
98
        L{basic.BasicCredentialFactory.decode} raises L{LoginFailed} if passed
 
99
        a response which is not base64-encoded.
 
100
        """
 
101
        response = 'x' # one byte cannot be valid base64 text
 
102
        self.assertRaises(
 
103
            error.LoginFailed,
 
104
            self.credentialFactory.decode, response, self.makeRequest())
 
105
 
 
106
 
 
107
    def test_invalidCredentials(self):
 
108
        """
 
109
        L{basic.BasicCredentialFactory.decode} raises L{LoginFailed} when
 
110
        passed a response which is not valid base64-encoded text.
 
111
        """
 
112
        response = b64encode('123abc+/')
 
113
        self.assertRaises(
 
114
            error.LoginFailed,
 
115
            self.credentialFactory.decode,
 
116
            response, self.makeRequest())
 
117
 
 
118
 
 
119
class RequestMixin:
 
120
    def makeRequest(self, method='GET', clientAddress=None):
 
121
        """
 
122
        Create a L{DummyRequest} (change me to create a
 
123
        L{twisted.web.http.Request} instead).
 
124
        """
 
125
        request = DummyRequest('/')
 
126
        request.method = method
 
127
        request.client = clientAddress
 
128
        return request
 
129
 
 
130
 
 
131
 
 
132
class BasicAuthTestCase(RequestMixin, BasicAuthTestsMixin, unittest.TestCase):
 
133
    """
 
134
    Basic authentication tests which use L{twisted.web.http.Request}.
 
135
    """
 
136
 
 
137
 
 
138
 
 
139
class DigestAuthTestCase(RequestMixin, unittest.TestCase):
 
140
    """
 
141
    Digest authentication tests which use L{twisted.web.http.Request}.
 
142
    """
 
143
 
 
144
    def setUp(self):
 
145
        """
 
146
        Create a DigestCredentialFactory for testing
 
147
        """
 
148
        self.realm = "test realm"
 
149
        self.algorithm = "md5"
 
150
        self.credentialFactory = digest.DigestCredentialFactory(
 
151
            self.algorithm, self.realm)
 
152
        self.request = self.makeRequest()
 
153
 
 
154
 
 
155
    def test_decode(self):
 
156
        """
 
157
        L{digest.DigestCredentialFactory.decode} calls the C{decode} method on
 
158
        L{twisted.cred.digest.DigestCredentialFactory} with the HTTP method and
 
159
        host of the request.
 
160
        """
 
161
        host = '169.254.0.1'
 
162
        method = 'GET'
 
163
        done = [False]
 
164
        response = object()
 
165
        def check(_response, _method, _host):
 
166
            self.assertEqual(response, _response)
 
167
            self.assertEqual(method, _method)
 
168
            self.assertEqual(host, _host)
 
169
            done[0] = True
 
170
 
 
171
        self.patch(self.credentialFactory.digest, 'decode', check)
 
172
        req = self.makeRequest(method, IPv4Address('TCP', host, 81))
 
173
        self.credentialFactory.decode(response, req)
 
174
        self.assertTrue(done[0])
 
175
 
 
176
 
 
177
    def test_interface(self):
 
178
        """
 
179
        L{DigestCredentialFactory} implements L{ICredentialFactory}.
 
180
        """
 
181
        self.assertTrue(
 
182
            verifyObject(ICredentialFactory, self.credentialFactory))
 
183
 
 
184
 
 
185
    def test_getChallenge(self):
 
186
        """
 
187
        The challenge issued by L{DigestCredentialFactory.getChallenge} must
 
188
        include C{'qop'}, C{'realm'}, C{'algorithm'}, C{'nonce'}, and
 
189
        C{'opaque'} keys.  The values for the C{'realm'} and C{'algorithm'}
 
190
        keys must match the values supplied to the factory's initializer.
 
191
        None of the values may have newlines in them.
 
192
        """
 
193
        challenge = self.credentialFactory.getChallenge(self.request)
 
194
        self.assertEquals(challenge['qop'], 'auth')
 
195
        self.assertEquals(challenge['realm'], 'test realm')
 
196
        self.assertEquals(challenge['algorithm'], 'md5')
 
197
        self.assertIn('nonce', challenge)
 
198
        self.assertIn('opaque', challenge)
 
199
        for v in challenge.values():
 
200
            self.assertNotIn('\n', v)
 
201
 
 
202
 
 
203
    def test_getChallengeWithoutClientIP(self):
 
204
        """
 
205
        L{DigestCredentialFactory.getChallenge} can issue a challenge even if
 
206
        the L{Request} it is passed returns C{None} from C{getClientIP}.
 
207
        """
 
208
        request = self.makeRequest('GET', None)
 
209
        challenge = self.credentialFactory.getChallenge(request)
 
210
        self.assertEqual(challenge['qop'], 'auth')
 
211
        self.assertEqual(challenge['realm'], 'test realm')
 
212
        self.assertEqual(challenge['algorithm'], 'md5')
 
213
        self.assertIn('nonce', challenge)
 
214
        self.assertIn('opaque', challenge)
 
215
 
 
216
 
 
217
 
 
218
class UnauthorizedResourceTests(unittest.TestCase):
 
219
    """
 
220
    Tests for L{UnauthorizedResource}.
 
221
    """
 
222
    def test_getChildWithDefault(self):
 
223
        """
 
224
        An L{UnauthorizedResource} is every child of itself.
 
225
        """
 
226
        resource = UnauthorizedResource([])
 
227
        self.assertIdentical(
 
228
            resource.getChildWithDefault("foo", None), resource)
 
229
        self.assertIdentical(
 
230
            resource.getChildWithDefault("bar", None), resource)
 
231
 
 
232
 
 
233
    def test_render(self):
 
234
        """
 
235
        L{UnauthorizedResource} renders with a 401 response code and a
 
236
        I{WWW-Authenticate} header and puts a simple unauthorized message
 
237
        into the response body.
 
238
        """
 
239
        resource = UnauthorizedResource([
 
240
                BasicCredentialFactory('example.com')])
 
241
        request = DummyRequest([''])
 
242
        request.render(resource)
 
243
        self.assertEqual(request.responseCode, 401)
 
244
        self.assertEqual(
 
245
            request.responseHeaders.getRawHeaders('www-authenticate'),
 
246
            ['basic realm="example.com"'])
 
247
        self.assertEqual(request.written, ['Unauthorized'])
 
248
 
 
249
 
 
250
    def test_renderQuotesRealm(self):
 
251
        """
 
252
        The realm value included in the I{WWW-Authenticate} header set in
 
253
        the response when L{UnauthorizedResounrce} is rendered has quotes
 
254
        and backslashes escaped.
 
255
        """
 
256
        resource = UnauthorizedResource([
 
257
                BasicCredentialFactory('example\\"foo')])
 
258
        request = DummyRequest([''])
 
259
        request.render(resource)
 
260
        self.assertEqual(
 
261
            request.responseHeaders.getRawHeaders('www-authenticate'),
 
262
            ['basic realm="example\\\\\\"foo"'])
 
263
 
 
264
 
 
265
 
 
266
class Realm(object):
 
267
    """
 
268
    A simple L{IRealm} implementation which gives out L{WebAvatar} for any
 
269
    avatarId.
 
270
 
 
271
    @type loggedIn: C{int}
 
272
    @ivar loggedIn: The number of times C{requestAvatar} has been invoked for
 
273
        L{IResource}.
 
274
 
 
275
    @type loggedOut: C{int}
 
276
    @ivar loggedOut: The number of times the logout callback has been invoked.
 
277
    """
 
278
    implements(portal.IRealm)
 
279
 
 
280
    def __init__(self, avatarFactory):
 
281
        self.loggedOut = 0
 
282
        self.loggedIn = 0
 
283
        self.avatarFactory = avatarFactory
 
284
 
 
285
 
 
286
    def requestAvatar(self, avatarId, mind, *interfaces):
 
287
        if IResource in interfaces:
 
288
            self.loggedIn += 1
 
289
            return IResource, self.avatarFactory(avatarId), self.logout
 
290
        raise NotImplementedError()
 
291
 
 
292
 
 
293
    def logout(self):
 
294
        self.loggedOut += 1
 
295
 
 
296
 
 
297
 
 
298
class HTTPAuthHeaderTests(unittest.TestCase):
 
299
    """
 
300
    Tests for L{HTTPAuthSessionWrapper}.
 
301
    """
 
302
    makeRequest = DummyRequest
 
303
 
 
304
    def setUp(self):
 
305
        """
 
306
        Create a realm, portal, and L{HTTPAuthSessionWrapper} to use in the tests.
 
307
        """
 
308
        self.username = 'foo bar'
 
309
        self.password = 'bar baz'
 
310
        self.avatarContent = "contents of the avatar resource itself"
 
311
        self.childName = "foo-child"
 
312
        self.childContent = "contents of the foo child of the avatar"
 
313
        self.checker = InMemoryUsernamePasswordDatabaseDontUse()
 
314
        self.checker.addUser(self.username, self.password)
 
315
        self.avatar = Data(self.avatarContent, 'text/plain')
 
316
        self.avatar.putChild(
 
317
            self.childName, Data(self.childContent, 'text/plain'))
 
318
        self.avatars = {self.username: self.avatar}
 
319
        self.realm = Realm(self.avatars.get)
 
320
        self.portal = portal.Portal(self.realm, [self.checker])
 
321
        self.credentialFactories = []
 
322
        self.wrapper = HTTPAuthSessionWrapper(
 
323
            self.portal, self.credentialFactories)
 
324
 
 
325
 
 
326
    def _authorizedBasicLogin(self, request):
 
327
        """
 
328
        Add an I{basic authorization} header to the given request and then
 
329
        dispatch it, starting from C{self.wrapper} and returning the resulting
 
330
        L{IResource}.
 
331
        """
 
332
        authorization = b64encode(self.username + ':' + self.password)
 
333
        request.headers['authorization'] = 'Basic ' + authorization
 
334
        return getChildForRequest(self.wrapper, request)
 
335
 
 
336
 
 
337
    def test_getChildWithDefault(self):
 
338
        """
 
339
        Resource traversal which encounters an L{HTTPAuthSessionWrapper}
 
340
        results in an L{UnauthorizedResource} instance when the request does
 
341
        not have the required I{Authorization} headers.
 
342
        """
 
343
        request = self.makeRequest([self.childName])
 
344
        child = getChildForRequest(self.wrapper, request)
 
345
        d = request.notifyFinish()
 
346
        def cbFinished(result):
 
347
            self.assertEquals(request.responseCode, 401)
 
348
        d.addCallback(cbFinished)
 
349
        request.render(child)
 
350
        return d
 
351
 
 
352
 
 
353
    def _invalidAuthorizationTest(self, response):
 
354
        """
 
355
        Create a request with the given value as the value of an
 
356
        I{Authorization} header and perform resource traversal with it,
 
357
        starting at C{self.wrapper}.  Assert that the result is a 401 response
 
358
        code.  Return a L{Deferred} which fires when this is all done.
 
359
        """
 
360
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
361
        request = self.makeRequest([self.childName])
 
362
        request.headers['authorization'] = response
 
363
        child = getChildForRequest(self.wrapper, request)
 
364
        d = request.notifyFinish()
 
365
        def cbFinished(result):
 
366
            self.assertEqual(request.responseCode, 401)
 
367
        d.addCallback(cbFinished)
 
368
        request.render(child)
 
369
        return d
 
370
 
 
371
 
 
372
    def test_getChildWithDefaultUnauthorizedUser(self):
 
373
        """
 
374
        Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
 
375
        results in an L{UnauthorizedResource} when the request has an
 
376
        I{Authorization} header with a user which does not exist.
 
377
        """
 
378
        return self._invalidAuthorizationTest('Basic ' + b64encode('foo:bar'))
 
379
 
 
380
 
 
381
    def test_getChildWithDefaultUnauthorizedPassword(self):
 
382
        """
 
383
        Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
 
384
        results in an L{UnauthorizedResource} when the request has an
 
385
        I{Authorization} header with a user which exists and the wrong
 
386
        password.
 
387
        """
 
388
        return self._invalidAuthorizationTest(
 
389
            'Basic ' + b64encode(self.username + ':bar'))
 
390
 
 
391
 
 
392
    def test_getChildWithDefaultUnrecognizedScheme(self):
 
393
        """
 
394
        Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
 
395
        results in an L{UnauthorizedResource} when the request has an
 
396
        I{Authorization} header with an unrecognized scheme.
 
397
        """
 
398
        return self._invalidAuthorizationTest('Quux foo bar baz')
 
399
 
 
400
 
 
401
    def test_getChildWithDefaultAuthorized(self):
 
402
        """
 
403
        Resource traversal which encounters an L{HTTPAuthSessionWrapper}
 
404
        results in an L{IResource} which renders the L{IResource} avatar
 
405
        retrieved from the portal when the request has a valid I{Authorization}
 
406
        header.
 
407
        """
 
408
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
409
        request = self.makeRequest([self.childName])
 
410
        child = self._authorizedBasicLogin(request)
 
411
        d = request.notifyFinish()
 
412
        def cbFinished(ignored):
 
413
            self.assertEquals(request.written, [self.childContent])
 
414
        d.addCallback(cbFinished)
 
415
        request.render(child)
 
416
        return d
 
417
 
 
418
 
 
419
    def test_renderAuthorized(self):
 
420
        """
 
421
        Resource traversal which terminates at an L{HTTPAuthSessionWrapper}
 
422
        and includes correct authentication headers results in the
 
423
        L{IResource} avatar (not one of its children) retrieved from the
 
424
        portal being rendered.
 
425
        """
 
426
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
427
        # Request it exactly, not any of its children.
 
428
        request = self.makeRequest([])
 
429
        child = self._authorizedBasicLogin(request)
 
430
        d = request.notifyFinish()
 
431
        def cbFinished(ignored):
 
432
            self.assertEquals(request.written, [self.avatarContent])
 
433
        d.addCallback(cbFinished)
 
434
        request.render(child)
 
435
        return d
 
436
 
 
437
 
 
438
    def test_getChallengeCalledWithRequest(self):
 
439
        """
 
440
        When L{HTTPAuthSessionWrapper} finds an L{ICredentialFactory} to issue
 
441
        a challenge, it calls the C{getChallenge} method with the request as an
 
442
        argument.
 
443
        """
 
444
        class DumbCredentialFactory(object):
 
445
            implements(ICredentialFactory)
 
446
            scheme = 'dumb'
 
447
 
 
448
            def __init__(self):
 
449
                self.requests = []
 
450
 
 
451
            def getChallenge(self, request):
 
452
                self.requests.append(request)
 
453
                return {}
 
454
 
 
455
        factory = DumbCredentialFactory()
 
456
        self.credentialFactories.append(factory)
 
457
        request = self.makeRequest([self.childName])
 
458
        child = getChildForRequest(self.wrapper, request)
 
459
        d = request.notifyFinish()
 
460
        def cbFinished(ignored):
 
461
            self.assertEqual(factory.requests, [request])
 
462
        d.addCallback(cbFinished)
 
463
        request.render(child)
 
464
        return d
 
465
 
 
466
 
 
467
    def test_logout(self):
 
468
        """
 
469
        The realm's logout callback is invoked after the resource is rendered.
 
470
        """
 
471
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
472
 
 
473
        class SlowerResource(Resource):
 
474
            def render(self, request):
 
475
                return NOT_DONE_YET
 
476
 
 
477
        self.avatar.putChild(self.childName, SlowerResource())
 
478
        request = self.makeRequest([self.childName])
 
479
        child = self._authorizedBasicLogin(request)
 
480
        request.render(child)
 
481
        self.assertEqual(self.realm.loggedOut, 0)
 
482
        request.finish()
 
483
        self.assertEqual(self.realm.loggedOut, 1)
 
484
 
 
485
 
 
486
    def test_decodeRaises(self):
 
487
        """
 
488
        Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
 
489
        results in an L{UnauthorizedResource} when the request has a I{Basic
 
490
        Authorization} header which cannot be decoded using base64.
 
491
        """
 
492
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
493
        request = self.makeRequest([self.childName])
 
494
        request.headers['authorization'] = 'Basic decode should fail'
 
495
        child = getChildForRequest(self.wrapper, request)
 
496
        self.assertIsInstance(child, UnauthorizedResource)
 
497
 
 
498
 
 
499
    def test_selectParseResponse(self):
 
500
        """
 
501
        L{HTTPAuthSessionWrapper._selectParseHeader} returns a two-tuple giving
 
502
        the L{ICredentialFactory} to use to parse the header and a string
 
503
        containing the portion of the header which remains to be parsed.
 
504
        """
 
505
        basicAuthorization = 'Basic abcdef123456'
 
506
        self.assertEqual(
 
507
            self.wrapper._selectParseHeader(basicAuthorization),
 
508
            (None, None))
 
509
        factory = BasicCredentialFactory('example.com')
 
510
        self.credentialFactories.append(factory)
 
511
        self.assertEqual(
 
512
            self.wrapper._selectParseHeader(basicAuthorization),
 
513
            (factory, 'abcdef123456'))
 
514
 
 
515
 
 
516
    def test_unexpectedDecodeError(self):
 
517
        """
 
518
        Any unexpected exception raised by the credential factory's C{decode}
 
519
        method results in a 500 response code and causes the exception to be
 
520
        logged.
 
521
        """
 
522
        class UnexpectedException(Exception):
 
523
            pass
 
524
 
 
525
        class BadFactory(object):
 
526
            scheme = 'bad'
 
527
 
 
528
            def getChallenge(self, client):
 
529
                return {}
 
530
 
 
531
            def decode(self, response, request):
 
532
                raise UnexpectedException()
 
533
 
 
534
        self.credentialFactories.append(BadFactory())
 
535
        request = self.makeRequest([self.childName])
 
536
        request.headers['authorization'] = 'Bad abc'
 
537
        child = getChildForRequest(self.wrapper, request)
 
538
        request.render(child)
 
539
        self.assertEqual(request.responseCode, 500)
 
540
        self.assertEqual(len(self.flushLoggedErrors(UnexpectedException)), 1)
 
541
 
 
542
 
 
543
    def test_unexpectedLoginError(self):
 
544
        """
 
545
        Any unexpected failure from L{Portal.login} results in a 500 response
 
546
        code and causes the failure to be logged.
 
547
        """
 
548
        class UnexpectedException(Exception):
 
549
            pass
 
550
 
 
551
        class BrokenChecker(object):
 
552
            credentialInterfaces = (IUsernamePassword,)
 
553
 
 
554
            def requestAvatarId(self, credentials):
 
555
                raise UnexpectedException()
 
556
 
 
557
        self.portal.registerChecker(BrokenChecker())
 
558
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
559
        request = self.makeRequest([self.childName])
 
560
        child = self._authorizedBasicLogin(request)
 
561
        request.render(child)
 
562
        self.assertEqual(request.responseCode, 500)
 
563
        self.assertEqual(len(self.flushLoggedErrors(UnexpectedException)), 1)
 
564
 
 
565
 
 
566
    def test_anonymousAccess(self):
 
567
        """
 
568
        Anonymous requests are allowed if a L{Portal} has an anonymous checker
 
569
        registered.
 
570
        """
 
571
        unprotectedContents = "contents of the unprotected child resource"
 
572
 
 
573
        self.avatars[ANONYMOUS] = Resource()
 
574
        self.avatars[ANONYMOUS].putChild(
 
575
            self.childName, Data(unprotectedContents, 'text/plain'))
 
576
        self.portal.registerChecker(AllowAnonymousAccess())
 
577
 
 
578
        self.credentialFactories.append(BasicCredentialFactory('example.com'))
 
579
        request = self.makeRequest([self.childName])
 
580
        child = getChildForRequest(self.wrapper, request)
 
581
        d = request.notifyFinish()
 
582
        def cbFinished(ignored):
 
583
            self.assertEquals(request.written, [unprotectedContents])
 
584
        d.addCallback(cbFinished)
 
585
        request.render(child)
 
586
        return d