Merge lp:~stub/launchpad/oauth-db into lp:launchpad
- oauth-db
- Merge into devel
Status: | Rejected |
---|---|
Rejected by: | Stuart Bishop |
Proposed branch: | lp:~stub/launchpad/oauth-db |
Merge into: | lp:launchpad |
Diff against target: |
436 lines (+55/-169) 7 files modified
lib/canonical/launchpad/database/oauth.py (+44/-36) lib/canonical/launchpad/database/tests/test_oauth.py (+2/-8) lib/canonical/launchpad/doc/oauth.txt (+8/-26) lib/canonical/launchpad/interfaces/oauth.py (+1/-16) lib/canonical/launchpad/scripts/garbo.py (+0/-38) lib/canonical/launchpad/scripts/tests/test_garbo.py (+0/-41) lib/canonical/launchpad/zcml/oauth.zcml (+0/-4) |
To merge this branch: | bzr merge lp:~stub/launchpad/oauth-db |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical Launchpad Engineering | Pending | ||
Review via email: mp+27025@code.launchpad.net |
Commit message
Description of the change
The OAuthNonce table is our most heavily updated table by an order of magnitude and responsible for the majority of our replication load. It generally sits at 40 updates per second and spikes at over two hundred for short periods.
We maintain the nonce seen history per the OAuth spec to help prevent replay attacks. There is no need for this information to be stored in a relational database at all - it is overkill. Memcache seems a better storage mechanism.
If memory pressure causes memcached to prematurely evict nonce history, users will not notice - what we lose is the replay protection. Monitoring is being put in place so we will see when evictions start happening and we can add RAM to our memcached instances or investigate alternative storages.
- 5590. By Stuart Bishop
-
Better expiry to avoid memcached pressure
- 5591. By Stuart Bishop
-
Betterer expiry
- 5592. By Stuart Bishop
-
Shorten keys
Unmerged revisions
- 5592. By Stuart Bishop
-
Shorten keys
- 5591. By Stuart Bishop
-
Betterer expiry
- 5590. By Stuart Bishop
-
Better expiry to avoid memcached pressure
- 5589. By Stuart Bishop
-
Drop OAuthNonce DB class, replacing its use with memcached
- 5588. By Stuart Bishop
-
Log OAuthNonces in memcached rather than PostgreSQL
- 5587. By Stuart Bishop
-
Merge from lp:~launchpad-pqm/launchpad/devel
Preview Diff
1 | === modified file 'lib/canonical/launchpad/database/oauth.py' | |||
2 | --- lib/canonical/launchpad/database/oauth.py 2010-04-19 09:39:29 +0000 | |||
3 | +++ lib/canonical/launchpad/database/oauth.py 2010-06-08 12:11:25 +0000 | |||
4 | @@ -6,16 +6,16 @@ | |||
5 | 6 | 'OAuthAccessToken', | 6 | 'OAuthAccessToken', |
6 | 7 | 'OAuthConsumer', | 7 | 'OAuthConsumer', |
7 | 8 | 'OAuthConsumerSet', | 8 | 'OAuthConsumerSet', |
8 | 9 | 'OAuthNonce', | ||
9 | 10 | 'OAuthRequestToken', | 9 | 'OAuthRequestToken', |
10 | 11 | 'OAuthRequestTokenSet'] | 10 | 'OAuthRequestTokenSet'] |
11 | 12 | 11 | ||
13 | 13 | import pytz | 12 | import time |
14 | 14 | from datetime import datetime, timedelta | 13 | from datetime import datetime, timedelta |
15 | 15 | 14 | ||
16 | 16 | from zope.component import getUtility | 15 | from zope.component import getUtility |
17 | 17 | from zope.interface import implements | 16 | from zope.interface import implements |
18 | 18 | 17 | ||
19 | 18 | from pytz import UTC | ||
20 | 19 | from sqlobject import BoolCol, ForeignKey, StringCol | 19 | from sqlobject import BoolCol, ForeignKey, StringCol |
21 | 20 | from storm.expr import And | 20 | from storm.expr import And |
22 | 21 | 21 | ||
23 | @@ -32,8 +32,9 @@ | |||
24 | 32 | from lp.registry.interfaces.projectgroup import IProjectGroup | 32 | from lp.registry.interfaces.projectgroup import IProjectGroup |
25 | 33 | from lp.registry.interfaces.distributionsourcepackage import ( | 33 | from lp.registry.interfaces.distributionsourcepackage import ( |
26 | 34 | IDistributionSourcePackage) | 34 | IDistributionSourcePackage) |
27 | 35 | from lp.services.memcache.interfaces import IMemcacheClient | ||
28 | 35 | from canonical.launchpad.interfaces import ( | 36 | from canonical.launchpad.interfaces import ( |
30 | 36 | IOAuthAccessToken, IOAuthConsumer, IOAuthConsumerSet, IOAuthNonce, | 37 | IOAuthAccessToken, IOAuthConsumer, IOAuthConsumerSet, |
31 | 37 | IOAuthRequestToken, IOAuthRequestTokenSet, NonceAlreadyUsed, | 38 | IOAuthRequestToken, IOAuthRequestTokenSet, NonceAlreadyUsed, |
32 | 38 | TimestampOrderingError, ClockSkew) | 39 | TimestampOrderingError, ClockSkew) |
33 | 39 | from canonical.launchpad.webapp.interfaces import ( | 40 | from canonical.launchpad.webapp.interfaces import ( |
34 | @@ -46,7 +47,7 @@ | |||
35 | 46 | # timestamp "MUST be equal or greater than the timestamp used in previous | 47 | # timestamp "MUST be equal or greater than the timestamp used in previous |
36 | 47 | # requests," but this is likely to cause problems if the client does request | 48 | # requests," but this is likely to cause problems if the client does request |
37 | 48 | # pipelining, so we use a time window (relative to the timestamp of the | 49 | # pipelining, so we use a time window (relative to the timestamp of the |
39 | 49 | # existing OAuthNonce) to check if the timestamp can is acceptable. As | 50 | # existing nonce) to check if the timestamp can is acceptable. As |
40 | 50 | # suggested by Robert, we use a window which is at least twice the size of our | 51 | # suggested by Robert, we use a window which is at least twice the size of our |
41 | 51 | # hard time out. This is a safe bet since no requests should take more than | 52 | # hard time out. This is a safe bet since no requests should take more than |
42 | 52 | # one hard time out. | 53 | # one hard time out. |
43 | @@ -87,7 +88,7 @@ | |||
44 | 87 | def newRequestToken(self): | 88 | def newRequestToken(self): |
45 | 88 | """See `IOAuthConsumer`.""" | 89 | """See `IOAuthConsumer`.""" |
46 | 89 | key, secret = create_token_key_and_secret(table=OAuthRequestToken) | 90 | key, secret = create_token_key_and_secret(table=OAuthRequestToken) |
48 | 90 | date_expires = (datetime.now(pytz.timezone('UTC')) | 91 | date_expires = (datetime.now(UTC) |
49 | 91 | + timedelta(hours=REQUEST_TOKEN_VALIDITY)) | 92 | + timedelta(hours=REQUEST_TOKEN_VALIDITY)) |
50 | 92 | return OAuthRequestToken( | 93 | return OAuthRequestToken( |
51 | 93 | consumer=self, key=key, secret=secret, date_expires=date_expires) | 94 | consumer=self, key=key, secret=secret, date_expires=date_expires) |
52 | @@ -161,35 +162,52 @@ | |||
53 | 161 | 162 | ||
54 | 162 | def checkNonceAndTimestamp(self, nonce, timestamp): | 163 | def checkNonceAndTimestamp(self, nonce, timestamp): |
55 | 163 | """See `IOAuthAccessToken`.""" | 164 | """See `IOAuthAccessToken`.""" |
58 | 164 | timestamp = float(timestamp) | 165 | date = float(timestamp) |
57 | 165 | date = datetime.fromtimestamp(timestamp, pytz.UTC) | ||
59 | 166 | # Determine if the timestamp is too far off from now. | 166 | # Determine if the timestamp is too far off from now. |
62 | 167 | skew = timedelta(seconds=TIMESTAMP_SKEW_WINDOW) | 167 | skew = TIMESTAMP_SKEW_WINDOW |
63 | 168 | now = datetime.now(pytz.UTC) | 168 | now = time.time() |
64 | 169 | if date < (now-skew) or date > (now+skew): | 169 | if date < (now-skew) or date > (now+skew): |
65 | 170 | raise ClockSkew('Timestamp appears to come from bad system clock') | 170 | raise ClockSkew('Timestamp appears to come from bad system clock') |
66 | 171 | |||
67 | 172 | # Have we seen this nonce before with this token? | ||
68 | 173 | # Pull the nonce details, if they exists, from memcached. | ||
69 | 174 | cache = getUtility(IMemcacheClient) | ||
70 | 175 | nonce_seen_key = 'oauth:t_%s:n_%s' % ( | ||
71 | 176 | self.key.encode('US-ASCII'), nonce.encode('US-ASCII')) | ||
72 | 177 | nonce_seen = cache.get(nonce_seen_key) | ||
73 | 178 | |||
74 | 171 | # Determine if the nonce was already used for this timestamp. | 179 | # Determine if the nonce was already used for this timestamp. |
82 | 172 | store = OAuthNonce.getStore() | 180 | if nonce_seen == date: |
76 | 173 | oauth_nonce = store.find(OAuthNonce, | ||
77 | 174 | And(OAuthNonce.access_token==self, | ||
78 | 175 | OAuthNonce.nonce==nonce, | ||
79 | 176 | OAuthNonce.request_timestamp==date) | ||
80 | 177 | ).one() | ||
81 | 178 | if oauth_nonce is not None: | ||
83 | 179 | raise NonceAlreadyUsed('This nonce has been used already.') | 181 | raise NonceAlreadyUsed('This nonce has been used already.') |
84 | 182 | |||
85 | 180 | # Determine if the timestamp is too old compared to most recent | 183 | # Determine if the timestamp is too old compared to most recent |
86 | 181 | # request. | 184 | # request. |
93 | 182 | limit = date + timedelta(seconds=TIMESTAMP_ACCEPTANCE_WINDOW) | 185 | token_seen_key = 'oauth:t_%s:seen' % self.key.encode('US-ASCII') |
94 | 183 | match = store.find(OAuthNonce, | 186 | token_seen = cache.get(token_seen_key) |
95 | 184 | And(OAuthNonce.access_token==self, | 187 | |
96 | 185 | OAuthNonce.request_timestamp>limit) | 188 | limit = date + TIMESTAMP_ACCEPTANCE_WINDOW |
97 | 186 | ).any() | 189 | if token_seen > limit: |
92 | 187 | if match is not None: | ||
98 | 188 | raise TimestampOrderingError( | 190 | raise TimestampOrderingError( |
99 | 189 | 'Timestamp too old compared to most recent request') | 191 | 'Timestamp too old compared to most recent request') |
103 | 190 | # Looks OK. Give a Nonce object back. | 192 | |
104 | 191 | return OAuthNonce( | 193 | # Looks OK. |
105 | 192 | access_token=self, nonce=nonce, request_timestamp=date) | 194 | |
106 | 195 | # No need to remember things beyond | ||
107 | 196 | # TIMESTAMP_ACCEPTANCE_WINDOW * 2 seconds | ||
108 | 197 | # Where n==TIMESTAMP_ACCEPTANCE_WINDOW, n * 2 is the worst case. | ||
109 | 198 | # We will accept a timestamp claiming to be from n seconds in | ||
110 | 199 | # the future. If, in n*2 seconds time we receive the same | ||
111 | 200 | # timestamp, it would still be withing the acceptable skew | ||
112 | 201 | # and we need to generate an 'already seen' exception. | ||
113 | 202 | expires = TIMESTAMP_SKEW_WINDOW * 2 + TIMESTAMP_ACCEPTANCE_WINDOW | ||
114 | 203 | |||
115 | 204 | # Remember when we saw this nonce to avoid replay attacks. | ||
116 | 205 | cache.set(nonce_seen_key, date, expires) | ||
117 | 206 | |||
118 | 207 | # Remember when we last checked a nonce for this access | ||
119 | 208 | # token so we don't accept nonces in the past but still within | ||
120 | 209 | # acceptable skew. | ||
121 | 210 | cache.set(token_seen_key, max(date, token_seen), expires) | ||
122 | 193 | 211 | ||
123 | 194 | 212 | ||
124 | 195 | class OAuthRequestToken(OAuthBase): | 213 | class OAuthRequestToken(OAuthBase): |
125 | @@ -240,7 +258,7 @@ | |||
126 | 240 | """See `IOAuthRequestToken`.""" | 258 | """See `IOAuthRequestToken`.""" |
127 | 241 | assert not self.is_reviewed, ( | 259 | assert not self.is_reviewed, ( |
128 | 242 | "Request tokens can be reviewed only once.") | 260 | "Request tokens can be reviewed only once.") |
130 | 243 | self.date_reviewed = datetime.now(pytz.timezone('UTC')) | 261 | self.date_reviewed = datetime.now(UTC) |
131 | 244 | self.person = user | 262 | self.person = user |
132 | 245 | self.permission = permission | 263 | self.permission = permission |
133 | 246 | if IProduct.providedBy(context): | 264 | if IProduct.providedBy(context): |
134 | @@ -286,16 +304,6 @@ | |||
135 | 286 | return OAuthRequestToken.selectOneBy(key=key) | 304 | return OAuthRequestToken.selectOneBy(key=key) |
136 | 287 | 305 | ||
137 | 288 | 306 | ||
138 | 289 | class OAuthNonce(OAuthBase): | ||
139 | 290 | """See `IOAuthNonce`.""" | ||
140 | 291 | implements(IOAuthNonce) | ||
141 | 292 | |||
142 | 293 | access_token = ForeignKey( | ||
143 | 294 | dbName='access_token', foreignKey='OAuthAccessToken', notNull=True) | ||
144 | 295 | request_timestamp = UtcDateTimeCol(default=UTC_NOW, notNull=True) | ||
145 | 296 | nonce = StringCol(notNull=True) | ||
146 | 297 | |||
147 | 298 | |||
148 | 299 | def create_token_key_and_secret(table): | 307 | def create_token_key_and_secret(table): |
149 | 300 | """Create a key and secret for an OAuth token. | 308 | """Create a key and secret for an OAuth token. |
150 | 301 | 309 | ||
151 | 302 | 310 | ||
152 | === modified file 'lib/canonical/launchpad/database/tests/test_oauth.py' | |||
153 | --- lib/canonical/launchpad/database/tests/test_oauth.py 2010-03-24 17:37:26 +0000 | |||
154 | +++ lib/canonical/launchpad/database/tests/test_oauth.py 2010-06-08 12:11:25 +0000 | |||
155 | @@ -13,7 +13,7 @@ | |||
156 | 13 | from zope.component import getUtility | 13 | from zope.component import getUtility |
157 | 14 | 14 | ||
158 | 15 | from canonical.launchpad.database.oauth import ( | 15 | from canonical.launchpad.database.oauth import ( |
160 | 16 | OAuthAccessToken, OAuthConsumer, OAuthNonce, OAuthRequestToken) | 16 | OAuthAccessToken, OAuthConsumer, OAuthRequestToken) |
161 | 17 | from canonical.testing.layers import DatabaseFunctionalLayer | 17 | from canonical.testing.layers import DatabaseFunctionalLayer |
162 | 18 | from canonical.launchpad.webapp.interfaces import MAIN_STORE, MASTER_FLAVOR | 18 | from canonical.launchpad.webapp.interfaces import MAIN_STORE, MASTER_FLAVOR |
163 | 19 | 19 | ||
164 | @@ -45,14 +45,8 @@ | |||
165 | 45 | class_ = OAuthConsumer | 45 | class_ = OAuthConsumer |
166 | 46 | 46 | ||
167 | 47 | 47 | ||
168 | 48 | class OAuthNonceTestCase(BaseOAuthTestCase): | ||
169 | 49 | class_ = OAuthNonce | ||
170 | 50 | |||
171 | 51 | |||
172 | 52 | def test_suite(): | 48 | def test_suite(): |
173 | 53 | return unittest.TestSuite(( | 49 | return unittest.TestSuite(( |
174 | 54 | unittest.makeSuite(OAuthAccessTokenTestCase), | 50 | unittest.makeSuite(OAuthAccessTokenTestCase), |
175 | 55 | unittest.makeSuite(OAuthRequestTokenTestCase), | 51 | unittest.makeSuite(OAuthRequestTokenTestCase), |
179 | 56 | unittest.makeSuite(OAuthNonceTestCase), | 52 | unittest.makeSuite(OAuthConsumerTestCase))) |
177 | 57 | unittest.makeSuite(OAuthConsumerTestCase), | ||
178 | 58 | )) | ||
180 | 59 | 53 | ||
181 | === modified file 'lib/canonical/launchpad/doc/oauth.txt' | |||
182 | --- lib/canonical/launchpad/doc/oauth.txt 2010-04-16 15:06:55 +0000 | |||
183 | +++ lib/canonical/launchpad/doc/oauth.txt 2010-06-08 12:11:25 +0000 | |||
184 | @@ -15,7 +15,7 @@ | |||
185 | 15 | ... AccessLevel, OAuthPermission) | 15 | ... AccessLevel, OAuthPermission) |
186 | 16 | >>> from canonical.launchpad.interfaces import ( | 16 | >>> from canonical.launchpad.interfaces import ( |
187 | 17 | ... IOAuthAccessToken, IOAuthConsumer, IOAuthConsumerSet, | 17 | ... IOAuthAccessToken, IOAuthConsumer, IOAuthConsumerSet, |
189 | 18 | ... IOAuthNonce, IOAuthRequestToken, IPersonSet) | 18 | ... IOAuthRequestToken, IPersonSet) |
190 | 19 | >>> consumer_set = getUtility(IOAuthConsumerSet) | 19 | >>> consumer_set = getUtility(IOAuthConsumerSet) |
191 | 20 | >>> verifyObject(IOAuthConsumerSet, consumer_set) | 20 | >>> verifyObject(IOAuthConsumerSet, consumer_set) |
192 | 21 | True | 21 | True |
193 | @@ -307,26 +307,16 @@ | |||
194 | 307 | 307 | ||
195 | 308 | >>> import time | 308 | >>> import time |
196 | 309 | >>> now = time.time() - 1 | 309 | >>> now = time.time() - 1 |
200 | 310 | >>> nonce1 = access_token.checkNonceAndTimestamp('boo', now) | 310 | >>> access_token.checkNonceAndTimestamp('boo', now) |
198 | 311 | >>> verifyObject(IOAuthNonce, nonce1) | ||
199 | 312 | True | ||
201 | 313 | 311 | ||
202 | 314 | - We can use an existing nonce with a new time. | 312 | - We can use an existing nonce with a new time. |
203 | 315 | 313 | ||
204 | 316 | >>> now += 1 | 314 | >>> now += 1 |
210 | 317 | >>> nonce2 = access_token.checkNonceAndTimestamp('boo', now) | 315 | >>> access_token.checkNonceAndTimestamp('boo', now) |
206 | 318 | >>> IOAuthNonce.providedBy(nonce2) | ||
207 | 319 | True | ||
208 | 320 | >>> nonce1 is nonce2 | ||
209 | 321 | False | ||
211 | 322 | 316 | ||
212 | 323 | - We can use a new nonce with the same time. | 317 | - We can use a new nonce with the same time. |
213 | 324 | 318 | ||
219 | 325 | >>> nonce3 = access_token.checkNonceAndTimestamp('surprise!', now) | 319 | >>> access_token.checkNonceAndTimestamp('surprise!', now) |
215 | 326 | >>> IOAuthNonce.providedBy(nonce3) | ||
216 | 327 | True | ||
217 | 328 | >>> nonce1 is nonce3 or nonce2 is nonce3 | ||
218 | 329 | False | ||
220 | 330 | 320 | ||
221 | 331 | - But we cannot use an existing nonce used for the same time. | 321 | - But we cannot use an existing nonce used for the same time. |
222 | 332 | 322 | ||
223 | @@ -368,12 +358,8 @@ | |||
224 | 368 | ... TIMESTAMP_ACCEPTANCE_WINDOW) | 358 | ... TIMESTAMP_ACCEPTANCE_WINDOW) |
225 | 369 | >>> TIMESTAMP_ACCEPTANCE_WINDOW | 359 | >>> TIMESTAMP_ACCEPTANCE_WINDOW |
226 | 370 | 60 | 360 | 60 |
233 | 371 | >>> nonce4 = access_token.checkNonceAndTimestamp('boo', now-30) | 361 | >>> access_token.checkNonceAndTimestamp('boo', now-30) |
234 | 372 | >>> IOAuthNonce.providedBy(nonce4) | 362 | >>> access_token.checkNonceAndTimestamp('boo', now-60) |
229 | 373 | True | ||
230 | 374 | >>> nonce5 = access_token.checkNonceAndTimestamp('boo', now-60) | ||
231 | 375 | >>> IOAuthNonce.providedBy(nonce5) | ||
232 | 376 | True | ||
235 | 377 | 363 | ||
236 | 378 | - Once outside of the window (defined by the *latest* timestamp, even if it | 364 | - Once outside of the window (defined by the *latest* timestamp, even if it |
237 | 379 | is not the most recent), we get a TimestampOrderingError. | 365 | is not the most recent), we get a TimestampOrderingError. |
238 | @@ -405,9 +391,7 @@ | |||
239 | 405 | ... TIMESTAMP_SKEW_WINDOW) | 391 | ... TIMESTAMP_SKEW_WINDOW) |
240 | 406 | >>> TIMESTAMP_SKEW_WINDOW | 392 | >>> TIMESTAMP_SKEW_WINDOW |
241 | 407 | 3600 | 393 | 3600 |
245 | 408 | >>> nonce6 = access_token.checkNonceAndTimestamp('boo', now + 55*60) | 394 | >>> access_token.checkNonceAndTimestamp('boo', now + 55*60) |
243 | 409 | >>> IOAuthNonce.providedBy(nonce6) | ||
244 | 410 | True | ||
246 | 411 | 395 | ||
247 | 412 | - We cannot access it 65 minutes in the future. | 396 | - We cannot access it 65 minutes in the future. |
248 | 413 | 397 | ||
249 | @@ -419,9 +403,7 @@ | |||
250 | 419 | - It's also worth noting that now the TIMESTAMP_ACCEPTANCE_WINDOW is based | 403 | - It's also worth noting that now the TIMESTAMP_ACCEPTANCE_WINDOW is based |
251 | 420 | off of the time 55 minutes in the future. | 404 | off of the time 55 minutes in the future. |
252 | 421 | 405 | ||
256 | 422 | >>> nonce7 = access_token.checkNonceAndTimestamp('boo', now + 54*60 + 30) | 406 | >>> access_token.checkNonceAndTimestamp('boo', now + 54*60 + 30) |
254 | 423 | >>> IOAuthNonce.providedBy(nonce7) | ||
255 | 424 | True | ||
257 | 425 | >>> access_token.checkNonceAndTimestamp('boo', now + 60) | 407 | >>> access_token.checkNonceAndTimestamp('boo', now + 60) |
258 | 426 | Traceback (most recent call last): | 408 | Traceback (most recent call last): |
259 | 427 | ... | 409 | ... |
260 | 428 | 410 | ||
261 | === modified file 'lib/canonical/launchpad/interfaces/oauth.py' | |||
262 | --- lib/canonical/launchpad/interfaces/oauth.py 2010-04-19 14:47:49 +0000 | |||
263 | +++ lib/canonical/launchpad/interfaces/oauth.py 2010-06-08 12:11:25 +0000 | |||
264 | @@ -13,7 +13,6 @@ | |||
265 | 13 | 'IOAuthAccessToken', | 13 | 'IOAuthAccessToken', |
266 | 14 | 'IOAuthConsumer', | 14 | 'IOAuthConsumer', |
267 | 15 | 'IOAuthConsumerSet', | 15 | 'IOAuthConsumerSet', |
268 | 16 | 'IOAuthNonce', | ||
269 | 17 | 'IOAuthRequestToken', | 16 | 'IOAuthRequestToken', |
270 | 18 | 'IOAuthRequestTokenSet', | 17 | 'IOAuthRequestTokenSet', |
271 | 19 | 'IOAuthSignedRequest', | 18 | 'IOAuthSignedRequest', |
272 | @@ -174,7 +173,7 @@ | |||
273 | 174 | +/- `TIMESTAMP_SKEW_WINDOW` of now. | 173 | +/- `TIMESTAMP_SKEW_WINDOW` of now. |
274 | 175 | 174 | ||
275 | 176 | If the nonce has never been used together with this token and | 175 | If the nonce has never been used together with this token and |
277 | 177 | timestamp before, we store it in the database with the given timestamp | 176 | timestamp before, we store it with the given timestamp |
278 | 178 | and associated with this token. | 177 | and associated with this token. |
279 | 179 | """ | 178 | """ |
280 | 180 | 179 | ||
281 | @@ -236,20 +235,6 @@ | |||
282 | 236 | """ | 235 | """ |
283 | 237 | 236 | ||
284 | 238 | 237 | ||
285 | 239 | class IOAuthNonce(Interface): | ||
286 | 240 | """The unique (nonce,timestamp) for requests using a given access token. | ||
287 | 241 | |||
288 | 242 | The nonce value (which is unique for all requests with that timestamp) | ||
289 | 243 | is generated by the consumer and included, together with the timestamp, | ||
290 | 244 | in each request made. It's used to prevent replay attacks. | ||
291 | 245 | """ | ||
292 | 246 | |||
293 | 247 | request_timestamp = Datetime( | ||
294 | 248 | title=_('Date issued'), required=True, readonly=True) | ||
295 | 249 | access_token = Object(schema=IOAuthAccessToken, title=_('The token')) | ||
296 | 250 | nonce = TextLine(title=_('Nonce'), required=True, readonly=True) | ||
297 | 251 | |||
298 | 252 | |||
299 | 253 | class IOAuthSignedRequest(Interface): | 238 | class IOAuthSignedRequest(Interface): |
300 | 254 | """Marker interface for a request signed with OAuth credentials.""" | 239 | """Marker interface for a request signed with OAuth credentials.""" |
301 | 255 | 240 | ||
302 | 256 | 241 | ||
303 | === modified file 'lib/canonical/launchpad/scripts/garbo.py' | |||
304 | --- lib/canonical/launchpad/scripts/garbo.py 2010-05-27 16:49:09 +0000 | |||
305 | +++ lib/canonical/launchpad/scripts/garbo.py 2010-06-08 12:11:25 +0000 | |||
306 | @@ -23,7 +23,6 @@ | |||
307 | 23 | from canonical.launchpad.database.emailaddress import EmailAddress | 23 | from canonical.launchpad.database.emailaddress import EmailAddress |
308 | 24 | from lp.hardwaredb.model.hwdb import HWSubmission | 24 | from lp.hardwaredb.model.hwdb import HWSubmission |
309 | 25 | from canonical.launchpad.database.librarian import LibraryFileAlias | 25 | from canonical.launchpad.database.librarian import LibraryFileAlias |
310 | 26 | from canonical.launchpad.database.oauth import OAuthNonce | ||
311 | 27 | from canonical.launchpad.database.openidconsumer import OpenIDConsumerNonce | 26 | from canonical.launchpad.database.openidconsumer import OpenIDConsumerNonce |
312 | 28 | from canonical.launchpad.interfaces import IMasterStore | 27 | from canonical.launchpad.interfaces import IMasterStore |
313 | 29 | from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus | 28 | from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus |
314 | @@ -51,42 +50,6 @@ | |||
315 | 51 | ONE_DAY_IN_SECONDS = 24*60*60 | 50 | ONE_DAY_IN_SECONDS = 24*60*60 |
316 | 52 | 51 | ||
317 | 53 | 52 | ||
318 | 54 | class OAuthNoncePruner(TunableLoop): | ||
319 | 55 | """An ITunableLoop to prune old OAuthNonce records. | ||
320 | 56 | |||
321 | 57 | We remove all OAuthNonce records older than 1 day. | ||
322 | 58 | """ | ||
323 | 59 | maximum_chunk_size = 6*60*60 # 6 hours in seconds. | ||
324 | 60 | |||
325 | 61 | def __init__(self, log, abort_time=None): | ||
326 | 62 | super(OAuthNoncePruner, self).__init__(log, abort_time) | ||
327 | 63 | self.store = IMasterStore(OAuthNonce) | ||
328 | 64 | self.oldest_age = self.store.execute(""" | ||
329 | 65 | SELECT COALESCE(EXTRACT(EPOCH FROM | ||
330 | 66 | CURRENT_TIMESTAMP AT TIME ZONE 'UTC' | ||
331 | 67 | - MIN(request_timestamp)), 0) | ||
332 | 68 | FROM OAuthNonce | ||
333 | 69 | """).get_one()[0] | ||
334 | 70 | |||
335 | 71 | def isDone(self): | ||
336 | 72 | return self.oldest_age <= ONE_DAY_IN_SECONDS | ||
337 | 73 | |||
338 | 74 | def __call__(self, chunk_size): | ||
339 | 75 | self.oldest_age = max( | ||
340 | 76 | ONE_DAY_IN_SECONDS, self.oldest_age - chunk_size) | ||
341 | 77 | |||
342 | 78 | self.log.debug( | ||
343 | 79 | "Removed OAuthNonce rows older than %d seconds" | ||
344 | 80 | % self.oldest_age) | ||
345 | 81 | |||
346 | 82 | self.store.find( | ||
347 | 83 | OAuthNonce, | ||
348 | 84 | OAuthNonce.request_timestamp < SQL( | ||
349 | 85 | "CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - interval '%d seconds'" | ||
350 | 86 | % self.oldest_age)).remove() | ||
351 | 87 | transaction.commit() | ||
352 | 88 | |||
353 | 89 | |||
354 | 90 | class OpenIDConsumerNoncePruner(TunableLoop): | 53 | class OpenIDConsumerNoncePruner(TunableLoop): |
355 | 91 | """An ITunableLoop to prune old OpenIDConsumerNonce records. | 54 | """An ITunableLoop to prune old OpenIDConsumerNonce records. |
356 | 92 | 55 | ||
357 | @@ -739,7 +702,6 @@ | |||
358 | 739 | class HourlyDatabaseGarbageCollector(BaseDatabaseGarbageCollector): | 702 | class HourlyDatabaseGarbageCollector(BaseDatabaseGarbageCollector): |
359 | 740 | script_name = 'garbo-hourly' | 703 | script_name = 'garbo-hourly' |
360 | 741 | tunable_loops = [ | 704 | tunable_loops = [ |
361 | 742 | OAuthNoncePruner, | ||
362 | 743 | OpenIDConsumerNoncePruner, | 705 | OpenIDConsumerNoncePruner, |
363 | 744 | OpenIDConsumerAssociationPruner, | 706 | OpenIDConsumerAssociationPruner, |
364 | 745 | RevisionCachePruner, | 707 | RevisionCachePruner, |
365 | 746 | 708 | ||
366 | === modified file 'lib/canonical/launchpad/scripts/tests/test_garbo.py' | |||
367 | --- lib/canonical/launchpad/scripts/tests/test_garbo.py 2010-05-26 01:48:12 +0000 | |||
368 | +++ lib/canonical/launchpad/scripts/tests/test_garbo.py 2010-06-08 12:11:25 +0000 | |||
369 | @@ -20,7 +20,6 @@ | |||
370 | 20 | from canonical.config import config | 20 | from canonical.config import config |
371 | 21 | from canonical.database.constants import THIRTY_DAYS_AGO, UTC_NOW | 21 | from canonical.database.constants import THIRTY_DAYS_AGO, UTC_NOW |
372 | 22 | from canonical.launchpad.database.message import Message | 22 | from canonical.launchpad.database.message import Message |
373 | 23 | from canonical.launchpad.database.oauth import OAuthNonce | ||
374 | 24 | from canonical.launchpad.database.openidconsumer import OpenIDConsumerNonce | 23 | from canonical.launchpad.database.openidconsumer import OpenIDConsumerNonce |
375 | 25 | from canonical.launchpad.interfaces import IMasterStore | 24 | from canonical.launchpad.interfaces import IMasterStore |
376 | 26 | from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus | 25 | from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus |
377 | @@ -90,46 +89,6 @@ | |||
378 | 90 | collector.main() | 89 | collector.main() |
379 | 91 | return collector | 90 | return collector |
380 | 92 | 91 | ||
381 | 93 | def test_OAuthNoncePruner(self): | ||
382 | 94 | now = datetime.utcnow().replace(tzinfo=UTC) | ||
383 | 95 | timestamps = [ | ||
384 | 96 | now - timedelta(days=2), # Garbage | ||
385 | 97 | now - timedelta(days=1) - timedelta(seconds=60), # Garbage | ||
386 | 98 | now - timedelta(days=1) + timedelta(seconds=60), # Not garbage | ||
387 | 99 | now, # Not garbage | ||
388 | 100 | ] | ||
389 | 101 | LaunchpadZopelessLayer.switchDbUser('testadmin') | ||
390 | 102 | store = IMasterStore(OAuthNonce) | ||
391 | 103 | |||
392 | 104 | # Make sure we start with 0 nonces. | ||
393 | 105 | self.failUnlessEqual(store.find(OAuthNonce).count(), 0) | ||
394 | 106 | |||
395 | 107 | for timestamp in timestamps: | ||
396 | 108 | OAuthNonce( | ||
397 | 109 | access_tokenID=1, | ||
398 | 110 | request_timestamp = timestamp, | ||
399 | 111 | nonce = str(timestamp)) | ||
400 | 112 | transaction.commit() | ||
401 | 113 | |||
402 | 114 | # Make sure we have 4 nonces now. | ||
403 | 115 | self.failUnlessEqual(store.find(OAuthNonce).count(), 4) | ||
404 | 116 | |||
405 | 117 | self.runHourly(maximum_chunk_size=60) # 1 minute maximum chunk size | ||
406 | 118 | |||
407 | 119 | store = IMasterStore(OAuthNonce) | ||
408 | 120 | |||
409 | 121 | # Now back to two, having removed the two garbage entries. | ||
410 | 122 | self.failUnlessEqual(store.find(OAuthNonce).count(), 2) | ||
411 | 123 | |||
412 | 124 | # And none of them are older than a day. | ||
413 | 125 | # Hmm... why is it I'm putting tz aware datetimes in and getting | ||
414 | 126 | # naive datetimes back? Bug in the SQLObject compatibility layer? | ||
415 | 127 | # Test is still fine as we know the timezone. | ||
416 | 128 | self.failUnless( | ||
417 | 129 | store.find( | ||
418 | 130 | Min(OAuthNonce.request_timestamp)).one().replace(tzinfo=UTC) | ||
419 | 131 | >= now - timedelta(days=1)) | ||
420 | 132 | |||
421 | 133 | def test_OpenIDConsumerNoncePruner(self): | 92 | def test_OpenIDConsumerNoncePruner(self): |
422 | 134 | now = int(time.mktime(time.gmtime())) | 93 | now = int(time.mktime(time.gmtime())) |
423 | 135 | MINUTES = 60 | 94 | MINUTES = 60 |
424 | 136 | 95 | ||
425 | === modified file 'lib/canonical/launchpad/zcml/oauth.zcml' | |||
426 | --- lib/canonical/launchpad/zcml/oauth.zcml 2009-07-13 18:15:02 +0000 | |||
427 | +++ lib/canonical/launchpad/zcml/oauth.zcml 2010-06-08 12:11:25 +0000 | |||
428 | @@ -44,8 +44,4 @@ | |||
429 | 44 | permission="launchpad.Edit" | 44 | permission="launchpad.Edit" |
430 | 45 | set_schema="canonical.launchpad.interfaces.IOAuthAccessToken"/> | 45 | set_schema="canonical.launchpad.interfaces.IOAuthAccessToken"/> |
431 | 46 | </class> | 46 | </class> |
432 | 47 | |||
433 | 48 | <class class="canonical.launchpad.database.OAuthNonce"> | ||
434 | 49 | <allow interface="canonical.launchpad.interfaces.IOAuthNonce"/> | ||
435 | 50 | </class> | ||
436 | 51 | </configure> | 47 | </configure> |