Merge branch 'main' into billing/AC-1970/provider-billing-area
This commit is contained in:
commit
e24acdcc83
|
@ -212,6 +212,8 @@ import { UpdateBadge } from "../platform/listeners/update-badge";
|
|||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
||||
/* eslint-enable no-restricted-imports */
|
||||
import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document";
|
||||
import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service";
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||
|
@ -336,6 +338,7 @@ export default class MainBackground {
|
|||
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
||||
scriptInjectorService: BrowserScriptInjectorService;
|
||||
kdfConfigService: kdfConfigServiceAbstraction;
|
||||
offscreenDocumentService: OffscreenDocumentService;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
|
@ -393,11 +396,14 @@ export default class MainBackground {
|
|||
),
|
||||
);
|
||||
|
||||
this.offscreenDocumentService = new DefaultOffscreenDocumentService();
|
||||
|
||||
this.platformUtilsService = new BackgroundPlatformUtilsService(
|
||||
this.messagingService,
|
||||
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
|
||||
async () => this.biometricUnlock(),
|
||||
self,
|
||||
this.offscreenDocumentService,
|
||||
);
|
||||
|
||||
// Creates a session key for mv3 storage of large memory items
|
||||
|
@ -737,7 +743,6 @@ export default class MainBackground {
|
|||
this.cipherService,
|
||||
this.folderService,
|
||||
this.collectionService,
|
||||
this.cryptoService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.searchService,
|
||||
|
|
|
@ -12,10 +12,6 @@ import {
|
|||
internalMasterPasswordServiceFactory,
|
||||
MasterPasswordServiceInitOptions,
|
||||
} from "../../auth/background/service-factories/master-password-service.factory";
|
||||
import {
|
||||
CryptoServiceInitOptions,
|
||||
cryptoServiceFactory,
|
||||
} from "../../platform/background/service-factories/crypto-service.factory";
|
||||
import {
|
||||
CachedServices,
|
||||
factory,
|
||||
|
@ -70,7 +66,6 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
|
|||
CipherServiceInitOptions &
|
||||
FolderServiceInitOptions &
|
||||
CollectionServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
PlatformUtilsServiceInitOptions &
|
||||
MessagingServiceInitOptions &
|
||||
SearchServiceInitOptions &
|
||||
|
@ -94,7 +89,6 @@ export function vaultTimeoutServiceFactory(
|
|||
await cipherServiceFactory(cache, opts),
|
||||
await folderServiceFactory(cache, opts),
|
||||
await collectionServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await platformUtilsServiceFactory(cache, opts),
|
||||
await messagingServiceFactory(cache, opts),
|
||||
await searchServiceFactory(cache, opts),
|
||||
|
|
|
@ -30,6 +30,7 @@ export function platformUtilsServiceFactory(
|
|||
opts.platformUtilsServiceOptions.clipboardWriteCallback,
|
||||
opts.platformUtilsServiceOptions.biometricCallback,
|
||||
opts.platformUtilsServiceOptions.win,
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -525,32 +525,6 @@ describe("BrowserApi", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("createOffscreenDocument", () => {
|
||||
it("creates the offscreen document with the supplied reasons and justification", async () => {
|
||||
const reasons = [chrome.offscreen.Reason.CLIPBOARD];
|
||||
const justification = "justification";
|
||||
|
||||
await BrowserApi.createOffscreenDocument(reasons, justification);
|
||||
|
||||
expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({
|
||||
url: "offscreen-document/index.html",
|
||||
reasons,
|
||||
justification,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeOffscreenDocument", () => {
|
||||
it("closes the offscreen document", () => {
|
||||
const callbackMock = jest.fn();
|
||||
|
||||
BrowserApi.closeOffscreenDocument(callbackMock);
|
||||
|
||||
expect(chrome.offscreen.closeDocument).toHaveBeenCalled();
|
||||
expect(callbackMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerContentScriptsMv2", () => {
|
||||
const details: browser.contentScripts.RegisteredContentScriptOptions = {
|
||||
matches: ["<all_urls>"],
|
||||
|
|
|
@ -558,34 +558,6 @@ export class BrowserApi {
|
|||
chrome.privacy.services.passwordSavingEnabled.set({ value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the offscreen document with the given reasons and justification.
|
||||
*
|
||||
* @param reasons - List of reasons for opening the offscreen document.
|
||||
* @see https://developer.chrome.com/docs/extensions/reference/api/offscreen#type-Reason
|
||||
* @param justification - Custom written justification for opening the offscreen document.
|
||||
*/
|
||||
static async createOffscreenDocument(reasons: chrome.offscreen.Reason[], justification: string) {
|
||||
await chrome.offscreen.createDocument({
|
||||
url: "offscreen-document/index.html",
|
||||
reasons,
|
||||
justification,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the offscreen document.
|
||||
*
|
||||
* @param callback - Optional callback to execute after the offscreen document is closed.
|
||||
*/
|
||||
static closeOffscreenDocument(callback?: () => void) {
|
||||
chrome.offscreen.closeDocument(() => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles registration of static content scripts within manifest v2.
|
||||
*
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
type OffscreenDocumentExtensionMessage = {
|
||||
export type OffscreenDocumentExtensionMessage = {
|
||||
[key: string]: any;
|
||||
command: string;
|
||||
text?: string;
|
||||
|
@ -9,18 +9,20 @@ type OffscreenExtensionMessageEventParams = {
|
|||
sender: chrome.runtime.MessageSender;
|
||||
};
|
||||
|
||||
type OffscreenDocumentExtensionMessageHandlers = {
|
||||
export type OffscreenDocumentExtensionMessageHandlers = {
|
||||
[key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any;
|
||||
offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any;
|
||||
offscreenReadFromClipboard: () => any;
|
||||
};
|
||||
|
||||
interface OffscreenDocument {
|
||||
export interface OffscreenDocument {
|
||||
init(): void;
|
||||
}
|
||||
|
||||
export {
|
||||
OffscreenDocumentExtensionMessage,
|
||||
OffscreenDocumentExtensionMessageHandlers,
|
||||
OffscreenDocument,
|
||||
};
|
||||
export abstract class OffscreenDocumentService {
|
||||
abstract withDocument<T>(
|
||||
reasons: chrome.offscreen.Reason[],
|
||||
justification: string,
|
||||
callback: () => Promise<T> | T,
|
||||
): Promise<T>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import { DefaultOffscreenDocumentService } from "./offscreen-document.service";
|
||||
|
||||
class TestCase {
|
||||
synchronicity: string;
|
||||
private _callback: () => Promise<any> | any;
|
||||
get callback() {
|
||||
return jest.fn(this._callback);
|
||||
}
|
||||
|
||||
constructor(synchronicity: string, callback: () => Promise<any> | any) {
|
||||
this.synchronicity = synchronicity;
|
||||
this._callback = callback;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.synchronicity;
|
||||
}
|
||||
}
|
||||
|
||||
describe.each([
|
||||
new TestCase("synchronous callback", () => 42),
|
||||
new TestCase("asynchronous callback", () => Promise.resolve(42)),
|
||||
])("DefaultOffscreenDocumentService %s", (testCase) => {
|
||||
let sut: DefaultOffscreenDocumentService;
|
||||
const reasons = [chrome.offscreen.Reason.TESTING];
|
||||
const justification = "justification is testing";
|
||||
const url = "offscreen-document/index.html";
|
||||
const api = {
|
||||
createDocument: jest.fn(),
|
||||
closeDocument: jest.fn(),
|
||||
hasDocument: jest.fn().mockResolvedValue(false),
|
||||
Reason: chrome.offscreen.Reason,
|
||||
};
|
||||
let callback: jest.Mock<() => Promise<number> | number>;
|
||||
|
||||
beforeEach(() => {
|
||||
callback = testCase.callback;
|
||||
chrome.offscreen = api;
|
||||
|
||||
sut = new DefaultOffscreenDocumentService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("withDocument", () => {
|
||||
it("creates a document when none exists", async () => {
|
||||
await sut.withDocument(reasons, justification, () => {});
|
||||
|
||||
expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({
|
||||
url,
|
||||
reasons,
|
||||
justification,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create a document when one exists", async () => {
|
||||
api.hasDocument.mockResolvedValue(true);
|
||||
|
||||
await sut.withDocument(reasons, justification, callback);
|
||||
|
||||
expect(chrome.offscreen.createDocument).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe.each([true, false])("hasDocument returns %s", (hasDocument) => {
|
||||
beforeEach(() => {
|
||||
api.hasDocument.mockResolvedValue(hasDocument);
|
||||
});
|
||||
|
||||
it("calls the callback", async () => {
|
||||
await sut.withDocument(reasons, justification, callback);
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns the callback result", async () => {
|
||||
const result = await sut.withDocument(reasons, justification, callback);
|
||||
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it("closes the document when the callback completes and no other callbacks are running", async () => {
|
||||
await sut.withDocument(reasons, justification, callback);
|
||||
|
||||
expect(chrome.offscreen.closeDocument).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not close the document when the callback completes and other callbacks are running", async () => {
|
||||
await Promise.all([
|
||||
sut.withDocument(reasons, justification, callback),
|
||||
sut.withDocument(reasons, justification, callback),
|
||||
sut.withDocument(reasons, justification, callback),
|
||||
sut.withDocument(reasons, justification, callback),
|
||||
]);
|
||||
|
||||
expect(chrome.offscreen.closeDocument).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService {
|
||||
private workerCount = 0;
|
||||
|
||||
constructor() {}
|
||||
|
||||
async withDocument<T>(
|
||||
reasons: chrome.offscreen.Reason[],
|
||||
justification: string,
|
||||
callback: () => Promise<T> | T,
|
||||
): Promise<T> {
|
||||
this.workerCount++;
|
||||
try {
|
||||
if (!(await this.documentExists())) {
|
||||
await this.create(reasons, justification);
|
||||
}
|
||||
|
||||
return await callback();
|
||||
} finally {
|
||||
this.workerCount--;
|
||||
if (this.workerCount === 0) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async create(reasons: chrome.offscreen.Reason[], justification: string): Promise<void> {
|
||||
await chrome.offscreen.createDocument({
|
||||
url: "offscreen-document/index.html",
|
||||
reasons,
|
||||
justification,
|
||||
});
|
||||
}
|
||||
|
||||
private async close(): Promise<void> {
|
||||
await chrome.offscreen.closeDocument();
|
||||
}
|
||||
|
||||
private async documentExists(): Promise<boolean> {
|
||||
return await chrome.offscreen.hasDocument();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||
|
||||
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||
|
||||
export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||
|
@ -8,8 +10,9 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService
|
|||
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||
biometricCallback: () => Promise<boolean>,
|
||||
win: Window & typeof globalThis,
|
||||
offscreenDocumentService: OffscreenDocumentService,
|
||||
) {
|
||||
super(clipboardWriteCallback, biometricCallback, win);
|
||||
super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService);
|
||||
}
|
||||
|
||||
override showToast(
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
|
||||
import { flushPromises } from "../../../autofill/spec/testing-utils";
|
||||
import { SafariApp } from "../../../browser/safariApp";
|
||||
import { BrowserApi } from "../../browser/browser-api";
|
||||
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||
import BrowserClipboardService from "../browser-clipboard.service";
|
||||
|
||||
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||
|
||||
class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||
constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) {
|
||||
super(clipboardSpy, null, win);
|
||||
constructor(
|
||||
clipboardSpy: jest.Mock,
|
||||
win: Window & typeof globalThis,
|
||||
offscreenDocumentService: OffscreenDocumentService,
|
||||
) {
|
||||
super(clipboardSpy, null, win, offscreenDocumentService);
|
||||
}
|
||||
|
||||
showToast(
|
||||
|
@ -24,13 +31,16 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
|
|||
|
||||
describe("Browser Utils Service", () => {
|
||||
let browserPlatformUtilsService: BrowserPlatformUtilsService;
|
||||
let offscreenDocumentService: MockProxy<OffscreenDocumentService>;
|
||||
const clipboardWriteCallbackSpy = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
offscreenDocumentService = mock();
|
||||
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
|
||||
browserPlatformUtilsService = new TestBrowserPlatformUtilsService(
|
||||
clipboardWriteCallbackSpy,
|
||||
window,
|
||||
offscreenDocumentService,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -223,23 +233,23 @@ describe("Browser Utils Service", () => {
|
|||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(undefined);
|
||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text);
|
||||
await flushPromises();
|
||||
|
||||
expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text);
|
||||
expect(clipboardServiceCopySpy).not.toHaveBeenCalled();
|
||||
expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith(
|
||||
expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Write text to the clipboard.",
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const callback = offscreenDocumentService.withDocument.mock.calls[0][2];
|
||||
await callback();
|
||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", {
|
||||
text,
|
||||
});
|
||||
expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips the clipboardWriteCallback if the clipboard is clearing", async () => {
|
||||
|
@ -298,18 +308,21 @@ describe("Browser Utils Service", () => {
|
|||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue("test");
|
||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
||||
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||
Promise.resolve("test"),
|
||||
);
|
||||
|
||||
await browserPlatformUtilsService.readFromClipboard();
|
||||
|
||||
expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith(
|
||||
expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Read text from the clipboard.",
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const callback = offscreenDocumentService.withDocument.mock.calls[0][2];
|
||||
await callback();
|
||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard");
|
||||
expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an empty string from the offscreen document if the response is not of type string", async () => {
|
||||
|
@ -317,9 +330,10 @@ describe("Browser Utils Service", () => {
|
|||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1);
|
||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
||||
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||
Promise.resolve(1),
|
||||
);
|
||||
|
||||
const result = await browserPlatformUtilsService.readFromClipboard();
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
|
||||
import { SafariApp } from "../../../browser/safariApp";
|
||||
import { BrowserApi } from "../../browser/browser-api";
|
||||
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||
import BrowserClipboardService from "../browser-clipboard.service";
|
||||
|
||||
export abstract class BrowserPlatformUtilsService implements PlatformUtilsService {
|
||||
|
@ -15,6 +16,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||
private biometricCallback: () => Promise<boolean>,
|
||||
private globalContext: Window | ServiceWorkerGlobalScope,
|
||||
private offscreenDocumentService: OffscreenDocumentService,
|
||||
) {}
|
||||
|
||||
static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType {
|
||||
|
@ -316,24 +318,26 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||
* Triggers the offscreen document API to copy the text to the clipboard.
|
||||
*/
|
||||
private async triggerOffscreenCopyToClipboard(text: string) {
|
||||
await BrowserApi.createOffscreenDocument(
|
||||
await this.offscreenDocumentService.withDocument(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Write text to the clipboard.",
|
||||
async () => {
|
||||
await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text });
|
||||
},
|
||||
);
|
||||
await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text });
|
||||
BrowserApi.closeOffscreenDocument();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the offscreen document API to read the text from the clipboard.
|
||||
*/
|
||||
private async triggerOffscreenReadFromClipboard() {
|
||||
await BrowserApi.createOffscreenDocument(
|
||||
const response = await this.offscreenDocumentService.withDocument(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Read text from the clipboard.",
|
||||
async () => {
|
||||
return await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard");
|
||||
},
|
||||
);
|
||||
const response = await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard");
|
||||
BrowserApi.closeOffscreenDocument();
|
||||
if (typeof response === "string") {
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||
|
||||
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||
|
||||
export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||
|
@ -8,8 +10,9 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService
|
|||
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||
biometricCallback: () => Promise<boolean>,
|
||||
win: Window & typeof globalThis,
|
||||
offscreenDocumentService: OffscreenDocumentService,
|
||||
) {
|
||||
super(clipboardWriteCallback, biometricCallback, win);
|
||||
super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService);
|
||||
}
|
||||
|
||||
override showToast(
|
||||
|
|
|
@ -100,6 +100,8 @@ import { runInsideAngular } from "../../platform/browser/run-inside-angular.oper
|
|||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender";
|
||||
/* eslint-enable no-restricted-imports */
|
||||
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
|
||||
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
|
||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||
|
@ -287,9 +289,17 @@ const safeProviders: SafeProvider[] = [
|
|||
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OffscreenDocumentService,
|
||||
useClass: DefaultOffscreenDocumentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PlatformUtilsService,
|
||||
useFactory: (toastService: ToastService) => {
|
||||
useFactory: (
|
||||
toastService: ToastService,
|
||||
offscreenDocumentService: OffscreenDocumentService,
|
||||
) => {
|
||||
return new ForegroundPlatformUtilsService(
|
||||
toastService,
|
||||
(clipboardValue: string, clearMs: number) => {
|
||||
|
@ -306,9 +316,10 @@ const safeProviders: SafeProvider[] = [
|
|||
return response.result;
|
||||
},
|
||||
window,
|
||||
offscreenDocumentService,
|
||||
);
|
||||
},
|
||||
deps: [ToastService],
|
||||
deps: [ToastService, OffscreenDocumentService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
|
|
|
@ -611,7 +611,6 @@ export class Main {
|
|||
this.cipherService,
|
||||
this.folderService,
|
||||
this.collectionService,
|
||||
this.cryptoService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.searchService,
|
||||
|
|
|
@ -50,7 +50,12 @@
|
|||
</bit-tab>
|
||||
|
||||
<bit-tab label="{{ 'collections' | i18n }}">
|
||||
<p>{{ "editGroupCollectionsDesc" | i18n }}</p>
|
||||
<p>
|
||||
{{ "editGroupCollectionsDesc" | i18n }}
|
||||
<span *ngIf="!(allowAdminAccessToAllCollectionItems$ | async)">
|
||||
{{ "editGroupCollectionsRestrictionsDesc" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
<div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3">
|
||||
<input type="checkbox" formControlName="accessAll" id="accessAll" />
|
||||
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{
|
||||
|
|
|
@ -11,13 +11,13 @@ import {
|
|||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
|
@ -26,12 +26,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
|
||||
import { InternalGroupService as GroupService, GroupView } from "../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
|
@ -95,9 +93,15 @@ export const openGroupAddEditDialog = (
|
|||
templateUrl: "group-add-edit.component.html",
|
||||
})
|
||||
export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
protected flexibleCollectionsEnabled$ = this.organizationService
|
||||
private organization$ = this.organizationService
|
||||
.get$(this.organizationId)
|
||||
.pipe(map((o) => o?.flexibleCollections));
|
||||
.pipe(shareReplay({ refCount: true }));
|
||||
protected flexibleCollectionsEnabled$ = this.organization$.pipe(
|
||||
map((o) => o?.flexibleCollections),
|
||||
);
|
||||
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
);
|
||||
|
||||
protected PermissionMode = PermissionMode;
|
||||
protected ResultType = GroupAddEditDialogResultType;
|
||||
|
@ -131,27 +135,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private get orgCollections$() {
|
||||
return from(this.apiService.getCollections(this.organizationId)).pipe(
|
||||
switchMap((response) => {
|
||||
return from(
|
||||
this.collectionService.decryptMany(
|
||||
response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
map((collections) =>
|
||||
collections.map<AccessItemView>((c) => ({
|
||||
id: c.id,
|
||||
type: AccessItemType.Collection,
|
||||
labelName: c.name,
|
||||
listName: c.name,
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe(
|
||||
shareReplay({ refCount: false }),
|
||||
);
|
||||
|
||||
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
||||
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
|
||||
|
@ -197,23 +183,24 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
restrictGroupAccess$ = combineLatest([
|
||||
this.organizationService.get$(this.organizationId),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
this.groupDetails$,
|
||||
allowAdminAccessToAllCollectionItems$ = combineLatest([
|
||||
this.organization$,
|
||||
this.flexibleCollectionsV1Enabled$,
|
||||
]).pipe(
|
||||
map(
|
||||
([organization, flexibleCollectionsV1Enabled, group]) =>
|
||||
// Feature flag conditionals
|
||||
flexibleCollectionsV1Enabled &&
|
||||
organization.flexibleCollections &&
|
||||
// Business logic conditionals
|
||||
!organization.allowAdminAccessToAllCollectionItems &&
|
||||
group !== undefined,
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
map(([organization, flexibleCollectionsV1Enabled]) => {
|
||||
if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return organization.allowAdminAccessToAllCollectionItems;
|
||||
}),
|
||||
);
|
||||
|
||||
restrictGroupAccess$ = combineLatest([
|
||||
this.allowAdminAccessToAllCollectionItems$,
|
||||
this.groupDetails$,
|
||||
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
||||
|
@ -221,7 +208,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
private organizationUserService: OrganizationUserService,
|
||||
private groupService: GroupService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private formBuilder: FormBuilder,
|
||||
|
@ -230,6 +216,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
private organizationService: OrganizationService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||
}
|
||||
|
@ -244,48 +231,61 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
this.groupDetails$,
|
||||
this.restrictGroupAccess$,
|
||||
this.accountService.activeAccount$,
|
||||
this.organization$,
|
||||
this.flexibleCollectionsV1Enabled$,
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => {
|
||||
this.collections = collections;
|
||||
this.members = members;
|
||||
this.group = group;
|
||||
|
||||
if (this.group != undefined) {
|
||||
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||
// collections/members set above, otherwise no selected values will be patched below
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
||||
this.groupForm.patchValue({
|
||||
name: this.group.name,
|
||||
externalId: this.group.externalId,
|
||||
accessAll: this.group.accessAll,
|
||||
members: this.group.members.map((m) => ({
|
||||
id: m,
|
||||
type: AccessItemType.Member,
|
||||
})),
|
||||
collections: this.group.collections.map((gc) => ({
|
||||
id: gc.id,
|
||||
type: AccessItemType.Collection,
|
||||
permission: convertToPermission(gc),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// If the current user is not already in the group and cannot add themselves, remove them from the list
|
||||
if (restrictGroupAccess) {
|
||||
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
|
||||
const isAlreadyInGroup = this.groupForm.value.members.some(
|
||||
(m) => m.id === organizationUserId,
|
||||
.subscribe(
|
||||
([
|
||||
collections,
|
||||
members,
|
||||
group,
|
||||
restrictGroupAccess,
|
||||
activeAccount,
|
||||
organization,
|
||||
flexibleCollectionsV1Enabled,
|
||||
]) => {
|
||||
this.members = members;
|
||||
this.group = group;
|
||||
this.collections = mapToAccessItemViews(
|
||||
collections,
|
||||
organization,
|
||||
flexibleCollectionsV1Enabled,
|
||||
group,
|
||||
);
|
||||
|
||||
if (!isAlreadyInGroup) {
|
||||
this.members = this.members.filter((m) => m.id !== organizationUserId);
|
||||
}
|
||||
}
|
||||
if (this.group != undefined) {
|
||||
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||
// collections/members set above, otherwise no selected values will be patched below
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
this.groupForm.patchValue({
|
||||
name: this.group.name,
|
||||
externalId: this.group.externalId,
|
||||
accessAll: this.group.accessAll,
|
||||
members: this.group.members.map((m) => ({
|
||||
id: m,
|
||||
type: AccessItemType.Member,
|
||||
})),
|
||||
collections: mapToAccessSelections(group, this.collections),
|
||||
});
|
||||
}
|
||||
|
||||
// If the current user is not already in the group and cannot add themselves, remove them from the list
|
||||
if (restrictGroupAccess) {
|
||||
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
|
||||
const isAlreadyInGroup = this.groupForm.value.members.some(
|
||||
(m) => m.id === organizationUserId,
|
||||
);
|
||||
|
||||
if (!isAlreadyInGroup) {
|
||||
this.members = this.members.filter((m) => m.id !== organizationUserId);
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -355,3 +355,46 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
this.dialogRef.close(GroupAddEditDialogResultType.Deleted);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the group's current collection access to AccessItemValues to populate the access-selector's FormControl
|
||||
*/
|
||||
function mapToAccessSelections(group: GroupView, items: AccessItemView[]): AccessItemValue[] {
|
||||
return (
|
||||
group.collections
|
||||
// The FormControl value only represents editable collection access - exclude readonly access selections
|
||||
.filter((selection) => !items.find((item) => item.id == selection.id).readonly)
|
||||
.map((gc) => ({
|
||||
id: gc.id,
|
||||
type: AccessItemType.Collection,
|
||||
permission: convertToPermission(gc),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the organization's collections to AccessItemViews to populate the access-selector's multi-select
|
||||
*/
|
||||
function mapToAccessItemViews(
|
||||
collections: CollectionAdminView[],
|
||||
organization: Organization,
|
||||
flexibleCollectionsV1Enabled: boolean,
|
||||
group?: GroupView,
|
||||
): AccessItemView[] {
|
||||
return (
|
||||
collections
|
||||
.map<AccessItemView>((c) => {
|
||||
const accessSelection = group?.collections.find((access) => access.id == c.id) ?? undefined;
|
||||
return {
|
||||
id: c.id,
|
||||
type: AccessItemType.Collection,
|
||||
labelName: c.name,
|
||||
listName: c.name,
|
||||
readonly: !c.canEditGroupAccess(organization, flexibleCollectionsV1Enabled),
|
||||
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
|
||||
};
|
||||
})
|
||||
// Remove any collection views that are not already assigned and that we don't have permissions to assign access to
|
||||
.filter((item) => !item.readonly || group?.collections.some((access) => access.id == item.id))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,13 @@ export class CollectionAdminView extends CollectionView {
|
|||
* Whether the user can modify user access to this collection
|
||||
*/
|
||||
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.canManageUsers;
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify group access to this collection
|
||||
*/
|
||||
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6480,6 +6480,9 @@
|
|||
"editGroupCollectionsDesc": {
|
||||
"message": "Grant access to collections by adding them to this group."
|
||||
},
|
||||
"editGroupCollectionsRestrictionsDesc": {
|
||||
"message": "You can only assign collections you manage."
|
||||
},
|
||||
"accessAllCollectionsDesc": {
|
||||
"message": "Grant access to all current and future collections."
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
@ -42,6 +42,7 @@ export class AccountComponent {
|
|||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -93,9 +94,8 @@ export class AccountComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.formPromise = this.providerApiService.deleteProvider(this.providerId);
|
||||
try {
|
||||
await this.formPromise;
|
||||
await this.providerApiService.deleteProvider(this.providerId);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("providerDeleted"),
|
||||
|
@ -104,7 +104,8 @@ export class AccountComponent {
|
|||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.formPromise = null;
|
||||
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
|
||||
private async verifyUser(): Promise<boolean> {
|
||||
|
|
|
@ -656,7 +656,6 @@ const safeProviders: SafeProvider[] = [
|
|||
CipherServiceAbstraction,
|
||||
FolderServiceAbstraction,
|
||||
CollectionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
|
|
|
@ -296,10 +296,6 @@ export abstract class CryptoService {
|
|||
kdfConfig: KdfConfig,
|
||||
oldPinKey: EncString,
|
||||
): Promise<UserKey>;
|
||||
/**
|
||||
* Replaces old master auto keys with new user auto keys
|
||||
*/
|
||||
abstract migrateAutoKeyIfNeeded(userId?: string): Promise<void>;
|
||||
/**
|
||||
* @param keyMaterial The key material to derive the send key from
|
||||
* @returns A new send key
|
||||
|
|
|
@ -82,10 +82,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
|
||||
*/
|
||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use getUserKeyAuto instead
|
||||
*/
|
||||
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use setUserKeyAuto instead
|
||||
*/
|
||||
|
|
|
@ -930,35 +930,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
}
|
||||
}
|
||||
|
||||
async migrateAutoKeyIfNeeded(userId?: UserId) {
|
||||
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
|
||||
if (!oldAutoKey) {
|
||||
return;
|
||||
}
|
||||
// Decrypt
|
||||
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey;
|
||||
if (await this.isLegacyUser(masterKey, userId)) {
|
||||
// Legacy users don't have a user key, so no need to migrate.
|
||||
// Instead, set the master key for additional isLegacyUser checks that will log the user out.
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
return;
|
||||
}
|
||||
const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
||||
userId: userId,
|
||||
});
|
||||
const userKey = await this.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
new EncString(encryptedUserKey),
|
||||
userId,
|
||||
);
|
||||
// Migrate
|
||||
await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
// Set encrypted user key in case user immediately locks without syncing
|
||||
await this.setMasterKeyEncryptedUserKey(encryptedUserKey);
|
||||
}
|
||||
|
||||
async decryptAndMigrateOldPinKey(
|
||||
masterPasswordOnRestart: boolean,
|
||||
pin: string,
|
||||
|
|
|
@ -268,23 +268,6 @@ export class StateService<
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UserKeyAuto instead
|
||||
*/
|
||||
async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(
|
||||
this.reconcileOptions(options, { keySuffix: "auto" }),
|
||||
await this.defaultSecureStorageOptions(),
|
||||
);
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}${partialKeys.autoKey}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UserKeyAuto instead
|
||||
*/
|
||||
|
|
|
@ -9,7 +9,6 @@ import { AuthService } from "../../auth/abstractions/auth.service";
|
|||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
|
||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
|
@ -28,7 +27,6 @@ describe("VaultTimeoutService", () => {
|
|||
let cipherService: MockProxy<CipherService>;
|
||||
let folderService: MockProxy<FolderService>;
|
||||
let collectionService: MockProxy<CollectionService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let searchService: MockProxy<SearchService>;
|
||||
|
@ -52,7 +50,6 @@ describe("VaultTimeoutService", () => {
|
|||
cipherService = mock();
|
||||
folderService = mock();
|
||||
collectionService = mock();
|
||||
cryptoService = mock();
|
||||
platformUtilsService = mock();
|
||||
messagingService = mock();
|
||||
searchService = mock();
|
||||
|
@ -76,7 +73,6 @@ describe("VaultTimeoutService", () => {
|
|||
cipherService,
|
||||
folderService,
|
||||
collectionService,
|
||||
cryptoService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
searchService,
|
||||
|
|
|
@ -7,9 +7,7 @@ import { AccountService } from "../../auth/abstractions/account.service";
|
|||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { ClientType } from "../../enums";
|
||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
|
@ -28,7 +26,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
private collectionService: CollectionService,
|
||||
private cryptoService: CryptoService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private messagingService: MessagingService,
|
||||
private searchService: SearchService,
|
||||
|
@ -44,8 +41,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483)
|
||||
await this.migrateKeyForNeverLockIfNeeded();
|
||||
|
||||
this.inited = true;
|
||||
if (checkOnInterval) {
|
||||
|
@ -175,21 +170,4 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||
? await this.logOut(userId)
|
||||
: await this.lock(userId);
|
||||
}
|
||||
|
||||
private async migrateKeyForNeverLockIfNeeded(): Promise<void> {
|
||||
// Web can't set vault timeout to never
|
||||
if (this.platformUtilsService.getClientType() == ClientType.Web) {
|
||||
return;
|
||||
}
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
for (const userId in accounts) {
|
||||
if (userId != null) {
|
||||
await this.cryptoService.migrateAutoKeyIfNeeded(userId);
|
||||
// Legacy users should be logged out since we're not on the web vault and can't migrate.
|
||||
if (await this.cryptoService.isLegacyUser(null, userId)) {
|
||||
await this.logOut(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue