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;