// 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.'); }