Skip to content

Commit f983625

Browse files
committed
feat(inactive accts): prevent deletion if account is active
1 parent e9cd53e commit f983625

14 files changed

Lines changed: 252 additions & 19 deletions

File tree

packages/fxa-auth-server/bin/key_server.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ async function run(config) {
272272
config,
273273
push,
274274
pushbox,
275+
mailer: senders.email,
276+
statsd,
277+
glean,
278+
log,
275279
});
276280
Container.set(AccountDeleteManager, accountDeleteManager);
277281

packages/fxa-auth-server/lib/account-delete.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
getAllPayPalBAByUid,
88
} from 'fxa-shared/db/models/auth';
99
import { Container } from 'typedi';
10+
import { Logger } from 'mozlog';
11+
import { StatsD } from 'hot-shots';
1012

1113
import * as Sentry from '@sentry/node';
1214

@@ -24,8 +26,15 @@ import pushboxApi from './pushbox';
2426
import { AppConfig, AuthLogger, AuthRequest } from './types';
2527
import { DB } from './db';
2628
import { reportSentryError } from './sentry';
29+
import { GleanMetricsType } from './metrics/glean';
30+
import {
31+
InactiveAccountsManager,
32+
InactiveStatusOAuthDb,
33+
requestForGlean,
34+
} from './inactive-accounts';
2735

28-
type OAuthDbDeleteAccount = Pick<typeof OAuthDb, 'removeTokensAndCodes'>;
36+
type OAuthDbDeleteAccount = Pick<typeof OAuthDb, 'removeTokensAndCodes'> &
37+
InactiveStatusOAuthDb;
2938
type PushboxDeleteAccount = Pick<
3039
ReturnType<typeof pushboxApi>,
3140
'deleteAccount'
@@ -47,25 +56,38 @@ export class AccountDeleteManager {
4756
private playBilling?: PlayBilling;
4857
private log: Log;
4958
private config: ConfigType;
59+
private glean: GleanMetricsType;
60+
private statsd: StatsD;
61+
private inactiveAccountsManager: InactiveAccountsManager;
5062

5163
constructor({
5264
fxaDb,
5365
oauthDb,
5466
config,
5567
push,
5668
pushbox,
69+
mailer,
70+
statsd,
71+
glean,
72+
log,
5773
}: {
5874
fxaDb: DB;
5975
oauthDb: OAuthDbDeleteAccount;
6076
config: ConfigType;
6177
push: PushForDeleteAccount;
6278
pushbox: PushboxDeleteAccount;
79+
mailer: any;
80+
statsd: StatsD;
81+
glean: GleanMetricsType;
82+
log: Logger;
6383
}) {
6484
this.fxaDb = fxaDb;
6585
this.oauthDb = oauthDb;
6686
this.config = config;
6787
this.push = push;
6888
this.pushbox = pushbox;
89+
this.glean = glean;
90+
this.statsd = statsd;
6991

7092
if (Container.has(StripeHelper)) {
7193
this.stripeHelper = Container.get(StripeHelper);
@@ -84,6 +106,16 @@ export class AccountDeleteManager {
84106

85107
// Is this intentional? Config is passed in the constructor
86108
this.config = Container.get(AppConfig);
109+
110+
this.inactiveAccountsManager = new InactiveAccountsManager({
111+
fxaDb,
112+
oauthDb,
113+
mailer,
114+
config,
115+
statsd,
116+
glean,
117+
log,
118+
});
87119
}
88120

89121
/**
@@ -99,6 +131,20 @@ export class AccountDeleteManager {
99131
reason: ReasonForDeletion,
100132
customerId?: string
101133
) {
134+
// there's a day's time between shceduling and deletion so we need to check
135+
// if the account has became active in the meantime
136+
if (
137+
reason === ReasonForDeletion.InactiveAccountScheduled &&
138+
(await this.inactiveAccountsManager.isActive(uid))
139+
) {
140+
this.glean.inactiveAccountDeletion.deletionSkipped(requestForGlean, {
141+
uid,
142+
reason: 'active_account',
143+
});
144+
this.statsd.increment('account.inactive.deletion.skipped.active');
145+
return;
146+
}
147+
102148
await this.deleteAccountFromDb(uid);
103149
await this.deleteOAuthTokens(uid);
104150
// see comment in the function on why we are not awaiting

packages/fxa-auth-server/lib/inactive-accounts/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
ReasonForDeletion,
1616
DeleteAccountTasks,
1717
} from '@fxa/shared/cloud-tasks';
18-
import { ConnectedServicesDb } from 'fxa-shared/connected-services';
18+
import OAuthDb from '../oauth/db';
1919

2020
import { ConfigType } from '../../config';
2121
import { AccountEventsManager } from '../account-events';
@@ -65,6 +65,10 @@ export const requestForGlean = {
6565
export const isCloudTaskAlreadyExistsError = (error) =>
6666
error.code === 10 && error.message.includes('entity already exists');
6767

68+
export type InactiveStatusOAuthDb = Pick<
69+
typeof OAuthDb,
70+
'getAccessTokensByUid' | 'getRefreshTokensByUid'
71+
>;
6872
export type NotificationAttempt = 'first' | 'second' | 'final';
6973
export type GleanEmailSkippedEvent = `${NotificationAttempt}EmailSkipped`;
7074
export type GleanEmailTaskRequestEvent =
@@ -136,7 +140,7 @@ export const emailTypeToHandlerVals: Record<
136140

137141
export class InactiveAccountsManager {
138142
fxaDb: DB;
139-
oauthDb: ConnectedServicesDb;
143+
oauthDb: InactiveStatusOAuthDb;
140144
accountEventsManager: AccountEventsManager;
141145
accountTasks: DeleteAccountTasks;
142146
mailer: any;
@@ -155,7 +159,7 @@ export class InactiveAccountsManager {
155159
log,
156160
}: {
157161
fxaDb: DB;
158-
oauthDb: ConnectedServicesDb;
162+
oauthDb: InactiveStatusOAuthDb;
159163
mailer: any;
160164
config: ConfigType;
161165
statsd: StatsD;

packages/fxa-auth-server/lib/metrics/glean/index.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,10 @@ export function gleanMetrics(config: ConfigType) {
350350
}
351351
),
352352
secondEmailSkipped: createEventFn(
353-
'inactive_account_deletion_second_email_skipped'
353+
'inactive_account_deletion_second_email_skipped',
354+
{
355+
additionalMetrics: extraKeyReasonCb,
356+
}
354357
),
355358

356359
finalEmailTaskRequest: createEventFn(
@@ -368,7 +371,16 @@ export function gleanMetrics(config: ConfigType) {
368371
}
369372
),
370373
finalEmailSkipped: createEventFn(
371-
'inactive_account_deletion_final_email_skipped'
374+
'inactive_account_deletion_final_email_skipped',
375+
{
376+
additionalMetrics: extraKeyReasonCb,
377+
}
378+
),
379+
deletionSkipped: createEventFn(
380+
'inactive_account_deletion_deletion_skipped',
381+
{
382+
additionalMetrics: extraKeyReasonCb,
383+
}
372384
),
373385
deletionScheduled: createEventFn(
374386
'inactive_account_deletion_deletion_scheduled'

packages/fxa-auth-server/lib/metrics/glean/server_events.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,94 @@ class EventsServerEventLogger {
720720
event,
721721
});
722722
}
723+
/**
724+
* Record and submit a inactive_account_deletion_deletion_skipped event:
725+
* The scheduled deletion of an inactive account was skipped.
726+
* Event is logged using internal mozlog logger.
727+
*
728+
* @param {string} user_agent - The user agent.
729+
* @param {string} ip_address - The IP address. Will be used to decode Geo
730+
* information and scrubbed at ingestion.
731+
* @param {string} account_user_id - The firefox/mozilla account id.
732+
* @param {string} account_user_id_sha256 - A hex string of a sha256 hash of the account's uid.
733+
* @param {string} relying_party_oauth_client_id - The client id of the relying party.
734+
* @param {string} relying_party_service - The service name of the relying party.
735+
* @param {string} session_device_type - one of 'mobile', 'tablet', or ''.
736+
* @param {string} session_entrypoint - Entrypoint to the service.
737+
* @param {string} session_entrypoint_experiment - Identifier for the experiment the user is part of at the entrypoint.
738+
* @param {string} session_entrypoint_variation - Identifier for the experiment variation the user is part of at the entrypoint.
739+
* @param {string} session_flow_id - an ID generated by FxA for its flow metrics.
740+
* @param {string} utm_campaign - A marketing campaign. For example, if a user signs into FxA from selecting a Mozilla VPN plan on Mozilla VPN's product site, then the value of this metric could be 'vpn-product-page'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters. The special value of 'page+referral+-+not+part+of+a+campaign' is also allowed..
741+
* @param {string} utm_content - The content on which the user acted. For example, if the user clicked on the (previously available) "Get started here" link in "Looking for Firefox Sync? Get started here", then the value for this metric would be 'fx-sync-get-started'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
742+
* @param {string} utm_medium - The "medium" on which the user acted. For example, if the user clicked on a link in an email, then the value of this metric would be 'email'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
743+
* @param {string} utm_source - The source from where the user started. For example, if the user clicked on a link on the Mozilla accounts web site, this value could be 'fx-website'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
744+
* @param {string} utm_term - This metric is similar to the `utm.source`; it is used in the Firefox browser. For example, if the user started from about:welcome, then the value could be 'aboutwelcome-default-screen'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
745+
* @param {string} reason - The reason the deletion was skipped..
746+
*/
747+
recordInactiveAccountDeletionDeletionSkipped({
748+
user_agent,
749+
ip_address,
750+
account_user_id,
751+
account_user_id_sha256,
752+
relying_party_oauth_client_id,
753+
relying_party_service,
754+
session_device_type,
755+
session_entrypoint,
756+
session_entrypoint_experiment,
757+
session_entrypoint_variation,
758+
session_flow_id,
759+
utm_campaign,
760+
utm_content,
761+
utm_medium,
762+
utm_source,
763+
utm_term,
764+
reason,
765+
}: {
766+
user_agent: string;
767+
ip_address: string;
768+
account_user_id: string;
769+
account_user_id_sha256: string;
770+
relying_party_oauth_client_id: string;
771+
relying_party_service: string;
772+
session_device_type: string;
773+
session_entrypoint: string;
774+
session_entrypoint_experiment: string;
775+
session_entrypoint_variation: string;
776+
session_flow_id: string;
777+
utm_campaign: string;
778+
utm_content: string;
779+
utm_medium: string;
780+
utm_source: string;
781+
utm_term: string;
782+
reason: string;
783+
}) {
784+
const event = {
785+
category: 'inactive_account_deletion',
786+
name: 'deletion_skipped',
787+
extra: {
788+
reason: String(reason),
789+
},
790+
};
791+
this.#record({
792+
user_agent,
793+
ip_address,
794+
account_user_id,
795+
account_user_id_sha256,
796+
relying_party_oauth_client_id,
797+
relying_party_service,
798+
session_device_type,
799+
session_entrypoint,
800+
session_entrypoint_experiment,
801+
session_entrypoint_variation,
802+
session_flow_id,
803+
utm_campaign,
804+
utm_content,
805+
utm_medium,
806+
utm_source,
807+
utm_term,
808+
event,
809+
});
810+
}
723811
/**
724812
* Record and submit a inactive_account_deletion_final_email_skipped event:
725813
* The final email notification was skipped.

packages/fxa-auth-server/scripts/delete-account.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
DeleteAccountTasksFactory,
3636
} from '@fxa/shared/cloud-tasks';
3737
import { ProfileClient } from '@fxa/profile/client';
38+
const { gleanMetrics } = require('../lib/metrics/glean');
3839

3940
const config = configProperties.getProperties();
4041
const mailer = null;
@@ -43,13 +44,14 @@ const statsd = {
4344
increment: () => {},
4445
timing: () => {},
4546
close: () => {},
46-
};
47+
} as unknown as StatsD;
4748
const log = require('../lib/log')(config.log.level, 'delete-account-script', {
4849
statsd,
4950
});
5051
const Token = require('../lib/tokens')(log, config);
5152
const { createDB } = require('../lib/db');
5253
const DB = createDB(config, log, Token);
54+
const glean = gleanMetrics(config);
5355

5456
Container.set(StatsD, statsd);
5557
Container.set(AppConfig, config);
@@ -132,6 +134,10 @@ DB.connect(config).then(async (db: any) => {
132134
config,
133135
push,
134136
pushbox,
137+
mailer,
138+
statsd,
139+
glean,
140+
log,
135141
});
136142
Container.set(AccountDeleteManager, accountDeleteManager);
137143

0 commit comments

Comments
 (0)