Merge lp:~rvb/maas/oauth-retry-bug-1435767 into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 3760
Proposed branch: lp:~rvb/maas/oauth-retry-bug-1435767
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 186 lines (+106/-5)
2 files modified
src/maasserver/utils/tests/test_views.py (+90/-5)
src/maasserver/utils/views.py (+16/-0)
To merge this branch: bzr merge lp:~rvb/maas/oauth-retry-bug-1435767
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+255063@code.launchpad.net

Commit message

Delete the nonce before retrying requests.

Description of the change

- I've used the oauth_request.get_parameter() methods in delete_oauth_nonce instead of using the similar methods that return the Consumer and the Token object in order to minimize the number of database queries: the only thing we're interested in here is the Nonce object.

- I realize the testing done in test__get_response_deleted_nonces_across_requests is a bit fragile because it assumes a lot of stuff about the internals but that's the best I could come up with in a testing environment.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Cool.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/utils/tests/test_views.py'
2--- src/maasserver/utils/tests/test_views.py 2015-03-26 00:12:12 +0000
3+++ src/maasserver/utils/tests/test_views.py 2015-04-02 11:03:19 +0000
4@@ -34,10 +34,11 @@
5 from django.http import HttpResponse
6 from django.http.response import HttpResponseServerError
7 from fixtures import FakeLogger
8+#from maastesting.factory import factory
9+from maasserver.testing.factory import factory
10 from maasserver.testing.testcase import SerializationFailureTestCase
11 from maasserver.utils import views
12 from maasserver.utils.orm import validate_in_transaction
13-from maastesting.factory import factory
14 from maastesting.matchers import (
15 MockCalledOnceWith,
16 MockCallsMatch,
17@@ -49,6 +50,8 @@
18 call,
19 sentinel,
20 )
21+from piston.authentication import initialize_server_request
22+from piston.models import Nonce
23 from testtools.matchers import (
24 Contains,
25 Equals,
26@@ -57,17 +60,27 @@
27 IsInstance,
28 Not,
29 )
30+from testtools.testcase import ExpectedException
31 from twisted.internet.task import Clock
32 from twisted.python import log
33 from twisted.web import wsgi
34
35
36-def make_request():
37+def make_request(env=None, oauth_env=None):
38 # Return a minimal WSGIRequest.
39- return WSGIRequest({
40+ if oauth_env is None:
41+ oauth_env = {}
42+ base_env = {
43 "REQUEST_METHOD": "GET",
44 "wsgi.input": wsgi._InputStream(io.BytesIO()),
45- })
46+ "SERVER_NAME": "server",
47+ "SERVER_PORT": 80,
48+ "HTTP_AUTHORIZATION": factory.make_oauth_header(**oauth_env),
49+ }
50+ if env is not None:
51+ base_env.update(env)
52+ request = WSGIRequest(base_env)
53+ return request
54
55
56 class TestLogFunctions(MAASTestCase):
57@@ -121,6 +134,42 @@
58 self.assertEqual({}, request.COOKIES)
59
60
61+class TestDeleteOAuthNonce(MAASTestCase):
62+ """Tests for :py:func:`maasserver.utils.views.delete_oauth_nonce`."""
63+
64+ def test__deletes_nonce(self):
65+ oauth_consumer_key = factory.make_string(18)
66+ oauth_token = factory.make_string(18)
67+ oauth_nonce = randint(0, 99999)
68+ Nonce.objects.create(
69+ consumer_key=oauth_consumer_key, token_key=oauth_token,
70+ key=oauth_nonce)
71+ oauth_env = {
72+ 'oauth_consumer_key': oauth_consumer_key,
73+ 'oauth_token': oauth_token,
74+ 'oauth_nonce': oauth_nonce,
75+ }
76+ request = make_request(oauth_env=oauth_env)
77+ views.delete_oauth_nonce(request)
78+ with ExpectedException(Nonce.DoesNotExist):
79+ Nonce.objects.get(
80+ consumer_key=oauth_consumer_key, token_key=oauth_token,
81+ key=oauth_nonce)
82+
83+ def test__skips_missing_nonce(self):
84+ oauth_consumer_key = factory.make_string(18)
85+ oauth_token = factory.make_string(18)
86+ oauth_nonce = randint(0, 99999)
87+ oauth_env = {
88+ 'oauth_consumer_key': oauth_consumer_key,
89+ 'oauth_token': oauth_token,
90+ 'oauth_nonce': oauth_nonce,
91+ }
92+ request = make_request(oauth_env=oauth_env)
93+ # No exception is raised.
94+ self.assertIsNone(views.delete_oauth_nonce(request))
95+
96+
97 class TestWebApplicationHandler(SerializationFailureTestCase):
98
99 def setUp(self):
100@@ -357,8 +406,44 @@
101 'CONTENT_LENGTH': headers['Content-Length'],
102 'HTTP_MIME_VERSION': headers['MIME-Version'],
103 }
104- request = WSGIRequest(env)
105+ request = make_request(env)
106
107 response = handler.get_response(request)
108 self.assertEqual(file_content, response.content)
109 self.assertEqual(recorder, [file_content] * 3)
110+
111+ def test__get_response_deleted_nonces_across_requests(self):
112+ handler = views.WebApplicationHandler(3)
113+ user = factory.make_User()
114+ token = user.userprofile.get_authorisation_tokens()[0]
115+
116+ recorder = []
117+
118+ def get_response_check_nonce(self, request):
119+ _, oauth_req = initialize_server_request(request)
120+ # get_or _create the Nonce object like the authentication
121+ # mechanism does.
122+ nonce_obj, created = Nonce.objects.get_or_create(
123+ consumer_key=token.consumer.key, token_key=token.key,
124+ key=oauth_req.get_parameter('oauth_nonce'))
125+
126+ # Record calls.
127+ recorder.append(created)
128+ response = HttpResponse(
129+ content='',
130+ status=httplib.OK,
131+ mimetype=b"text/plain; charset=utf-8")
132+ handler._WebApplicationHandler__retry.add(response)
133+ return response
134+
135+ self.patch(
136+ WSGIHandler, "get_response", get_response_check_nonce)
137+
138+ oauth_env = {
139+ 'oauth_consumer_key': token.consumer.key,
140+ 'oauth_token': token.key,
141+ }
142+ request = make_request(oauth_env=oauth_env)
143+
144+ handler.get_response(request)
145+ self.assertEqual(recorder, [True] * 3, "Nonce hasn't been cleaned up!")
146
147=== modified file 'src/maasserver/utils/views.py'
148--- src/maasserver/utils/views.py 2015-03-26 00:12:12 +0000
149+++ src/maasserver/utils/views.py 2015-04-02 11:03:19 +0000
150@@ -31,6 +31,8 @@
151 is_serialization_failure,
152 post_commit_hooks,
153 )
154+from piston.authentication import initialize_server_request
155+from piston.models import Nonce
156 from provisioningserver.utils.twisted import retries
157 from twisted.internet import reactor as clock
158 from twisted.python import log
159@@ -56,6 +58,19 @@
160 attempt, request.path, elapsed)
161
162
163+def delete_oauth_nonce(request):
164+ """Delete the OAuth nonce for the given request from the database.
165+
166+ This is to allow the exact same request to be retried.
167+ """
168+ oauth_server, oauth_request = initialize_server_request(request)
169+ consumer_key = oauth_request.get_parameter('oauth_consumer_key')
170+ token_key = oauth_request.get_parameter('oauth_token')
171+ nonce = oauth_request.get_parameter('oauth_nonce')
172+ Nonce.objects.filter(
173+ consumer_key=consumer_key, token_key=token_key, key=nonce).delete()
174+
175+
176 def reset_request(request):
177 """Return a pristine new request object.
178
179@@ -172,6 +187,7 @@
180 return response
181 # We'll retry after a brief interlude.
182 log_failed_attempt(request, attempt, elapsed, remaining, wait)
183+ delete_oauth_nonce(request)
184 request = reset_request(request)
185 sleep(wait)
186 else: