1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[PM-5617] Re-add setting to turn off browser autofill (#7592)

* [PM-5617] Re-Add Setting to Turn Off Default Browser Autofill

* [PM-5617] Re-Add Setting to Turn Off Default Browser Autofill

* [PM-5617] Removing the privacy optional permission from Firefox

* [PM-5617] Adding jest tests to validate the behavior within BrowserApi

* [PM-5617] Adjusting messaging based on feedback from design

* [PM-5617] Adjusting messaging based on feedback from design

* [PM-5617] Adjusting messaging based on feedback from design

* [PM-5617] Removing unnecessary configService dependency
This commit is contained in:
Cesar Gonzalez 2024-01-23 13:23:08 -06:00 committed by GitHub
parent dbf836b573
commit 609296ad2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 278 additions and 26 deletions

View File

@ -62,6 +62,9 @@ function distFirefox() {
return dist("firefox", (manifest) => {
delete manifest.storage;
delete manifest.sandbox;
manifest.optional_permissions = manifest.optional_permissions.filter(
(permission) => permission !== "privacy",
);
return manifest;
});
}

View File

@ -2895,5 +2895,33 @@
"commonImportFormats": {
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"overrideDefaultBrowserAutofillTitle": {
"message": "Make Bitwarden your default password manager?",
"description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutofillDescription": {
"message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.",
"description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutofillPrivacyRequiredDescription": {
"message": "This action will restart the Bitwarden extension. Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.",
"description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutoFillSettings": {
"message": "Make Bitwarden your default password manager",
"description": "Label for the setting that allows overriding the default browser autofill settings"
},
"privacyPermissionAdditionNotGrantedTitle": {
"message": "Unable to set Bitwarden as the default password manager",
"description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"privacyPermissionAdditionNotGrantedDescription": {
"message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.",
"description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"makeDefault": {
"message": "Make default",
"description": "Button text for the setting that allows overriding the default browser autofill settings"
}
}

View File

@ -10,10 +10,6 @@ import {
settingsServiceFactory,
SettingsServiceInitOptions,
} from "../../../background/service-factories/settings-service.factory";
import {
configServiceFactory,
ConfigServiceInitOptions,
} from "../../../platform/background/service-factories/config-service.factory";
import {
CachedServices,
factory,
@ -47,8 +43,7 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
EventCollectionServiceInitOptions &
LogServiceInitOptions &
SettingsServiceInitOptions &
UserVerificationServiceInitOptions &
ConfigServiceInitOptions;
UserVerificationServiceInitOptions;
export function autofillServiceFactory(
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
@ -67,7 +62,6 @@ export function autofillServiceFactory(
await logServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts),
await configServiceFactory(cache, opts),
),
);
}

View File

@ -41,21 +41,37 @@
</option>
</select>
</div>
<div class="box-footer" *ngIf="accountSwitcherEnabled">
<div class="box-footer" *ngIf="accountSwitcherEnabled && canOverrideBrowserAutofillSetting">
{{ "showAutoFillMenuOnFormFieldsDescAlt" | i18n }}
</div>
<div class="box-footer">
{{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }}
<a
[attr.href]="disablePasswordManagerLink"
(click)="openDisablePasswordManagerLink($event)"
target="_blank"
rel="noopener noreferrer"
>
{{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }}
</a>
</div>
</div>
<div class="box">
<div class="box-content" *ngIf="canOverrideBrowserAutofillSetting">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="overrideBrowserAutofill" class="!tw-mr-0">{{
"overrideDefaultBrowserAutoFillSettings" | i18n
}}</label>
<input
id="overrideBrowserAutofill"
type="checkbox"
(change)="updateDefaultBrowserAutofillDisabled()"
[(ngModel)]="defaultBrowserAutofillDisabled"
/>
</div>
</div>
<div class="box-footer">
<span *ngIf="accountSwitcherEnabled">{{ "showAutoFillMenuOnFormFieldsDescAlt" | i18n }}</span>
{{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }}
<a
[attr.href]="disablePasswordManagerLink"
(click)="openDisablePasswordManagerLink($event)"
target="_blank"
rel="noopener noreferrer"
>
{{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }}
</a>
</div>
</div>
<div class="box tw-mt-4">
<div class="box-content">

View File

@ -6,6 +6,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
@ -17,6 +18,8 @@ import { AutofillOverlayVisibility } from "../../utils/autofill-overlay.enum";
templateUrl: "autofill.component.html",
})
export class AutofillComponent implements OnInit {
protected canOverrideBrowserAutofillSetting = false;
protected defaultBrowserAutofillDisabled = false;
protected autoFillOverlayVisibility: number;
protected autoFillOverlayVisibilityOptions: any[];
protected disablePasswordManagerLink: string;
@ -35,6 +38,7 @@ export class AutofillComponent implements OnInit {
private configService: ConfigServiceAbstraction,
private settingsService: SettingsService,
private autofillService: AutofillService,
private dialogService: DialogService,
) {
this.autoFillOverlayVisibilityOptions = [
{
@ -68,6 +72,14 @@ export class AutofillComponent implements OnInit {
}
async ngOnInit() {
this.canOverrideBrowserAutofillSetting =
this.platformUtilsService.isChrome() ||
this.platformUtilsService.isEdge() ||
this.platformUtilsService.isOpera() ||
this.platformUtilsService.isVivaldi();
this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden();
this.autoFillOverlayVisibility =
(await this.settingsService.getAutoFillOverlayVisibility()) || AutofillOverlayVisibility.Off;
@ -87,6 +99,7 @@ export class AutofillComponent implements OnInit {
await this.settingsService.getAutoFillOverlayVisibility();
await this.settingsService.setAutoFillOverlayVisibility(this.autoFillOverlayVisibility);
await this.handleUpdatingAutofillOverlayContentScripts(previousAutoFillOverlayVisibility);
await this.requestPrivacyPermission();
}
async updateAutoFillOnPageLoad() {
@ -165,4 +178,73 @@ export class AutofillComponent implements OnInit {
await this.autofillService.reloadAutofillScripts();
}
async requestPrivacyPermission() {
if (
this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off ||
!this.canOverrideBrowserAutofillSetting ||
(await this.browserAutofillSettingCurrentlyOverridden())
) {
return;
}
const permissionGranted = await this.privacyPermissionGranted();
const contentKey = permissionGranted
? "overrideDefaultBrowserAutofillDescription"
: "overrideDefaultBrowserAutofillPrivacyRequiredDescription";
await this.dialogService.openSimpleDialog({
title: { key: "overrideDefaultBrowserAutofillTitle" },
content: { key: contentKey },
acceptButtonText: { key: "makeDefault" },
acceptAction: async () => await this.handleOverrideDialogAccept(),
cancelButtonText: { key: "ignore" },
type: "info",
});
}
async updateDefaultBrowserAutofillDisabled() {
const privacyPermissionGranted = await this.privacyPermissionGranted();
if (!this.defaultBrowserAutofillDisabled && !privacyPermissionGranted) {
return;
}
if (
!privacyPermissionGranted &&
!(await BrowserApi.requestPermission({ permissions: ["privacy"] }))
) {
await this.dialogService.openSimpleDialog({
title: { key: "privacyPermissionAdditionNotGrantedTitle" },
content: { key: "privacyPermissionAdditionNotGrantedDescription" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "warning",
});
this.defaultBrowserAutofillDisabled = false;
return;
}
BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
}
private handleOverrideDialogAccept = async () => {
this.defaultBrowserAutofillDisabled = true;
await this.updateDefaultBrowserAutofillDisabled();
};
async browserAutofillSettingCurrentlyOverridden() {
if (!this.canOverrideBrowserAutofillSetting) {
return false;
}
if (!(await this.privacyPermissionGranted())) {
return false;
}
return await BrowserApi.browserAutofillSettingsOverridden();
}
async privacyPermissionGranted(): Promise<boolean> {
return await BrowserApi.permissionsGranted(["privacy"]);
}
}

View File

@ -3,7 +3,6 @@ import { mock, mockReset } from "jest-mock-extended";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { EventType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import {
@ -56,7 +55,6 @@ describe("AutofillService", () => {
const logService = mock<LogService>();
const settingsService = mock<SettingsService>();
const userVerificationService = mock<UserVerificationService>();
const configService = mock<ConfigService>();
beforeEach(() => {
autofillService = new AutofillService(
@ -67,7 +65,6 @@ describe("AutofillService", () => {
logService,
settingsService,
userVerificationService,
configService,
);
});

View File

@ -2,7 +2,6 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@ -47,7 +46,6 @@ export default class AutofillService implements AutofillServiceInterface {
private logService: LogService,
private settingsService: SettingsService,
private userVerificationService: UserVerificationService,
private configService: ConfigServiceAbstraction,
) {}
/**

View File

@ -614,7 +614,6 @@ export default class MainBackground {
this.logService,
this.settingsService,
this.userVerificationService,
this.configService,
);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);

View File

@ -69,7 +69,7 @@
"webRequest",
"webRequestBlocking"
],
"optional_permissions": ["nativeMessaging"],
"optional_permissions": ["nativeMessaging", "privacy"],
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
"sandbox": {
"pages": ["overlay/button.html", "overlay/list.html"],

View File

@ -65,7 +65,7 @@
"alarms",
"scripting"
],
"optional_permissions": ["nativeMessaging"],
"optional_permissions": ["nativeMessaging", "privacy"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",

View File

@ -136,4 +136,82 @@ describe("BrowserApi", () => {
expect(result).toEqual(executeScriptResult);
});
});
describe("browserAutofillSettingsOverridden", () => {
it("returns true if the browser autofill settings are overridden", async () => {
const expectedDetails = {
value: false,
levelOfControl: "controlled_by_this_extension",
} as chrome.types.ChromeSettingGetResultDetails;
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
const result = await BrowserApi.browserAutofillSettingsOverridden();
expect(result).toBe(true);
});
it("returns false if the browser autofill settings are not overridden", async () => {
const expectedDetails = {
value: true,
levelOfControl: "controlled_by_this_extension",
} as chrome.types.ChromeSettingGetResultDetails;
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
const result = await BrowserApi.browserAutofillSettingsOverridden();
expect(result).toBe(false);
});
it("returns false if the browser autofill settings are not controlled by the extension", async () => {
const expectedDetails = {
value: false,
levelOfControl: "controlled_by_other_extensions",
} as chrome.types.ChromeSettingGetResultDetails;
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
callback(expectedDetails),
);
const result = await BrowserApi.browserAutofillSettingsOverridden();
expect(result).toBe(false);
});
});
describe("updateDefaultBrowserAutofillSettings", () => {
it("updates the default browser autofill settings", async () => {
await BrowserApi.updateDefaultBrowserAutofillSettings(false);
expect(chrome.privacy.services.autofillAddressEnabled.set).toHaveBeenCalledWith({
value: false,
});
expect(chrome.privacy.services.autofillCreditCardEnabled.set).toHaveBeenCalledWith({
value: false,
});
expect(chrome.privacy.services.passwordSavingEnabled.set).toHaveBeenCalledWith({
value: false,
});
});
});
});

View File

@ -445,4 +445,43 @@ export class BrowserApi {
});
});
}
/**
* Identifies if the browser autofill settings are overridden by the extension.
*/
static async browserAutofillSettingsOverridden(): Promise<boolean> {
const checkOverrideStatus = (details: chrome.types.ChromeSettingGetResultDetails) =>
details.levelOfControl === "controlled_by_this_extension" && !details.value;
const autofillAddressOverridden: boolean = await new Promise((resolve) =>
chrome.privacy.services.autofillAddressEnabled.get({}, (details) =>
resolve(checkOverrideStatus(details)),
),
);
const autofillCreditCardOverridden: boolean = await new Promise((resolve) =>
chrome.privacy.services.autofillCreditCardEnabled.get({}, (details) =>
resolve(checkOverrideStatus(details)),
),
);
const passwordSavingOverridden: boolean = await new Promise((resolve) =>
chrome.privacy.services.passwordSavingEnabled.get({}, (details) =>
resolve(checkOverrideStatus(details)),
),
);
return autofillAddressOverridden && autofillCreditCardOverridden && passwordSavingOverridden;
}
/**
* Updates the browser autofill settings to the given value.
*
* @param value - Determines whether to enable or disable the autofill settings.
*/
static updateDefaultBrowserAutofillSettings(value: boolean) {
chrome.privacy.services.autofillAddressEnabled.set({ value });
chrome.privacy.services.autofillCreditCardEnabled.set({ value });
chrome.privacy.services.passwordSavingEnabled.set({ value });
}
}

View File

@ -88,6 +88,23 @@ const port = {
postMessage: jest.fn(),
};
const privacy = {
services: {
autofillAddressEnabled: {
get: jest.fn(),
set: jest.fn(),
},
autofillCreditCardEnabled: {
get: jest.fn(),
set: jest.fn(),
},
passwordSavingEnabled: {
get: jest.fn(),
set: jest.fn(),
},
},
};
// set chrome
global.chrome = {
i18n,
@ -98,4 +115,5 @@ global.chrome = {
scripting,
windows,
port,
privacy,
} as any;