Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mask tokens when exporting onyx state #47996

Merged
merged 10 commits into from
Sep 2, 2024
33 changes: 30 additions & 3 deletions src/libs/ExportOnyxState/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import {Str} from 'expensify-common';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Session} from '@src/types/onyx';

const maskSessionDetails = (data: Record<string, unknown>): Record<string, unknown> => {
const session = data.session as Session;

Object.keys(session).forEach((key) => {
if (key !== 'authToken' && key !== 'encryptedAuthToken') {
return;
}
session[key] = '***';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about extracting this pattern to a global constant?

});

return {
...data,
session,
};
};

const maskFragileData = (data: Record<string, unknown>, parentKey?: string): Record<string, unknown> => {
const maskedData: Record<string, unknown> = {};
Expand All @@ -15,7 +32,7 @@ const maskFragileData = (data: Record<string, unknown>, parentKey?: string): Rec

const value = data[key];

if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) {
if (typeof value === 'string' && Str.isValidEmail(value)) {
maskedData[key] = '***';
} else if (parentKey && parentKey.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) && (key === 'text' || key === 'html')) {
maskedData[key] = '***';
Expand All @@ -29,6 +46,16 @@ const maskFragileData = (data: Record<string, unknown>, parentKey?: string): Rec
return maskedData;
};

export default {
maskFragileData,
const maskOnyxState = (data: Record<string, unknown>, isMaskingFragileDataEnabled?: boolean) => {
let onyxState = data;
// Mask session details by default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Mask session details by default
// Mask session details by default

onyxState = maskSessionDetails(data);
// Mask fragile data other than session details if the user has enabled the option
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Mask fragile data other than session details if the user has enabled the option
// Mask fragile data other than session details if the user has enabled the option

if (isMaskingFragileDataEnabled) {
onyxState = maskFragileData(data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function maskSessionDetails is also modifying the data input (we should avoid it). That's why it's weird here, I'm expecting that we should pass onyxState as input here.

}

return onyxState;
};

export default maskOnyxState;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export default maskOnyxState;
export default {
maskOnyxState;
}

I think we should export objects here. So we can export other common utils later. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, done 👍

4 changes: 2 additions & 2 deletions src/libs/ExportOnyxState/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import RNFS from 'react-native-fs';
import {open} from 'react-native-quick-sqlite';
import Share from 'react-native-share';
import CONST from '@src/CONST';
import common from './common';
import maskOnyxState from './common';

const readFromOnyxDatabase = () =>
new Promise((resolve) => {
Expand Down Expand Up @@ -36,7 +36,7 @@ const shareAsFile = (value: string) => {
};

export default {
maskFragileData: common.maskFragileData,
maskOnyxState,
readFromOnyxDatabase,
shareAsFile,
};
4 changes: 2 additions & 2 deletions src/libs/ExportOnyxState/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CONST from '@src/CONST';
import common from './common';
import maskOnyxState from './common';

const readFromOnyxDatabase = () =>
new Promise<Record<string, unknown>>((resolve) => {
Expand Down Expand Up @@ -44,7 +44,7 @@ const shareAsFile = (value: string) => {
};

export default {
maskFragileData: common.maskFragileData,
maskOnyxState,
readFromOnyxDatabase,
shareAsFile,
};
6 changes: 1 addition & 5 deletions src/pages/settings/Troubleshoot/TroubleshootPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,7 @@ function TroubleshootPage({shouldStoreLogs, shouldMaskOnyxState}: TroubleshootPa

const exportOnyxState = useCallback(() => {
ExportOnyxState.readFromOnyxDatabase().then((value: Record<string, unknown>) => {
let dataToShare = value;
if (shouldMaskOnyxState) {
dataToShare = ExportOnyxState.maskFragileData(value);
}

const dataToShare = ExportOnyxState.maskOnyxState(value, shouldMaskOnyxState);
ExportOnyxState.shareAsFile(JSON.stringify(dataToShare));
});
}, [shouldMaskOnyxState]);
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/ExportOnyxStateTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import maskOnyxState from '@libs/ExportOnyxState/common';
import type * as OnyxTypes from '@src/types/onyx';

type ExampleOnyxState = {
session: OnyxTypes.Session;
};

describe('maskOnyxState', () => {
const mockSession = {
authToken: 'sensitive-auth-token',
encryptedAuthToken: 'sensitive-encrypted-token',
email: 'user@example.com',
accountID: 12345,
};

it('should mask session details by default', () => {
const input = {session: mockSession};
const result = maskOnyxState(input) as ExampleOnyxState;

expect(result.session.authToken).toBe('***');
expect(result.session.encryptedAuthToken).toBe('***');
});

it('should not mask fragile data when isMaskingFragileDataEnabled is false', () => {
const input = {
session: mockSession,
};
const result = maskOnyxState(input) as ExampleOnyxState;

expect(result.session.authToken).toBe('***');
expect(result.session.encryptedAuthToken).toBe('***');
expect(result.session.email).toBe('user@example.com');
});

it('should mask fragile data when isMaskingFragileDataEnabled is true', () => {
const input = {
session: mockSession,
};
const result = maskOnyxState(input, true) as ExampleOnyxState;

expect(result.session.authToken).toBe('***');
expect(result.session.encryptedAuthToken).toBe('***');
expect(result.session.email).toBe('***');
});
});
Loading