Merge lp:~michael.nelson/ubuntu-webcatalog/1040523-oops-creating-nonce into lp:ubuntu-webcatalog

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
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.
Revision history for this message
Anthony Lenton (elachuni) wrote :

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.

review: Approve

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())

Subscribers

People subscribed via source and target branches