[PM-3589] Context Menu No Longer Shows Autofill Ciphers (#6085)
* [PM-3589] Context Menu No Longer Shows Autofill Ciphers * [PM-3589] Ensuring that passwordless users can also access ciphers that require reprompt * [PM-3589] Fixing jest test * [PM-3589] Fixing issue where context menu autofill does not allow filling when passwordless setup is in place
This commit is contained in:
parent
9288367bc8
commit
b444eed0b5
|
@ -1,7 +1,6 @@
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
@ -14,7 +13,6 @@ describe("CipherContextMenuHandler", () => {
|
||||||
let mainContextMenuHandler: MockProxy<MainContextMenuHandler>;
|
let mainContextMenuHandler: MockProxy<MainContextMenuHandler>;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let cipherService: MockProxy<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let userVerificationService: MockProxy<UserVerificationService>;
|
|
||||||
|
|
||||||
let sut: CipherContextMenuHandler;
|
let sut: CipherContextMenuHandler;
|
||||||
|
|
||||||
|
@ -22,17 +20,10 @@ describe("CipherContextMenuHandler", () => {
|
||||||
mainContextMenuHandler = mock();
|
mainContextMenuHandler = mock();
|
||||||
authService = mock();
|
authService = mock();
|
||||||
cipherService = mock();
|
cipherService = mock();
|
||||||
userVerificationService = mock();
|
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
|
||||||
|
|
||||||
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
|
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
|
||||||
|
|
||||||
sut = new CipherContextMenuHandler(
|
sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService);
|
||||||
mainContextMenuHandler,
|
|
||||||
authService,
|
|
||||||
cipherService,
|
|
||||||
userVerificationService
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => jest.resetAllMocks());
|
afterEach(() => jest.resetAllMocks());
|
||||||
|
@ -78,7 +69,7 @@ describe("CipherContextMenuHandler", () => {
|
||||||
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
|
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("only adds valid ciphers", async () => {
|
it("only adds login ciphers including ciphers that require reprompt", async () => {
|
||||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
|
|
||||||
mainContextMenuHandler.init.mockResolvedValue(true);
|
mainContextMenuHandler.init.mockResolvedValue(true);
|
||||||
|
@ -90,47 +81,6 @@ describe("CipherContextMenuHandler", () => {
|
||||||
name: "Test Cipher",
|
name: "Test Cipher",
|
||||||
login: { username: "Test Username" },
|
login: { username: "Test Username" },
|
||||||
};
|
};
|
||||||
|
|
||||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([
|
|
||||||
null, // invalid cipher
|
|
||||||
undefined, // invalid cipher
|
|
||||||
{ type: CipherType.Card }, // invalid cipher
|
|
||||||
{ type: CipherType.Login, reprompt: CipherRepromptType.Password }, // invalid cipher
|
|
||||||
realCipher, // valid cipher
|
|
||||||
] as any[]);
|
|
||||||
|
|
||||||
await sut.update("https://test.com");
|
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
|
||||||
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
|
||||||
"Test Cipher (Test Username)",
|
|
||||||
"5",
|
|
||||||
"https://test.com",
|
|
||||||
realCipher
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds ciphers with master password reprompt if the user does not have a master password", async () => {
|
|
||||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
|
||||||
|
|
||||||
// User does not have a master password, or has one but hasn't logged in with it (key connector user or TDE user)
|
|
||||||
userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false);
|
|
||||||
|
|
||||||
mainContextMenuHandler.init.mockResolvedValue(true);
|
|
||||||
|
|
||||||
const realCipher = {
|
|
||||||
id: "5",
|
|
||||||
type: CipherType.Login,
|
|
||||||
reprompt: CipherRepromptType.None,
|
|
||||||
name: "Test Cipher",
|
|
||||||
login: { username: "Test Username" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const repromptCipher = {
|
const repromptCipher = {
|
||||||
id: "6",
|
id: "6",
|
||||||
type: CipherType.Login,
|
type: CipherType.Login,
|
||||||
|
@ -143,8 +93,8 @@ describe("CipherContextMenuHandler", () => {
|
||||||
null, // invalid cipher
|
null, // invalid cipher
|
||||||
undefined, // invalid cipher
|
undefined, // invalid cipher
|
||||||
{ type: CipherType.Card }, // invalid cipher
|
{ type: CipherType.Card }, // invalid cipher
|
||||||
repromptCipher, // valid cipher
|
|
||||||
realCipher, // valid cipher
|
realCipher, // valid cipher
|
||||||
|
repromptCipher,
|
||||||
] as any[]);
|
] as any[]);
|
||||||
|
|
||||||
await sut.update("https://test.com");
|
await sut.update("https://test.com");
|
||||||
|
@ -153,7 +103,6 @@ describe("CipherContextMenuHandler", () => {
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
||||||
|
|
||||||
// Should call this twice, once for each valid cipher
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
@ -12,7 +11,6 @@ import {
|
||||||
authServiceFactory,
|
authServiceFactory,
|
||||||
AuthServiceInitOptions,
|
AuthServiceInitOptions,
|
||||||
} from "../../auth/background/service-factories/auth-service.factory";
|
} from "../../auth/background/service-factories/auth-service.factory";
|
||||||
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { CachedServices } from "../../platform/background/service-factories/factory-options";
|
import { CachedServices } from "../../platform/background/service-factories/factory-options";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
@ -39,8 +37,7 @@ export class CipherContextMenuHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private mainContextMenuHandler: MainContextMenuHandler,
|
private mainContextMenuHandler: MainContextMenuHandler,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService
|
||||||
private userVerificationService: UserVerificationService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static async create(cachedServices: CachedServices) {
|
static async create(cachedServices: CachedServices) {
|
||||||
|
@ -79,8 +76,7 @@ export class CipherContextMenuHandler {
|
||||||
return new CipherContextMenuHandler(
|
return new CipherContextMenuHandler(
|
||||||
await MainContextMenuHandler.mv3Create(cachedServices),
|
await MainContextMenuHandler.mv3Create(cachedServices),
|
||||||
await authServiceFactory(cachedServices, serviceOptions),
|
await authServiceFactory(cachedServices, serviceOptions),
|
||||||
await cipherServiceFactory(cachedServices, serviceOptions),
|
await cipherServiceFactory(cachedServices, serviceOptions)
|
||||||
await userVerificationServiceFactory(cachedServices, serviceOptions)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,11 +176,7 @@ export class CipherContextMenuHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateForCipher(url: string, cipher: CipherView) {
|
private async updateForCipher(url: string, cipher: CipherView) {
|
||||||
if (
|
if (cipher == null || cipher.type !== CipherType.Login) {
|
||||||
cipher == null ||
|
|
||||||
cipher.type !== CipherType.Login ||
|
|
||||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
@ -63,6 +64,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||||
let cipherService: MockProxy<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let totpService: MockProxy<TotpService>;
|
let totpService: MockProxy<TotpService>;
|
||||||
let eventCollectionService: MockProxy<EventCollectionService>;
|
let eventCollectionService: MockProxy<EventCollectionService>;
|
||||||
|
let userVerificationService: MockProxy<UserVerificationService>;
|
||||||
|
|
||||||
let sut: ContextMenuClickedHandler;
|
let sut: ContextMenuClickedHandler;
|
||||||
|
|
||||||
|
@ -82,7 +84,8 @@ describe("ContextMenuClickedHandler", () => {
|
||||||
authService,
|
authService,
|
||||||
cipherService,
|
cipherService,
|
||||||
totpService,
|
totpService,
|
||||||
eventCollectionService
|
eventCollectionService,
|
||||||
|
userVerificationService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
|
@ -14,6 +15,7 @@ import {
|
||||||
AuthServiceInitOptions,
|
AuthServiceInitOptions,
|
||||||
} from "../../auth/background/service-factories/auth-service.factory";
|
} from "../../auth/background/service-factories/auth-service.factory";
|
||||||
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
|
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
|
||||||
|
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
||||||
import LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem";
|
import LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem";
|
||||||
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
|
@ -56,7 +58,8 @@ export class ContextMenuClickedHandler {
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private eventCollectionService: EventCollectionService
|
private eventCollectionService: EventCollectionService,
|
||||||
|
private userVerificationService: UserVerificationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static async mv3Create(cachedServices: CachedServices) {
|
static async mv3Create(cachedServices: CachedServices) {
|
||||||
|
@ -109,7 +112,8 @@ export class ContextMenuClickedHandler {
|
||||||
await authServiceFactory(cachedServices, serviceOptions),
|
await authServiceFactory(cachedServices, serviceOptions),
|
||||||
await cipherServiceFactory(cachedServices, serviceOptions),
|
await cipherServiceFactory(cachedServices, serviceOptions),
|
||||||
await totpServiceFactory(cachedServices, serviceOptions),
|
await totpServiceFactory(cachedServices, serviceOptions),
|
||||||
await eventCollectionServiceFactory(cachedServices, serviceOptions)
|
await eventCollectionServiceFactory(cachedServices, serviceOptions),
|
||||||
|
await userVerificationServiceFactory(cachedServices, serviceOptions)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +208,7 @@ export class ContextMenuClickedHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: AUTOFILL_ID,
|
action: AUTOFILL_ID,
|
||||||
|
@ -218,7 +222,7 @@ export class ContextMenuClickedHandler {
|
||||||
this.copyToClipboard({ text: cipher.login.username, tab: tab });
|
this.copyToClipboard({ text: cipher.login.username, tab: tab });
|
||||||
break;
|
break;
|
||||||
case COPY_PASSWORD_ID:
|
case COPY_PASSWORD_ID:
|
||||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: COPY_PASSWORD_ID,
|
action: COPY_PASSWORD_ID,
|
||||||
|
@ -230,7 +234,7 @@ export class ContextMenuClickedHandler {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case COPY_VERIFICATIONCODE_ID:
|
case COPY_VERIFICATIONCODE_ID:
|
||||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: COPY_VERIFICATIONCODE_ID,
|
action: COPY_VERIFICATIONCODE_ID,
|
||||||
|
@ -246,6 +250,13 @@ export class ContextMenuClickedHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async isPasswordRepromptRequired(cipher: CipherView): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
cipher.reprompt === CipherRepromptType.Password &&
|
||||||
|
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
BrowserApi.sendTabsMessage(
|
BrowserApi.sendTabsMessage(
|
||||||
|
|
|
@ -635,7 +635,8 @@ export default class MainBackground {
|
||||||
this.authService,
|
this.authService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.totpService,
|
this.totpService,
|
||||||
this.eventCollectionService
|
this.eventCollectionService,
|
||||||
|
this.userVerificationService
|
||||||
);
|
);
|
||||||
|
|
||||||
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
|
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
|
||||||
|
@ -670,8 +671,7 @@ export default class MainBackground {
|
||||||
this.cipherContextMenuHandler = new CipherContextMenuHandler(
|
this.cipherContextMenuHandler = new CipherContextMenuHandler(
|
||||||
this.mainContextMenuHandler,
|
this.mainContextMenuHandler,
|
||||||
this.authService,
|
this.authService,
|
||||||
this.cipherService,
|
this.cipherService
|
||||||
this.userVerificationService
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,8 +170,8 @@ export class ViewComponent extends BaseViewComponent {
|
||||||
|
|
||||||
switch (this.loadAction) {
|
switch (this.loadAction) {
|
||||||
case AUTOFILL_ID:
|
case AUTOFILL_ID:
|
||||||
this.fillCipher();
|
await this.fillCipher();
|
||||||
return;
|
break;
|
||||||
case COPY_USERNAME_ID:
|
case COPY_USERNAME_ID:
|
||||||
await this.copy(this.cipher.login.username, "username", "Username");
|
await this.copy(this.cipher.login.username, "username", "Username");
|
||||||
break;
|
break;
|
||||||
|
@ -186,7 +186,7 @@ export class ViewComponent extends BaseViewComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inPopout && this.loadAction) {
|
if (this.inPopout && this.loadAction) {
|
||||||
this.close();
|
setTimeout(() => this.close(), 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,10 +238,6 @@ export class ViewComponent extends BaseViewComponent {
|
||||||
const didAutofill = await this.doAutofill();
|
const didAutofill = await this.doAutofill();
|
||||||
if (didAutofill) {
|
if (didAutofill) {
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
|
||||||
|
|
||||||
if (this.inPopout) {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue