openvibers/openvibe.events Commits
rotate-subscription-secret: --consumer rotates every first-party subscription of a service to one new secret (they share one env variable)
Changes
@@@ -3,7 +3,7 @@/*** Operator rotation of a first-party subscription's secret, without the value ever being printed.*-- * node scripts/rotate-subscription-secret.js --db <events.db> --subscription <sub_…>++ * node scripts/rotate-subscription-secret.js --db <events.db> (--subscription <sub_…> | --consumer <service>)* --env-file </etc/openvibe/<consumer>.env> --env-var <NAME> [--overlap-s 86400] [--apply]** With --apply it (1) sets a new random secret on the subscription, keeping the old one signing next to it@@@ -22,34 +22,38 @@ const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] :const apply = args.includes('--apply');const dbPath = opt('--db');const subId = opt('--subscription');++const consumer = opt('--consumer'); // every first-party subscription of that service, to ONE new secret (they share one env variable)const envFile = opt('--env-file');const envVar = opt('--env-var');const overlapS = Number(opt('--overlap-s', '86400'));function fail(msg) { console.error(`rotate-subscription-secret: ${msg}`); process.exit(1); }--if (!dbPath || !subId || !envFile || !envVar) fail('--db, --subscription, --env-file and --env-var are required');++if (!dbPath || !(subId || consumer) || !envFile || !envVar) fail('--db, --subscription or --consumer, --env-file and --env-var are required');if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(envVar)) fail('--env-var must be an env variable name');if (!Number.isFinite(overlapS) || overlapS < 0 || overlapS > 7 * 86400) fail('--overlap-s is 0..604800');if (!fs.existsSync(envFile)) fail(`${envFile} does not exist`);const db = new Database(dbPath, { fileMustExist: true });--const sub = db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE id = ?').get(subId);--if (!sub) fail(`no subscription ${subId}`);--if (sub.project_id) fail('developer-app subscriptions rotate through the API (POST /api/v1/subscriptions/:id/rotate-secret)');++const subs = consumer++ ? db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE consumer = ? AND project_id IS NULL').all(consumer)++ : db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE id = ?').all(subId);++if (!subs.length) fail(`no subscription for ${consumer || subId}`);++if (subs.some((x) => x.project_id)) fail('developer-app subscriptions rotate through the API (POST /api/v1/subscriptions/:id/rotate-secret)');++const sub = subs[0];const cols = new Set(db.prepare('PRAGMA table_info(subscriptions)').all().map((c) => c.name));if (!cols.has('previous_secret')) fail('this Events database predates rotation; deploy Events first (it adds the columns at boot)');const lines = fs.readFileSync(envFile, 'utf8').split('\n');const has = lines.some((l) => l.startsWith(`${envVar}=`));--console.log(`subscription ${sub.id} (${sub.consumer}, ${sub.topic_pattern}); ${envFile}: ${envVar} ${has ? 'will be replaced' : 'will be appended'}; overlap ${overlapS}s`);++console.log(`${subs.length} subscription(s) of ${sub.consumer} (${subs.map((x) => x.topic_pattern).join(', ')}); ${envFile}: ${envVar} ${has ? 'will be replaced' : 'will be appended'}; overlap ${overlapS}s`);if (!apply) { console.log('dry run: nothing changed (add --apply)'); process.exit(0); }const secret = `whsec_${crypto.randomBytes(32).toString('hex')}`;const now = Date.now();fs.copyFileSync(envFile, `${envFile}.bak-${new Date(now).toISOString().replace(/[:.]/g, '-')}`);db.transaction(() => {-- db.prepare('UPDATE subscriptions SET previous_secret = secret, previous_secret_until = ?, secret = ?, updated_at = ? WHERE id = ?')-- .run(now + overlapS * 1000, secret, now, sub.id);++ const up = db.prepare('UPDATE subscriptions SET previous_secret = secret, previous_secret_until = ?, secret = ?, updated_at = ? WHERE id = ?');++ for (const x of subs) up.run(now + overlapS * 1000, secret, now, x.id);})();let out;if (has) out = lines.map((l) => (l.startsWith(`${envVar}=`) ? `${envVar}=${secret}` : l));
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
/**
/**
* Operator rotation of a first-party subscription's secret, without the value ever being printed.
* Operator rotation of a first-party subscription's secret, without the value ever being printed.
*
*
- * node scripts/rotate-subscription-secret.js --db <events.db> --subscription <sub_…>
+ * node scripts/rotate-subscription-secret.js --db <events.db> (--subscription <sub_…> | --consumer <service>)
* --env-file </etc/openvibe/<consumer>.env> --env-var <NAME> [--overlap-s 86400] [--apply]
* --env-file </etc/openvibe/<consumer>.env> --env-var <NAME> [--overlap-s 86400] [--apply]
*
*
* With --apply it (1) sets a new random secret on the subscription, keeping the old one signing next to it
* With --apply it (1) sets a new random secret on the subscription, keeping the old one signing next to it
@@ -22,34 +22,38 @@ const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] :
@@ -22,34 +22,38 @@ const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] :
const apply = args.includes('--apply');
const apply = args.includes('--apply');
const dbPath = opt('--db');
const dbPath = opt('--db');
const subId = opt('--subscription');
const subId = opt('--subscription');
+const consumer = opt('--consumer'); // every first-party subscription of that service, to ONE new secret (they share one env variable)
const envFile = opt('--env-file');
const envFile = opt('--env-file');
const envVar = opt('--env-var');
const envVar = opt('--env-var');
const overlapS = Number(opt('--overlap-s', '86400'));
const overlapS = Number(opt('--overlap-s', '86400'));
function fail(msg) { console.error(`rotate-subscription-secret: ${msg}`); process.exit(1); }
function fail(msg) { console.error(`rotate-subscription-secret: ${msg}`); process.exit(1); }
-if (!dbPath || !subId || !envFile || !envVar) fail('--db, --subscription, --env-file and --env-var are required');
+if (!dbPath || !(subId || consumer) || !envFile || !envVar) fail('--db, --subscription or --consumer, --env-file and --env-var are required');
if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(envVar)) fail('--env-var must be an env variable name');
if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(envVar)) fail('--env-var must be an env variable name');
if (!Number.isFinite(overlapS) || overlapS < 0 || overlapS > 7 * 86400) fail('--overlap-s is 0..604800');
if (!Number.isFinite(overlapS) || overlapS < 0 || overlapS > 7 * 86400) fail('--overlap-s is 0..604800');
if (!fs.existsSync(envFile)) fail(`${envFile} does not exist`);
if (!fs.existsSync(envFile)) fail(`${envFile} does not exist`);
const db = new Database(dbPath, { fileMustExist: true });
const db = new Database(dbPath, { fileMustExist: true });
-const sub = db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE id = ?').get(subId);
-if (!sub) fail(`no subscription ${subId}`);
-if (sub.project_id) fail('developer-app subscriptions rotate through the API (POST /api/v1/subscriptions/:id/rotate-secret)');
+const subs = consumer
+ ? db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE consumer = ? AND project_id IS NULL').all(consumer)
+ : db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE id = ?').all(subId);
+if (!subs.length) fail(`no subscription for ${consumer || subId}`);
+if (subs.some((x) => x.project_id)) fail('developer-app subscriptions rotate through the API (POST /api/v1/subscriptions/:id/rotate-secret)');
+const sub = subs[0];
const cols = new Set(db.prepare('PRAGMA table_info(subscriptions)').all().map((c) => c.name));
const cols = new Set(db.prepare('PRAGMA table_info(subscriptions)').all().map((c) => c.name));
if (!cols.has('previous_secret')) fail('this Events database predates rotation; deploy Events first (it adds the columns at boot)');
if (!cols.has('previous_secret')) fail('this Events database predates rotation; deploy Events first (it adds the columns at boot)');
const lines = fs.readFileSync(envFile, 'utf8').split('\n');
const lines = fs.readFileSync(envFile, 'utf8').split('\n');
const has = lines.some((l) => l.startsWith(`${envVar}=`));
const has = lines.some((l) => l.startsWith(`${envVar}=`));
-console.log(`subscription ${sub.id} (${sub.consumer}, ${sub.topic_pattern}); ${envFile}: ${envVar} ${has ? 'will be replaced' : 'will be appended'}; overlap ${overlapS}s`);
+console.log(`${subs.length} subscription(s) of ${sub.consumer} (${subs.map((x) => x.topic_pattern).join(', ')}); ${envFile}: ${envVar} ${has ? 'will be replaced' : 'will be appended'}; overlap ${overlapS}s`);
if (!apply) { console.log('dry run: nothing changed (add --apply)'); process.exit(0); }
if (!apply) { console.log('dry run: nothing changed (add --apply)'); process.exit(0); }
const secret = `whsec_${crypto.randomBytes(32).toString('hex')}`;
const secret = `whsec_${crypto.randomBytes(32).toString('hex')}`;
const now = Date.now();
const now = Date.now();
fs.copyFileSync(envFile, `${envFile}.bak-${new Date(now).toISOString().replace(/[:.]/g, '-')}`);
fs.copyFileSync(envFile, `${envFile}.bak-${new Date(now).toISOString().replace(/[:.]/g, '-')}`);
db.transaction(() => {
db.transaction(() => {
- db.prepare('UPDATE subscriptions SET previous_secret = secret, previous_secret_until = ?, secret = ?, updated_at = ? WHERE id = ?')
- .run(now + overlapS * 1000, secret, now, sub.id);
+ const up = db.prepare('UPDATE subscriptions SET previous_secret = secret, previous_secret_until = ?, secret = ?, updated_at = ? WHERE id = ?');
+ for (const x of subs) up.run(now + overlapS * 1000, secret, now, x.id);
})();
})();
let out;
let out;
if (has) out = lines.map((l) => (l.startsWith(`${envVar}=`) ? `${envVar}=${secret}` : l));
if (has) out = lines.map((l) => (l.startsWith(`${envVar}=`) ? `${envVar}=${secret}` : l));