La primera version solo invalidaba emails, fireBaseToken, campos telegram* y services.google.email. Auditando el staging aparecieron, sin neutralizar: - 42 users con services.google.accessToken/refreshToken/idToken -> credenciales OAuth VIVAS que dan acceso a la cuenta de Google REAL del usuario - 42 users con services.google.name/given_name/family_name/picture - 99 users con profile.name.first/last (nombre y apellidos reales) - tokens de sesion (services.resume.loginTokens) y verificationTokens Pasaron desapercibidos porque el bloque de verificacion tampoco los comprobaba. Se anaden los $unset y se amplia la verificacion para que falle si reaparecen. services.password.bcrypt se CONSERVA a proposito (hashes, no reversibles directamente, y son la unica forma de hacer QA de login: el volcado real no trae la cuenta de fixtures). Documentado en el propio script.
161 lines
7.4 KiB
JavaScript
161 lines
7.4 KiB
JavaScript
// Neutraliza los datos de contacto en un volcado REAL de `fuegos` restaurado en el
|
|
// Mongo 7 de STAGING, para que ninguna notificación pueda alcanzar a un usuario real.
|
|
// IDEMPOTENTE: se puede ejecutar varias veces sin efectos acumulativos indeseados.
|
|
//
|
|
// Uso (desde el host de staging, contra el contenedor del compose):
|
|
// docker exec -i tcef-staging-mongo mongosh --quiet fuegos < scripts/neutralize-contacts.mongo.js
|
|
//
|
|
// SEGURIDAD: ejecutar SOLO contra el Mongo aislado de staging. Aborta si detecta que la
|
|
// conexión no es la esperada (heurística: nombre de host contiene 'shiva'/'simone'/'rbg'/'rosaparks').
|
|
|
|
(function guardAgainstProd() {
|
|
try {
|
|
const host = db.getMongo().host || '';
|
|
const prod = /shiva|simone|rbg|rosaparks|rsmain/i;
|
|
if (prod.test(host)) {
|
|
throw new Error('ABORTADO: la conexión (' + host + ') parece de PRODUCCIÓN. Este script solo debe correr contra el Mongo de staging.');
|
|
}
|
|
} catch (e) {
|
|
if (/ABORTADO/.test(e.message)) { print(e.message); quit(1); }
|
|
}
|
|
})();
|
|
|
|
const dbx = db.getSiblingDB('fuegos');
|
|
|
|
// 1) users.emails[].address -> "<_id>@invalid.test" (único por usuario) + verified:false
|
|
const rEmails = dbx.users.updateMany(
|
|
{ emails: { $type: 'array', $ne: [] } },
|
|
[{
|
|
$set: {
|
|
emails: {
|
|
$map: {
|
|
input: '$emails',
|
|
as: 'e',
|
|
in: {
|
|
address: { $concat: [{ $toString: '$_id' }, '@invalid.test'] },
|
|
verified: false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
);
|
|
|
|
// 2) users: purgar tokens/handles de notificación (FCM y Telegram)
|
|
const rUserTokens = dbx.users.updateMany(
|
|
{}, { $unset: {
|
|
fireBaseToken: '',
|
|
telegramChatId: '',
|
|
telegramUsername: '',
|
|
telegramFirstName: '',
|
|
telegramLanguageCode: ''
|
|
}}
|
|
);
|
|
|
|
// 3) users.services.google.email -> invalidar (evita PII en OAuth); conservar el resto de services
|
|
const rGoogle = dbx.users.updateMany(
|
|
{ 'services.google.email': { $exists: true } },
|
|
[{ $set: { 'services.google.email': { $concat: [{ $toString: '$_id' }, '@invalid.test'] } } }]
|
|
);
|
|
|
|
// 3b) users.services.google: purgar CREDENCIALES VIVAS y nombre/foto reales.
|
|
// accessToken/refreshToken/idToken son credenciales OAuth que dan acceso a la cuenta
|
|
// de Google REAL del usuario — lo más grave del volcado. name/given_name/family_name/
|
|
// picture/id son PII directa. (El paso 3 solo invalidaba el email.)
|
|
const rGoogleCreds = dbx.users.updateMany(
|
|
{ 'services.google': { $exists: true } },
|
|
{ $unset: {
|
|
'services.google.accessToken': '',
|
|
'services.google.refreshToken': '',
|
|
'services.google.idToken': '',
|
|
'services.google.expiresAt': '',
|
|
'services.google.scope': '',
|
|
'services.google.id': '',
|
|
'services.google.name': '',
|
|
'services.google.given_name': '',
|
|
'services.google.family_name': '',
|
|
'services.google.picture': '',
|
|
'services.google.locale': '',
|
|
'services.google.gender': ''
|
|
}}
|
|
);
|
|
|
|
// 3c) users.profile.name -> nombres y apellidos REALES (first/last). Se vacía el subdoc.
|
|
const rProfile = dbx.users.updateMany(
|
|
{ 'profile.name': { $exists: true } },
|
|
{ $unset: { 'profile.name.first': '', 'profile.name.last': '', 'profile.email': '' } }
|
|
);
|
|
|
|
// 3d) users.services.resume.loginTokens -> tokens de SESIÓN vivos (permiten suplantar
|
|
// al usuario sin contraseña). Se vacía el array. Idem verificationTokens de email.
|
|
const rTokens = dbx.users.updateMany(
|
|
{ $or: [ { 'services.resume.loginTokens.0': { $exists: true } },
|
|
{ 'services.email.verificationTokens.0': { $exists: true } } ] },
|
|
{ $set: { 'services.resume.loginTokens': [], 'services.email.verificationTokens': [] } }
|
|
);
|
|
|
|
// NOTA (decisión consciente, NO es un olvido): `services.password.bcrypt` se CONSERVA.
|
|
// Son hashes bcrypt (no reversibles directamente) y son la única forma de hacer QA de
|
|
// login en staging — no hay cuenta de fixtures en el volcado real. Si se quisiera cerrar
|
|
// también ese vector, hay que crear antes una cuenta de pruebas dedicada.
|
|
|
|
// 4) subscriptions: quitar el destino de Telegram (chatId) de las subs de ese tipo
|
|
const rSubs = dbx.subscriptions.updateMany(
|
|
{ chatId: { $exists: true } }, { $unset: { chatId: '' } }
|
|
);
|
|
|
|
// 5) Cola de Mail-Time: el volcado de prod trae correos ya encolados con el
|
|
// DESTINATARIO real "horneado" (p.ej. de 2020). Neutralizar users.emails NO los
|
|
// toca; la web (isMailServer) los drenaría. Se vacían las colas de la cola/jobs.
|
|
// (En staging MAIL_URL apunta a Mailhog, así que aunque quedaran, se capturarían;
|
|
// esto es defensa en profundidad para no arrastrar direcciones reales.)
|
|
const rMailQueue = dbx.getCollection('__mailTimeQueue__').deleteMany({});
|
|
const rMailJobs = dbx.getCollection('__JobTasks__mailTimeQueue').deleteMany({});
|
|
|
|
printjson({
|
|
usersEmailsNeutralizados: rEmails.modifiedCount,
|
|
usersTokensPurgados: rUserTokens.modifiedCount,
|
|
googleEmailsInvalidados: rGoogle.modifiedCount,
|
|
googleCredsPurgadas: rGoogleCreds.modifiedCount,
|
|
profileNombresPurgados: rProfile.modifiedCount,
|
|
tokensSesionPurgados: rTokens.modifiedCount,
|
|
subsChatIdEliminados: rSubs.modifiedCount,
|
|
colaCorreoBorrada: rMailQueue.deletedCount,
|
|
colaJobsBorrada: rMailJobs.deletedCount
|
|
});
|
|
|
|
// Verificación rápida (debe dar 0 en todas). OJO: filtrar por $exists ANTES del
|
|
// $not/regex — si no, los usuarios SIN email cuentan como "no invalidados" (falso positivo).
|
|
const conToken = dbx.users.countDocuments({ fireBaseToken: { $exists: true } });
|
|
const emailMalo = dbx.users.countDocuments({ 'emails.address': { $exists: true, $not: /@invalid\.test$/ } });
|
|
const googleMalo = dbx.users.countDocuments({ 'services.google.email': { $exists: true, $not: /@invalid\.test$/ } });
|
|
const subsConChat = dbx.subscriptions.countDocuments({ chatId: { $exists: true } });
|
|
const colaCorreo = dbx.getCollection('__mailTimeQueue__').countDocuments({});
|
|
// Campos que la primera versión del script NO comprobaba — por eso la fuga pasó
|
|
// desapercibida (42 users con credenciales OAuth vivas + 99 con nombre real).
|
|
const googleCreds = dbx.users.countDocuments({ $or: [
|
|
{ 'services.google.accessToken': { $exists: true } },
|
|
{ 'services.google.refreshToken': { $exists: true } },
|
|
{ 'services.google.idToken': { $exists: true } }
|
|
] });
|
|
const googleNom = dbx.users.countDocuments({ 'services.google.name': { $exists: true } });
|
|
const profileNom = dbx.users.countDocuments({ $or: [
|
|
{ 'profile.name.first': { $exists: true } },
|
|
{ 'profile.name.last': { $exists: true } }
|
|
] });
|
|
const sesiones = dbx.users.countDocuments({ 'services.resume.loginTokens.0': { $exists: true } });
|
|
printjson({ verificacion: {
|
|
usersConFireBaseToken: conToken, usersConEmailRealVisible: emailMalo,
|
|
usersConGoogleEmailReal: googleMalo, usersConGoogleCredsVivas: googleCreds,
|
|
usersConGoogleNombreReal: googleNom, usersConProfileNombreReal: profileNom,
|
|
usersConTokenSesion: sesiones, subsConChatId: subsConChat, correosEnCola: colaCorreo
|
|
} });
|
|
const pendientes = conToken + emailMalo + googleMalo + googleCreds + googleNom +
|
|
profileNom + sesiones + subsConChat + colaCorreo;
|
|
if (pendientes === 0) {
|
|
print('OK: contactos neutralizados. Ninguna notificación puede alcanzar a un usuario real,');
|
|
print(' y no quedan credenciales OAuth/sesión ni nombres reales.');
|
|
print('NOTA: services.password.bcrypt se conserva a propósito (QA de login) — ver comentario arriba.');
|
|
} else {
|
|
print('AVISO: quedan ' + pendientes + ' campos sin neutralizar — revisar antes de exponer el staging.');
|
|
}
|