diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index a8f55cdee8..b2c85395f6 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -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; }); } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a03d94270a..62992ed20b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -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" } } diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index 972a2421cb..acd9be2a8e 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -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), ), ); } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 4320de5071..587d6860c0 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -41,21 +41,37 @@ - +
+
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index cfed6ca621..f0bca311a8 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -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 { + return await BrowserApi.permissionsGranted(["privacy"]); + } } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 5ffb78dcb8..300979d77d 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -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(); const settingsService = mock(); const userVerificationService = mock(); - const configService = mock(); beforeEach(() => { autofillService = new AutofillService( @@ -67,7 +65,6 @@ describe("AutofillService", () => { logService, settingsService, userVerificationService, - configService, ); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 80c5f7297f..74174c153e 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -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, ) {} /** diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index e9a992e9b0..dc58e54cfc 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -614,7 +614,6 @@ export default class MainBackground { this.logService, this.settingsService, this.userVerificationService, - this.configService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index f9925998f0..d6b02e0af6 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -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"], diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c9d9f979f0..32094c89c3 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -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'", diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index 887c1284a0..231f8b78ab 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -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, + }); + }); + }); }); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 229d993fb6..4cf3d44e21 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -445,4 +445,43 @@ export class BrowserApi { }); }); } + + /** + * Identifies if the browser autofill settings are overridden by the extension. + */ + static async browserAutofillSettingsOverridden(): Promise { + 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 }); + } } diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 647e4cfdfb..9f787d8109 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -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;