зеркало из https://github.com/mozilla/fxa.git
Merge pull request #13805 from mozilla/FXA-79-prune-oauth-authz-codez
feat(scripts): add script to prune oauth codes
This commit is contained in:
Коммит
ab49dcb9d5
|
@ -220,6 +220,12 @@ class OauthDB extends ConnectedServicesDb {
|
|||
getPocketIds() {
|
||||
return POCKET_IDS;
|
||||
}
|
||||
|
||||
async pruneAuthorizationCodes(ttlInMs) {
|
||||
return await this.mysql._pruneAuthorizationCodes(
|
||||
ttlInMs || config.get('oauthServer.expiration.code')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
|
|
@ -17,6 +17,7 @@ const { StatsD } = require('hot-shots');
|
|||
const REQUIRED_SQL_MODES = ['STRICT_ALL_TABLES', 'NO_ENGINE_SUBSTITUTION'];
|
||||
|
||||
const QUERY_GET_LOCK = 'SELECT GET_LOCK(?, ?) AS acquired';
|
||||
const QUERY_RELEASE_LOCK = 'SELECT RELEASE_LOCK(?)';
|
||||
const QUERY_CLIENT_REGISTER =
|
||||
'INSERT INTO clients ' +
|
||||
'(id, name, imageUri, hashedSecret, hashedSecretPrevious, redirectUri,' +
|
||||
|
@ -149,6 +150,8 @@ const DELETE_ACTIVE_REFRESH_TOKENS_BY_CLIENT_AND_UID =
|
|||
'DELETE FROM refreshTokens WHERE clientId=? AND userId=?';
|
||||
const DELETE_REFRESH_TOKEN_WITH_CLIENT_AND_UID =
|
||||
'DELETE FROM refreshTokens WHERE token=? AND clientId=? AND userId=?';
|
||||
const PRUNE_AUTHZ_CODES =
|
||||
'DELETE FROM codes WHERE TIMESTAMPDIFF(SECOND, createdAt, NOW()) > ? LIMIT 10000';
|
||||
|
||||
// Scope queries
|
||||
const QUERY_SCOPE_FIND = 'SELECT * ' + 'FROM scopes ' + 'WHERE scopes.scope=?;';
|
||||
|
@ -187,9 +190,16 @@ class MysqlStore extends MysqlOAuthShared {
|
|||
}
|
||||
}
|
||||
|
||||
getLock(lockName, timeout = 3) {
|
||||
// returns `acquired: 1` on success
|
||||
return this._readOne(QUERY_GET_LOCK, [lockName, timeout]);
|
||||
async _withLock(cb, lockName, timeout = 3) {
|
||||
const conn = await this._getConnection();
|
||||
|
||||
try {
|
||||
this._queryWithConnection(conn, QUERY_GET_LOCK, [lockName, timeout]);
|
||||
return await cb(conn);
|
||||
} finally {
|
||||
this._queryWithConnection(conn, QUERY_RELEASE_LOCK, [lockName]);
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
// createdAt is DEFAULT NOW() in the schema.sql
|
||||
|
@ -452,6 +462,23 @@ class MysqlStore extends MysqlOAuthShared {
|
|||
return Promise.all([deleteCodes, deleteTokens, deleteRefreshTokens]);
|
||||
}
|
||||
|
||||
async _pruneAuthorizationCodes(ttl) {
|
||||
const pruneAuthzCodes = async (conn) => {
|
||||
const ttlInSeconds = ttl / 1000;
|
||||
await this._queryWithConnection(conn, PRUNE_AUTHZ_CODES, [ttlInSeconds]);
|
||||
const prunedCount = await this._queryWithConnection(
|
||||
conn,
|
||||
'SELECT ROW_COUNT() AS pruned'
|
||||
);
|
||||
return firstRow(prunedCount);
|
||||
};
|
||||
|
||||
return await this._withLock(
|
||||
pruneAuthzCodes,
|
||||
'fxa-oauth.auth-codes.prune-lock'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific refresh token, for some clientId and uid.
|
||||
* Also deletes *all* access tokens for the clientId and uid combination,
|
||||
|
@ -662,19 +689,23 @@ class MysqlStore extends MysqlOAuthShared {
|
|||
async _query(sql, params) {
|
||||
const conn = await this._getConnection();
|
||||
try {
|
||||
return await new Promise(function (resolve, reject) {
|
||||
conn.query(sql, params || [], function (err, results) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
return await this._queryWithConnection(conn, sql, params);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async _queryWithConnection(conn, sql, params) {
|
||||
return await new Promise(function (resolve, reject) {
|
||||
conn.query(sql, params || [], function (err, results) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getProxyableFunctions() {
|
||||
return Reflect.ownKeys(MysqlStore.prototype).filter(
|
||||
(x) => x !== 'constructor' && /^[a-zA-Z]/.test(x)
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import program from 'commander';
|
||||
import { StatsD } from 'hot-shots';
|
||||
import { promisify } from 'util';
|
||||
const pckg = require('../package.json');
|
||||
const DEFAULT_TTL_MS = 900000;
|
||||
|
||||
export async function init() {
|
||||
// Setup utilities
|
||||
const config = require('../config').getProperties();
|
||||
const statsd = new StatsD({ ...config.statsd, maxBufferSize: 0 });
|
||||
const log = require('../lib/log')(
|
||||
config.log.level,
|
||||
'prune-oauth-codes',
|
||||
statsd
|
||||
);
|
||||
const oauthDb = require('../lib/oauth/db');
|
||||
|
||||
// Parse args
|
||||
program
|
||||
.version(pckg.version)
|
||||
.option(
|
||||
'--ttl <number>',
|
||||
'The TTL of OAuth authorization codes in milliseconds. Defaults to 15 minutes.',
|
||||
DEFAULT_TTL_MS
|
||||
)
|
||||
.on('--help', () =>
|
||||
console.log('\n\nPrunes up to 10000 expired OAuth authorization codes.')
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
const ttlInMs = parseInt(program.ttl) || DEFAULT_TTL_MS;
|
||||
|
||||
log.info('OAuth codes pruning', { ttl: ttlInMs });
|
||||
|
||||
try {
|
||||
log.info('OAuth codes pruning start', { ttl: ttlInMs });
|
||||
const result = await oauthDb.pruneAuthorizationCodes(ttlInMs);
|
||||
statsd.increment('oauth-codes.pruned', result.pruned);
|
||||
log.info('token pruning complete', result);
|
||||
} catch (err) {
|
||||
log.error('error during prune', err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
await promisify(statsd.close).bind(statsd)();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
let exitStatus = 1;
|
||||
init()
|
||||
.then((result) => {
|
||||
exitStatus = result;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
process.exit(exitStatus);
|
||||
});
|
||||
}
|
|
@ -12,11 +12,18 @@ const ScopeSet = require('fxa-shared').oauth.scopes;
|
|||
const encrypt = require('fxa-shared/auth/encrypt');
|
||||
const db = require('../../../lib/oauth/db');
|
||||
|
||||
const config = require('../../../config');
|
||||
|
||||
function randomString(len) {
|
||||
return crypto.randomBytes(Math.ceil(len)).toString('hex');
|
||||
}
|
||||
|
||||
describe('db', function () {
|
||||
before(async () => {
|
||||
// some other tests are not cleaning up their authorization codes
|
||||
await db.pruneAuthorizationCodes(1);
|
||||
});
|
||||
|
||||
describe('utf-8', function () {
|
||||
function makeTest(clientId, clientName) {
|
||||
return function () {
|
||||
|
@ -574,14 +581,45 @@ describe('db', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getLock', function () {
|
||||
it('should return an acquired status', function () {
|
||||
const lockName = randomString(10);
|
||||
return db.getLock(lockName, 3).then(function (result) {
|
||||
assert.ok(result);
|
||||
assert.ok('acquired' in result);
|
||||
assert.ok(result.acquired === 1);
|
||||
});
|
||||
describe('pruneAuthorizationCodes', () => {
|
||||
const clientId = buf(randomString(8));
|
||||
const userId = buf(randomString(16));
|
||||
const scope = ScopeSet.fromArray(['no_scope']).toString();
|
||||
const QUERY_CODE_INSERT =
|
||||
'INSERT INTO codes (code, clientId, userId, scope, createdAt) VALUES (?, ?, ?, ?, DATE_SUB(NOW(), INTERVAL ? SECOND ))';
|
||||
|
||||
const insertAuthzCode = async (ageInMs) => {
|
||||
await db.mysql._query(QUERY_CODE_INSERT, [
|
||||
randomString(16),
|
||||
clientId,
|
||||
userId,
|
||||
scope,
|
||||
ageInMs / 1000,
|
||||
]);
|
||||
};
|
||||
|
||||
it('prunes codes older than the given ttl', async () => {
|
||||
const ttl = 598989;
|
||||
const pruneCodesCount = 3;
|
||||
for (let i = 0; i < pruneCodesCount; i++) {
|
||||
await insertAuthzCode(ttl + 1000);
|
||||
}
|
||||
const validCodesCount = 2;
|
||||
for (let i = 0; i < validCodesCount; i++) {
|
||||
await insertAuthzCode(1);
|
||||
}
|
||||
const res = await db.pruneAuthorizationCodes(ttl);
|
||||
assert.equal(res.pruned, pruneCodesCount);
|
||||
});
|
||||
|
||||
it('prunes codes older than the default ttl', async () => {
|
||||
const ttl = config.get('oauthServer.expiration.code');
|
||||
const codesCount = 7;
|
||||
for (let i = 0; i < codesCount; i++) {
|
||||
await insertAuthzCode(ttl + 1000);
|
||||
}
|
||||
const res = await db.pruneAuthorizationCodes();
|
||||
assert.equal(res.pruned, codesCount);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
const ROOT_DIR = '../..';
|
||||
|
||||
const cp = require('child_process');
|
||||
const util = require('util');
|
||||
const path = require('path');
|
||||
|
||||
const execAsync = util.promisify(cp.exec);
|
||||
|
||||
const cwd = path.resolve(__dirname, ROOT_DIR);
|
||||
const execOptions = {
|
||||
cwd,
|
||||
};
|
||||
|
||||
describe('scripts/prune-oauth-authorization-codes:', () => {
|
||||
it('does not fail with no argument', () => {
|
||||
return execAsync(
|
||||
'node -r esbuild-register scripts/prune-oauth-authorization-codes',
|
||||
execOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('does not fail with an argument', () => {
|
||||
return execAsync(
|
||||
'node -r esbuild-register scripts/prune-oauth-authorization-codes --ttl 600000',
|
||||
execOptions
|
||||
);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче