Merge lp:~michael.nelson/ubuntu-webcatalog/1040523-oops-creating-nonce into lp:ubuntu-webcatalog
- 1040523-oops-creating-nonce
- Merge into trunk
Proposed by
Michael Nelson
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Anthony Lenton | ||||
Approved revision: | 169 | ||||
Merged at revision: | 168 | ||||
Proposed branch: | lp:~michael.nelson/ubuntu-webcatalog/1040523-oops-creating-nonce | ||||
Merge into: | lp:ubuntu-webcatalog | ||||
Diff against target: |
305 lines (+248/-4) 5 files modified
src/webcatalog/migrations/0028_nonce_unique_together.py (+174/-0) src/webcatalog/models/oauthtoken.py (+2/-1) src/webcatalog/tests/__init__.py (+1/-0) src/webcatalog/tests/factory.py (+6/-3) src/webcatalog/tests/test_models_oauth.py (+65/-0) |
||||
To merge this branch: | bzr merge lp:~michael.nelson/ubuntu-webcatalog/1040523-oops-creating-nonce | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Anthony Lenton (community) | Approve | ||
Review via email: mp+120959@code.launchpad.net |
Commit message
Nonce is unique per consumer/token rather than unique for the table.
Description of the change
Overview
========
See the pre-imp discussion with myself on the bug :P. The code was checking whether a nonce existed for a given consumer/token, and if not creating the nonce, even though the nonce may not be unique itself - causing nearly 4k oopses per day.
This branch relaxes the unique constraint on Nonce.nonce so that it is required to be unique for the consumer/token only, matching the actual check itself.
`fab test`
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'src/webcatalog/migrations/0028_nonce_unique_together.py' |
2 | --- src/webcatalog/migrations/0028_nonce_unique_together.py 1970-01-01 00:00:00 +0000 |
3 | +++ src/webcatalog/migrations/0028_nonce_unique_together.py 2012-08-23 10:17:22 +0000 |
4 | @@ -0,0 +1,174 @@ |
5 | +# encoding: utf-8 |
6 | +import datetime |
7 | +from south.db import db |
8 | +from south.v2 import SchemaMigration |
9 | +from django.db import models |
10 | + |
11 | +class Migration(SchemaMigration): |
12 | + |
13 | + def forwards(self, orm): |
14 | + |
15 | + # Removing unique constraint on 'Nonce', fields ['nonce'] |
16 | + db.delete_unique('webcatalog_nonce', ['nonce']) |
17 | + |
18 | + # Adding unique constraint on 'Nonce', fields ['nonce', 'token', 'consumer'] |
19 | + db.create_unique('webcatalog_nonce', ['nonce', 'token_id', 'consumer_id']) |
20 | + |
21 | + |
22 | + def backwards(self, orm): |
23 | + |
24 | + # Removing unique constraint on 'Nonce', fields ['nonce', 'token', 'consumer'] |
25 | + db.delete_unique('webcatalog_nonce', ['nonce', 'token_id', 'consumer_id']) |
26 | + |
27 | + # Adding unique constraint on 'Nonce', fields ['nonce'] |
28 | + db.create_unique('webcatalog_nonce', ['nonce']) |
29 | + |
30 | + |
31 | + models = { |
32 | + 'auth.group': { |
33 | + 'Meta': {'object_name': 'Group'}, |
34 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
35 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
36 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
37 | + }, |
38 | + 'auth.permission': { |
39 | + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, |
40 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
41 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), |
42 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
43 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
44 | + }, |
45 | + 'auth.user': { |
46 | + 'Meta': {'object_name': 'User'}, |
47 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
48 | + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), |
49 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
50 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), |
51 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
52 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
53 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
54 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
55 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
56 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
57 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
58 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), |
59 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
60 | + }, |
61 | + 'contenttypes.contenttype': { |
62 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
63 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
64 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
65 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
66 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
67 | + }, |
68 | + 'webcatalog.application': { |
69 | + 'Meta': {'ordering': "('-wilson_score', 'name')", 'unique_together': "(('distroseries', 'package_name'),)", 'object_name': 'Application'}, |
70 | + 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), |
71 | + 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
72 | + 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}), |
73 | + 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
74 | + 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
75 | + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
76 | + 'debtags': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
77 | + 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}), |
78 | + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
79 | + 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}), |
80 | + 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), |
81 | + 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
82 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
83 | + 'imported_from_sca': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
84 | + 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
85 | + 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
86 | + 'license': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), |
87 | + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}), |
88 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
89 | + 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
90 | + 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
91 | + 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}), |
92 | + 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}), |
93 | + 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), |
94 | + 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
95 | + 'section': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), |
96 | + 'version': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), |
97 | + 'wilson_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) |
98 | + }, |
99 | + 'webcatalog.applicationmedia': { |
100 | + 'Meta': {'ordering': "('url',)", 'unique_together': "(('application', 'url'),)", 'object_name': 'ApplicationMedia'}, |
101 | + 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Application']"}), |
102 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
103 | + 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), |
104 | + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) |
105 | + }, |
106 | + 'webcatalog.consumer': { |
107 | + 'Meta': {'object_name': 'Consumer'}, |
108 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
109 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
110 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
111 | + 'secret': ('django.db.models.fields.CharField', [], {'default': "'YduzTarbGEAUckidRHwqIvXkbMOIWW'", 'max_length': '255', 'blank': 'True'}), |
112 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
113 | + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"}) |
114 | + }, |
115 | + 'webcatalog.department': { |
116 | + 'Meta': {'object_name': 'Department'}, |
117 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
118 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
119 | + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}), |
120 | + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}) |
121 | + }, |
122 | + 'webcatalog.distroseries': { |
123 | + 'Meta': {'object_name': 'DistroSeries'}, |
124 | + 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), |
125 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
126 | + 'prerelease': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
127 | + 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}) |
128 | + }, |
129 | + 'webcatalog.exhibit': { |
130 | + 'Meta': {'object_name': 'Exhibit'}, |
131 | + 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), |
132 | + 'click_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'}), |
133 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
134 | + 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), |
135 | + 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}), |
136 | + 'html': ('django.db.models.fields.TextField', [], {}), |
137 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
138 | + 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), |
139 | + 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
140 | + 'sca_id': ('django.db.models.fields.IntegerField', [], {}), |
141 | + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'}) |
142 | + }, |
143 | + 'webcatalog.machine': { |
144 | + 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'}, |
145 | + 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
146 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
147 | + 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}), |
148 | + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), |
149 | + 'package_list': ('django.db.models.fields.TextField', [], {}), |
150 | + 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}), |
151 | + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}) |
152 | + }, |
153 | + 'webcatalog.nonce': { |
154 | + 'Meta': {'unique_together': "(('nonce', 'token', 'consumer'),)", 'object_name': 'Nonce'}, |
155 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}), |
156 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
157 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
158 | + 'nonce': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
159 | + 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"}) |
160 | + }, |
161 | + 'webcatalog.reviewstatsimport': { |
162 | + 'Meta': {'object_name': 'ReviewStatsImport'}, |
163 | + 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}), |
164 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
165 | + 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}) |
166 | + }, |
167 | + 'webcatalog.token': { |
168 | + 'Meta': {'object_name': 'Token'}, |
169 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}), |
170 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
171 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
172 | + 'token': ('django.db.models.fields.CharField', [], {'default': "'oHbbjpYHzIoCMTZpBVYAeMYiogLeMYUWeHyabzbvrPRqEzijjY'", 'max_length': '50', 'primary_key': 'True'}), |
173 | + 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'QdhhAEWTjdtjTMnLoBEdnEIEzgFowyDRwkQjGDVtAlFpKgxvmK'", 'max_length': '50'}), |
174 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
175 | + } |
176 | + } |
177 | + |
178 | + complete_apps = ['webcatalog'] |
179 | |
180 | === modified file 'src/webcatalog/models/oauthtoken.py' |
181 | --- src/webcatalog/models/oauthtoken.py 2012-06-06 16:38:11 +0000 |
182 | +++ src/webcatalog/models/oauthtoken.py 2012-08-23 10:17:22 +0000 |
183 | @@ -135,7 +135,7 @@ |
184 | class Nonce(models.Model): |
185 | token = models.ForeignKey(Token) |
186 | consumer = models.ForeignKey(Consumer) |
187 | - nonce = models.CharField(max_length=255, unique=True, editable=False) |
188 | + nonce = models.CharField(max_length=255, editable=False) |
189 | created_at = models.DateTimeField(auto_now_add=True) |
190 | |
191 | @classmethod |
192 | @@ -149,6 +149,7 @@ |
193 | |
194 | class Meta: |
195 | app_label = 'webcatalog' |
196 | + unique_together = ('nonce', 'token', 'consumer') |
197 | |
198 | |
199 | class DataStore(OAuthDataStore): |
200 | |
201 | === modified file 'src/webcatalog/tests/__init__.py' |
202 | --- src/webcatalog/tests/__init__.py 2012-06-28 18:26:25 +0000 |
203 | +++ src/webcatalog/tests/__init__.py 2012-08-23 10:17:22 +0000 |
204 | @@ -23,6 +23,7 @@ |
205 | from .test_forms import * |
206 | from .test_handlers import * |
207 | from .test_models import * |
208 | +from .test_models_oauth import * |
209 | from .test_managers import * |
210 | from .test_migrations import * |
211 | from .test_pep8 import * |
212 | |
213 | === modified file 'src/webcatalog/tests/factory.py' |
214 | --- src/webcatalog/tests/factory.py 2012-08-20 15:36:56 +0000 |
215 | +++ src/webcatalog/tests/factory.py 2012-08-23 10:17:22 +0000 |
216 | @@ -225,13 +225,16 @@ |
217 | token.save() |
218 | return token, consumer |
219 | |
220 | - def make_nonce(self, token=None, consumer=None, created_at=None): |
221 | + def make_nonce(self, token=None, consumer=None, created_at=None, |
222 | + nonce_string=None): |
223 | if token is None or consumer is None: |
224 | assert token is None and consumer is None |
225 | token, consumer = self.make_oauth_token_and_consumer() |
226 | + if nonce_string is None: |
227 | + nonce_string = self.get_unique_string(prefix='nonce-') |
228 | + |
229 | nonce = Nonce.objects.create( |
230 | - token=token, consumer=consumer, |
231 | - nonce=self.get_unique_string(prefix='nonce-')) |
232 | + token=token, consumer=consumer, nonce=nonce_string) |
233 | if created_at: |
234 | nonce.created_at = created_at |
235 | nonce.save() |
236 | |
237 | === added file 'src/webcatalog/tests/test_models_oauth.py' |
238 | --- src/webcatalog/tests/test_models_oauth.py 1970-01-01 00:00:00 +0000 |
239 | +++ src/webcatalog/tests/test_models_oauth.py 2012-08-23 10:17:22 +0000 |
240 | @@ -0,0 +1,65 @@ |
241 | +# -*- coding: utf-8 -*- |
242 | +# This file is part of the Apps Directory |
243 | +# Copyright (C) 2011 Canonical Ltd. |
244 | +# |
245 | +# This program is free software: you can redistribute it and/or modify |
246 | +# it under the terms of the GNU Affero General Public License as |
247 | +# published by the Free Software Foundation, either version 3 of the |
248 | +# License, or (at your option) any later version. |
249 | +# |
250 | +# This program is distributed in the hope that it will be useful, |
251 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
252 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
253 | +# GNU Affero General Public License for more details. |
254 | +# |
255 | +# You should have received a copy of the GNU Affero General Public License |
256 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
257 | + |
258 | +"""Test cases for oauth models.""" |
259 | + |
260 | +from webcatalog.models import ( |
261 | + DataStore, |
262 | + Nonce, |
263 | + ) |
264 | +from webcatalog.tests.factory import TestCaseWithFactory |
265 | + |
266 | + |
267 | +class DataStoreTestCase(TestCaseWithFactory): |
268 | + |
269 | + def test_lookup_nonce_creates_nonce(self): |
270 | + token, consumer = self.factory.make_oauth_token_and_consumer() |
271 | + |
272 | + result = DataStore().lookup_nonce( |
273 | + consumer.oauth_consumer(), token.oauth_token(), |
274 | + 'unique-nonce') |
275 | + |
276 | + self.assertFalse(result) |
277 | + self.assertEqual(1, Nonce.objects.filter( |
278 | + token=token, consumer=consumer, |
279 | + nonce='unique-nonce').count()) |
280 | + |
281 | + def test_lookup_nonce_exists(self): |
282 | + nonce = self.factory.make_nonce(nonce_string='unique') |
283 | + token = nonce.token |
284 | + consumer = nonce.consumer |
285 | + |
286 | + result = DataStore().lookup_nonce( |
287 | + consumer.oauth_consumer(), token.oauth_token(), |
288 | + 'unique') |
289 | + |
290 | + self.assertTrue(result) |
291 | + |
292 | + def test_lookup_nonce_uniquish(self): |
293 | + # If a nonce for the given consumer/token doesn't |
294 | + # exist, it's considered uniquish and valid. |
295 | + nonce = self.factory.make_nonce(nonce_string='uniquish-nonce') |
296 | + token, consumer = self.factory.make_oauth_token_and_consumer() |
297 | + |
298 | + result = DataStore().lookup_nonce( |
299 | + consumer.oauth_consumer(), token.oauth_token(), |
300 | + 'uniquish-nonce') |
301 | + |
302 | + self.assertFalse(result) |
303 | + self.assertEqual(1, Nonce.objects.filter( |
304 | + token=token, consumer=consumer, |
305 | + nonce='uniquish-nonce').count()) |
Good idea noodles!
I'd love to see this applied to rnr, sca and rec too at some point, though for some reason apps.u.c had many more of these oopses than the other services.