diff --git a/packages/fxa-auth-server/scripts/transfer-users/apple.js b/packages/fxa-auth-server/scripts/transfer-users/apple.js index b71f9b61e0c..5dc3de26b6d 100644 --- a/packages/fxa-auth-server/scripts/transfer-users/apple.js +++ b/packages/fxa-auth-server/scripts/transfer-users/apple.js @@ -11,6 +11,7 @@ const program = require('commander'); const axios = require('axios'); const random = require('../../lib/crypto/random'); +const Log = require('../../lib/log') const uuid = require('uuid'); const GRANT_TYPE = 'client_credentials'; @@ -20,7 +21,7 @@ const USER_MIGRATION_ENDPOINT = 'https://appleid.apple.com/auth/usermigrationinf const APPLE_PROVIDER = 'apple'; export class AppleUser { - constructor(email, transferSub, uid, alternateEmails, db, writeStream, config, mock) { + constructor(email, transferSub, uid, alternateEmails, db, writeStream, config, mock, log) { this.email = email; this.transferSub = transferSub; this.uid = uid; @@ -29,6 +30,7 @@ export class AppleUser { this.writeStream = writeStream; this.config = config; this.mock = mock; + this.log = log; } // Exchanges the Apple `transfer_sub` for the user's profile information and @@ -208,6 +210,18 @@ export class AppleUser { const transferSub = this.transferSub; const success = this.success; const err = (this.err && this.err.message) || ''; + + this.log.notifyAttachedServices( + 'appleUserMigration', {}, + { + uid, + appleEmail, + fxaEmail, + transferSub, + success, + err, + }, + ); const line = `${transferSub},${uid},${fxaEmail},${appleEmail},${success},${err}`; this.writeStream.write(line + '\n'); } @@ -229,6 +243,16 @@ export class ApplePocketFxAMigration { this.writeStream.on('error', (err) => { console.error(`There was an error writing the file: ${err}`); }); + + const statsd = { + increment: () => {}, + timing: () => {}, + close: () => {}, + }; + this.log = Log({ + ...config.log, + statsd, + }); } parseCSV() { @@ -258,7 +282,7 @@ export class ApplePocketFxAMigration { // Splits on `:` since they are not allowed in emails alternateEmails = tokens[3].replaceAll('"', '').split(':'); } - return new AppleUser(email, transferSub, uid, alternateEmails, this.db, this.writeStream, this.config, this.mock); + return new AppleUser(email, transferSub, uid, alternateEmails, this.db, this.writeStream, this.config, this.mock, this.log); }).filter((user) => user); } catch (err) { console.error('No such file or directory'); diff --git a/packages/fxa-auth-server/test/scripts/apple-transfer-users.js b/packages/fxa-auth-server/test/scripts/apple-transfer-users.js index f1a1b11b661..092bbbe2d20 100644 --- a/packages/fxa-auth-server/test/scripts/apple-transfer-users.js +++ b/packages/fxa-auth-server/test/scripts/apple-transfer-users.js @@ -171,7 +171,7 @@ describe('ApplePocketFxAMigration', function() { }); describe('AppleUser', function() { - let sandbox, dbStub, user, writeStreamStub; + let sandbox, dbStub, user, writeStreamStub, log; beforeEach(function() { sandbox = sinon.createSandbox(); dbStub = { @@ -184,7 +184,10 @@ describe('AppleUser', function() { writeStreamStub = { write: sandbox.stub() }; - user = new AppleUser('pocket@example.com', 'transferSub', 'uid', ['altEmail@example.com'], dbStub, writeStreamStub, config); + log = { + notifyAttachedServices: sandbox.stub().resolves() + } + user = new AppleUser('pocket@example.com', 'transferSub', 'uid', ['altEmail@example.com'], dbStub, writeStreamStub, config, false, log); }); afterEach(function() { @@ -300,5 +303,15 @@ describe('AppleUser', function() { user.saveResult(); const expectedOutput = 'transferSub,uid,fxa@example.com,apple@example.com,true,\n'; assert.calledWith(user.writeStream.write, expectedOutput); + + assert.calledOnceWithExactly(user.log.notifyAttachedServices, 'appleUserMigration', {}, + { + uid: 'uid', + appleEmail:'apple@example.com', + fxaEmail:'fxa@example.com', + transferSub: 'transferSub', + success: true, + err: '', + }) }); }); \ No newline at end of file diff --git a/packages/fxa-event-broker/src/jwtset/jwtset.service.ts b/packages/fxa-event-broker/src/jwtset/jwtset.service.ts index f1cd3243e80..a1c284da067 100644 --- a/packages/fxa-event-broker/src/jwtset/jwtset.service.ts +++ b/packages/fxa-event-broker/src/jwtset/jwtset.service.ts @@ -116,4 +116,21 @@ export class JwtsetService { uid: delEvent.uid, }); } + + public generateAppleMigrationSET(appleMigrationEvent: set.appleMigrationEvent): Promise { + return this.generateSET({ + uid: appleMigrationEvent.uid, + clientId: appleMigrationEvent.clientId, + events: { + [set.APPLE_USER_MIGRATION_ID]: { + fxaEmail: appleMigrationEvent.fxaEmail, + appleEmail: appleMigrationEvent.appleEmail, + transferSub: appleMigrationEvent.transferSub, + success: appleMigrationEvent.success, + err: appleMigrationEvent.err, + uid: appleMigrationEvent.uid, + } + } + }); + } } diff --git a/packages/fxa-event-broker/src/jwtset/set.interface.ts b/packages/fxa-event-broker/src/jwtset/set.interface.ts index 127349fe0e4..ab006921d3f 100644 --- a/packages/fxa-event-broker/src/jwtset/set.interface.ts +++ b/packages/fxa-event-broker/src/jwtset/set.interface.ts @@ -11,6 +11,10 @@ export const PROFILE_EVENT_ID = export const SUBSCRIPTION_STATE_EVENT_ID = 'https://schemas.accounts.firefox.com/event/subscription-state-change'; +export const APPLE_USER_MIGRATION_ID = + 'https://schemas.accounts.firefox.com/event/apple-user-migration'; + + export type deleteEvent = { clientId: string; uid: string; @@ -45,3 +49,13 @@ export type subscriptionEvent = { isActive: boolean; changeTime: number; }; + +export type appleMigrationEvent = { + uid: string; + clientId: string; + fxaEmail: string; + appleEmail: string; + transferSub: string; + success: boolean; + err: string; +}; diff --git a/packages/fxa-event-broker/src/pubsub-proxy/pubsub-proxy.controller.ts b/packages/fxa-event-broker/src/pubsub-proxy/pubsub-proxy.controller.ts index ec18437677c..3eadf803f7f 100644 --- a/packages/fxa-event-broker/src/pubsub-proxy/pubsub-proxy.controller.ts +++ b/packages/fxa-event-broker/src/pubsub-proxy/pubsub-proxy.controller.ts @@ -196,6 +196,17 @@ export class PubsubProxyController { email: message.email, }); } + case dto.APPLE_USER_MIGRATION_EVENT: { + return await this.jwtset.generateAppleMigrationSET({ + clientId, + uid: message.uid, + fxaEmail: message.fxaEmail, + appleEmail: message.appleEmail, + transferSub: message.transferSub, + success: message.success, + err: message.error + }); + } default: throw Error(`Invalid event: ${message.event}`); } diff --git a/packages/fxa-event-broker/src/queueworker/queueworker.service.spec.ts b/packages/fxa-event-broker/src/queueworker/queueworker.service.spec.ts index de8a035da68..2ddafa3d45c 100644 --- a/packages/fxa-event-broker/src/queueworker/queueworker.service.spec.ts +++ b/packages/fxa-event-broker/src/queueworker/queueworker.service.spec.ts @@ -70,6 +70,18 @@ const baseProfileMessage = { event: 'profileDataChange', }; +const appleMigrationMessage = { + event: 'appleUserMigration', + timestamp: now, + ts: now / 1000, + uid: '993d26bac72b471991b197b3d298a5de', + fxaEmail: 'fxa@email.com', + appleEmail: 'apple@email.com', + transferSub: '123', + success: true, + err: '', +}; + const updateStubMessage = (message: any) => { return { Body: JSON.stringify(message), @@ -149,6 +161,7 @@ describe('QueueworkerService', () => { serviceNotificationQueueUrl: 'https://sqs.us-east-2.amazonaws.com/queue.mozilla/321321321/notifications', log: { app: 'test' }, + topicPrefix: 'rp', }; const MockConfig: Provider = { provide: ConfigService, @@ -250,6 +263,28 @@ describe('QueueworkerService', () => { 'resource-server-client-id', ]); }); + + it('handles apple migration event', async () => { + const msg = updateStubMessage(appleMigrationMessage); + await (service as any).handleMessage(msg); + + const topicName = 'rp749818d3f2e7857f'; + expect(pubsub.topic).toBeCalledWith(topicName); + expect(pubsub.topic(topicName).publishMessage).toBeCalledTimes(1); + expect(pubsub.topic(topicName).publishMessage).toBeCalledWith({ + json: { + 'appleEmail': 'apple@email.com', + 'err': '', + 'event': 'appleUserMigration', + 'fxaEmail': 'fxa@email.com', + 'success': true, + 'timestamp': now, + 'transferSub': '123', + 'ts': now / 1000, + 'uid': '993d26bac72b471991b197b3d298a5de', + }, + }); + }); const fetchOnValidMessage = { 'delete message': baseDeleteMessage, diff --git a/packages/fxa-event-broker/src/queueworker/queueworker.service.ts b/packages/fxa-event-broker/src/queueworker/queueworker.service.ts index b04e4428dae..6d03967d3a7 100644 --- a/packages/fxa-event-broker/src/queueworker/queueworker.service.ts +++ b/packages/fxa-event-broker/src/queueworker/queueworker.service.ts @@ -162,7 +162,7 @@ export class QueueworkerService * @param eventType Event type to use for metrics */ private async handleMessageFanout( - message: dto.deleteSchema | dto.profileSchema | dto.passwordSchema, + message: dto.deleteSchema | dto.profileSchema | dto.passwordSchema | dto.appleUserMigrationSchema, eventType: string ) { this.metrics.increment('message.type', { eventType }); @@ -261,6 +261,22 @@ export class QueueworkerService await Promise.all(notifyClientPromises); } + /** + * Notify Pocket that a user's Apple account has been migrated. + * + * @param message Incoming SQS Message + */ + private async handleAppleUserMigrationEvent(message: dto.appleUserMigrationSchema) { + // Note the hardcoded Pocket clientId, this value should not change in production + const clientId = '749818d3f2e7857f'; + this.metrics.increment('message.type', { eventType: 'appleMigration' }); + const rpMessage = { + timestamp: Date.now(), + ...message, + }; + await this.publishMessage(clientId, rpMessage); + } + /** * Process a SQS message, dispatch based on message event type. * @@ -306,6 +322,10 @@ export class QueueworkerService await this.handleMessageFanout(message, 'password'); break; } + case dto.APPLE_USER_MIGRATION_EVENT: { + await this.handleAppleUserMigrationEvent(message); + break; + } default: unhandledEventType(message); } diff --git a/packages/fxa-event-broker/src/queueworker/service-notification.interface.ts b/packages/fxa-event-broker/src/queueworker/service-notification.interface.ts index 49e2b79ed01..6c639c03b37 100644 --- a/packages/fxa-event-broker/src/queueworker/service-notification.interface.ts +++ b/packages/fxa-event-broker/src/queueworker/service-notification.interface.ts @@ -13,6 +13,7 @@ export type ServiceNotification = | dto.passwordSchema | dto.profileSchema | dto.subscriptionUpdateSchema + | dto.appleUserMigrationSchema | undefined; interface SchemaTable { @@ -28,6 +29,7 @@ const eventSchemas = { [dto.PRIMARY_EMAIL_EVENT]: dto.PROFILE_CHANGE_SCHEMA, [dto.PASSWORD_CHANGE_EVENT]: dto.PASSWORD_CHANGE_SCHEMA, [dto.PASSWORD_RESET_EVENT]: dto.PASSWORD_CHANGE_SCHEMA, + [dto.APPLE_USER_MIGRATION_EVENT]: dto.APPLE_USER_MIGRATION_SCHEMA, }; /** diff --git a/packages/fxa-event-broker/src/queueworker/sqs.dto.ts b/packages/fxa-event-broker/src/queueworker/sqs.dto.ts index 596c7ec29e4..ce4256339a9 100644 --- a/packages/fxa-event-broker/src/queueworker/sqs.dto.ts +++ b/packages/fxa-event-broker/src/queueworker/sqs.dto.ts @@ -11,6 +11,7 @@ export const PASSWORD_RESET_EVENT = 'reset'; export const PRIMARY_EMAIL_EVENT = 'primaryEmailChanged'; export const PROFILE_CHANGE_EVENT = 'profileDataChange'; export const SUBSCRIPTION_UPDATE_EVENT = 'subscription:update'; +export const APPLE_USER_MIGRATION_EVENT = 'appleUserMigration'; // Message schemas export const CLIENT_ID = joi.string().regex(/[a-z0-9]{16}/); @@ -91,6 +92,18 @@ export const PROFILE_CHANGE_SCHEMA = joi .unknown(true) .required(); +export const APPLE_USER_MIGRATION_SCHEMA = joi + .object() + .keys({ + event: joi.string().valid(APPLE_USER_MIGRATION_EVENT), + timestamp: joi.number().optional(), + ts: joi.number().required(), + uid: joi.string().required(), + }) + .unknown(true) + .required(); + + export type deleteSchema = { event: typeof DELETE_EVENT; timestamp?: number; @@ -136,3 +149,15 @@ export type subscriptionUpdateSchema = { ts: number; uid: string; }; + +export type appleUserMigrationSchema = { + event: typeof APPLE_USER_MIGRATION_EVENT; + timestamp?: number; + ts: number; + uid: string; + fxaEmail: string; + appleEmail: string; + transferSub: string; + success: boolean; + err: string; +} \ No newline at end of file