Merge branch 'main' of https://github.com/bitwarden/clients into PM-7843-two-factor-verification-is-empty-on-organization-duo-2-fa

This commit is contained in:
KiruthigaManivannan 2024-05-03 10:44:07 +05:30
commit 176ff91736
42 changed files with 843 additions and 218 deletions

1
.github/CODEOWNERS vendored
View File

@ -57,6 +57,7 @@ libs/common/src/admin-console @bitwarden/team-admin-console-dev
libs/admin-console @bitwarden/team-admin-console-dev libs/admin-console @bitwarden/team-admin-console-dev
## Billing team files ## ## Billing team files ##
apps/browser/src/billing @bitwarden/team-billing-dev
apps/web/src/app/billing @bitwarden/team-billing-dev apps/web/src/app/billing @bitwarden/team-billing-dev
libs/angular/src/billing @bitwarden/team-billing-dev libs/angular/src/billing @bitwarden/team-billing-dev
libs/common/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev

View File

@ -12,6 +12,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { import {
@ -74,9 +75,10 @@ describe("AutofillService", () => {
const logService = mock<LogService>(); const logService = mock<LogService>();
const userVerificationService = mock<UserVerificationService>(); const userVerificationService = mock<UserVerificationService>();
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
const platformUtilsService = mock<PlatformUtilsService>();
beforeEach(() => { beforeEach(() => {
scriptInjectorService = new BrowserScriptInjectorService(); scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
autofillService = new AutofillService( autofillService = new AutofillService(
cipherService, cipherService,
autofillSettingsService, autofillSettingsService,

View File

@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
@ -107,17 +106,13 @@ export default class AutofillService implements AutofillServiceInterface {
frameId = 0, frameId = 0,
triggeringOnPageLoad = true, triggeringOnPageLoad = true,
): Promise<void> { ): Promise<void> {
// Autofill settings loaded from state can await the active account state indefinitely if // Autofill user settings loaded from state can await the active account state indefinitely
// not guarded by an active account check (e.g. the user is logged in) // if not guarded by an active account check (e.g. the user is logged in)
const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
// These settings are not available until the user logs in
let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
let autoFillOnPageLoadIsEnabled = false; let autoFillOnPageLoadIsEnabled = false;
const overlayVisibility = await this.getOverlayVisibility();
if (activeAccount) {
overlayVisibility = await this.getOverlayVisibility();
}
const mainAutofillScript = overlayVisibility const mainAutofillScript = overlayVisibility
? "bootstrap-autofill-overlay.js" ? "bootstrap-autofill-overlay.js"
: "bootstrap-autofill.js"; : "bootstrap-autofill.js";
@ -2087,9 +2082,7 @@ export default class AutofillService implements AutofillServiceInterface {
for (let index = 0; index < tabs.length; index++) { for (let index = 0; index < tabs.length; index++) {
const tab = tabs[index]; const tab = tabs[index];
if (tab.url?.startsWith("http")) { if (tab.url?.startsWith("http")) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.injectAutofillScripts(tab, 0, false);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.injectAutofillScripts(tab, 0, false);
} }
} }
} }

View File

@ -106,7 +106,6 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@ -219,6 +218,7 @@ import { BrowserCryptoService } from "../platform/services/browser-crypto.servic
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
import { BrowserMultithreadEncryptServiceImplementation } from "../platform/services/browser-multithread-encrypt.service.implementation";
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
import I18nService from "../platform/services/i18n.service"; import I18nService from "../platform/services/i18n.service";
@ -475,14 +475,14 @@ export default class MainBackground {
storageServiceProvider, storageServiceProvider,
); );
this.encryptService = this.encryptService = flagEnabled("multithreadDecryption")
flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2) ? new BrowserMultithreadEncryptServiceImplementation(
? new MultithreadEncryptServiceImplementation( this.cryptoFunctionService,
this.cryptoFunctionService, this.logService,
this.logService, true,
true, this.offscreenDocumentService,
) )
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
this.singleUserStateProvider = new DefaultSingleUserStateProvider( this.singleUserStateProvider = new DefaultSingleUserStateProvider(
storageServiceProvider, storageServiceProvider,
@ -813,7 +813,10 @@ export default class MainBackground {
); );
this.totpService = new TotpService(this.cryptoFunctionService, this.logService); this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
this.scriptInjectorService = new BrowserScriptInjectorService(); this.scriptInjectorService = new BrowserScriptInjectorService(
this.platformUtilsService,
this.logService,
);
this.autofillService = new AutofillService( this.autofillService = new AutofillService(
this.cipherService, this.cipherService,
this.autofillSettingsService, this.autofillSettingsService,

View File

@ -51,7 +51,7 @@
"default_popup": "popup/index.html" "default_popup": "popup/index.html"
}, },
"permissions": [ "permissions": [
"<all_urls>", "activeTab",
"tabs", "tabs",
"contextMenus", "contextMenus",
"storage", "storage",
@ -65,7 +65,7 @@
"webRequestAuthProvider" "webRequestAuthProvider"
], ],
"optional_permissions": ["nativeMessaging", "privacy"], "optional_permissions": ["nativeMessaging", "privacy"],
"host_permissions": ["<all_urls>"], "host_permissions": ["https://*/*", "http://*/*"],
"content_security_policy": { "content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
"sandbox": "sandbox allow-scripts; script-src 'self'" "sandbox": "sandbox allow-scripts; script-src 'self'"

View File

@ -1,10 +1,20 @@
import {
LogServiceInitOptions,
logServiceFactory,
} from "../../background/service-factories/log-service.factory";
import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service"; import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service";
import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
} from "./platform-utils-service.factory";
type BrowserScriptInjectorServiceOptions = FactoryOptions; type BrowserScriptInjectorServiceOptions = FactoryOptions;
export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions; export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions &
PlatformUtilsServiceInitOptions &
LogServiceInitOptions;
export function browserScriptInjectorServiceFactory( export function browserScriptInjectorServiceFactory(
cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices, cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices,
@ -14,6 +24,10 @@ export function browserScriptInjectorServiceFactory(
cache, cache,
"browserScriptInjectorService", "browserScriptInjectorService",
opts, opts,
async () => new BrowserScriptInjectorService(), async () =>
new BrowserScriptInjectorService(
await platformUtilsServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
),
); );
} }

View File

@ -2,6 +2,7 @@ export type OffscreenDocumentExtensionMessage = {
[key: string]: any; [key: string]: any;
command: string; command: string;
text?: string; text?: string;
decryptRequest?: string;
}; };
type OffscreenExtensionMessageEventParams = { type OffscreenExtensionMessageEventParams = {
@ -13,6 +14,7 @@ export type OffscreenDocumentExtensionMessageHandlers = {
[key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any; [key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any;
offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any; offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any;
offscreenReadFromClipboard: () => any; offscreenReadFromClipboard: () => any;
offscreenDecryptItems: ({ message }: OffscreenExtensionMessageEventParams) => Promise<string>;
}; };
export interface OffscreenDocument { export interface OffscreenDocument {

View File

@ -1,7 +1,25 @@
import { mock } from "jest-mock-extended";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { flushPromises, sendExtensionRuntimeMessage } from "../../autofill/spec/testing-utils"; import { flushPromises, sendExtensionRuntimeMessage } from "../../autofill/spec/testing-utils";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
import BrowserClipboardService from "../services/browser-clipboard.service"; import BrowserClipboardService from "../services/browser-clipboard.service";
jest.mock(
"@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation",
() => ({
MultithreadEncryptServiceImplementation: class MultithreadEncryptServiceImplementation {
getDecryptedItemsFromWorker = async <T extends InitializerMetadata>(
items: Decryptable<T>[],
_key: SymmetricCryptoKey,
): Promise<string> => JSON.stringify(items);
},
}),
);
describe("OffscreenDocument", () => { describe("OffscreenDocument", () => {
const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener"); const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
@ -60,5 +78,37 @@ describe("OffscreenDocument", () => {
expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window); expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window);
}); });
}); });
describe("handleOffscreenDecryptItems", () => {
it("returns an empty array as a string if the decrypt request is not present in the message", async () => {
let response: string | undefined;
sendExtensionRuntimeMessage(
{ command: "offscreenDecryptItems" },
mock<chrome.runtime.MessageSender>(),
(res: string) => (response = res),
);
await flushPromises();
expect(response).toBe("[]");
});
it("decrypts the items and sends back the response as a string", async () => {
const items = [{ id: "test" }];
const key = { id: "test" };
const decryptRequest = JSON.stringify({ items, key });
let response: string | undefined;
sendExtensionRuntimeMessage(
{ command: "offscreenDecryptItems", decryptRequest },
mock<chrome.runtime.MessageSender>(),
(res: string) => {
response = res;
},
);
await flushPromises();
expect(response).toBe(JSON.stringify(items));
});
});
}); });
}); });

View File

@ -1,21 +1,35 @@
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
import BrowserClipboardService from "../services/browser-clipboard.service"; import BrowserClipboardService from "../services/browser-clipboard.service";
import { import {
OffscreenDocument as OffscreenDocumentInterface,
OffscreenDocumentExtensionMessage, OffscreenDocumentExtensionMessage,
OffscreenDocumentExtensionMessageHandlers, OffscreenDocumentExtensionMessageHandlers,
OffscreenDocument as OffscreenDocumentInterface,
} from "./abstractions/offscreen-document"; } from "./abstractions/offscreen-document";
class OffscreenDocument implements OffscreenDocumentInterface { class OffscreenDocument implements OffscreenDocumentInterface {
private consoleLogService: ConsoleLogService = new ConsoleLogService(false); private readonly consoleLogService: ConsoleLogService;
private encryptService: MultithreadEncryptServiceImplementation;
private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = { private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = {
offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message), offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message),
offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(), offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(),
offscreenDecryptItems: ({ message }) => this.handleOffscreenDecryptItems(message),
}; };
constructor() {
const cryptoFunctionService = new WebCryptoFunctionService(self);
this.consoleLogService = new ConsoleLogService(false);
this.encryptService = new MultithreadEncryptServiceImplementation(
cryptoFunctionService,
this.consoleLogService,
true,
);
}
/** /**
* Initializes the offscreen document extension. * Initializes the offscreen document extension.
*/ */
@ -39,6 +53,23 @@ class OffscreenDocument implements OffscreenDocumentInterface {
return await BrowserClipboardService.read(self); return await BrowserClipboardService.read(self);
} }
/**
* Decrypts the items in the message using the encrypt service.
*
* @param message - The extension message containing the items to decrypt
*/
private async handleOffscreenDecryptItems(
message: OffscreenDocumentExtensionMessage,
): Promise<string> {
const { decryptRequest } = message;
if (!decryptRequest) {
return "[]";
}
const request = JSON.parse(decryptRequest);
return await this.encryptService.getDecryptedItemsFromWorker(request.items, request.key);
}
/** /**
* Sets up the listener for extension messages. * Sets up the listener for extension messages.
*/ */

View File

@ -0,0 +1,97 @@
import { mock, MockProxy } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { InitializerKey } from "@bitwarden/common/platform/services/cryptography/initializer-key";
import { makeStaticByteArray } from "@bitwarden/common/spec";
import { BrowserApi } from "../browser/browser-api";
import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document";
import { BrowserMultithreadEncryptServiceImplementation } from "./browser-multithread-encrypt.service.implementation";
describe("BrowserMultithreadEncryptServiceImplementation", () => {
let cryptoFunctionServiceMock: MockProxy<CryptoFunctionService>;
let logServiceMock: MockProxy<LogService>;
let offscreenDocumentServiceMock: MockProxy<OffscreenDocumentService>;
let encryptService: BrowserMultithreadEncryptServiceImplementation;
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
const sendMessageWithResponseSpy = jest.spyOn(BrowserApi, "sendMessageWithResponse");
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
const items: Decryptable<InitializerMetadata>[] = [
{
decrypt: jest.fn(),
initializerKey: InitializerKey.Cipher,
},
];
beforeEach(() => {
cryptoFunctionServiceMock = mock<CryptoFunctionService>();
logServiceMock = mock<LogService>();
offscreenDocumentServiceMock = mock<OffscreenDocumentService>({
withDocument: jest.fn((_, __, callback) => callback() as any),
});
encryptService = new BrowserMultithreadEncryptServiceImplementation(
cryptoFunctionServiceMock,
logServiceMock,
false,
offscreenDocumentServiceMock,
);
manifestVersionSpy.mockReturnValue(3);
sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify([]));
});
afterEach(() => {
jest.clearAllMocks();
});
it("decrypts items using web workers if the chrome.offscreen API is not supported", async () => {
manifestVersionSpy.mockReturnValue(2);
await encryptService.decryptItems([], key);
expect(offscreenDocumentServiceMock.withDocument).not.toHaveBeenCalled();
});
it("decrypts items using the chrome.offscreen API if it is supported", async () => {
sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify(items));
await encryptService.decryptItems(items, key);
expect(offscreenDocumentServiceMock.withDocument).toHaveBeenCalledWith(
[chrome.offscreen.Reason.WORKERS],
"Use web worker to decrypt items.",
expect.any(Function),
);
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenDecryptItems", {
decryptRequest: expect.any(String),
});
});
it("returns an empty array if the passed items are not defined", async () => {
const result = await encryptService.decryptItems(null, key);
expect(result).toEqual([]);
});
it("returns an empty array if the offscreen document message returns an empty value", async () => {
sendMessageWithResponseSpy.mockResolvedValue("");
const result = await encryptService.decryptItems(items, key);
expect(result).toEqual([]);
});
it("returns an empty array if the offscreen document message returns an empty array", async () => {
sendMessageWithResponseSpy.mockResolvedValue("[]");
const result = await encryptService.decryptItems(items, key);
expect(result).toEqual([]);
});
});

View File

@ -0,0 +1,91 @@
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { BrowserApi } from "../browser/browser-api";
import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document";
export class BrowserMultithreadEncryptServiceImplementation extends MultithreadEncryptServiceImplementation {
constructor(
cryptoFunctionService: CryptoFunctionService,
logService: LogService,
logMacFailures: boolean,
private offscreenDocumentService: OffscreenDocumentService,
) {
super(cryptoFunctionService, logService, logMacFailures);
}
/**
* Handles decryption of items, will use the offscreen document if supported.
*
* @param items - The items to decrypt.
* @param key - The key to use for decryption.
*/
async decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (!this.isOffscreenDocumentSupported()) {
return await super.decryptItems(items, key);
}
return await this.decryptItemsInOffscreenDocument(items, key);
}
/**
* Decrypts items using the offscreen document api.
*
* @param items - The items to decrypt.
* @param key - The key to use for decryption.
*/
private async decryptItemsInOffscreenDocument<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (items == null || items.length < 1) {
return [];
}
const request = {
id: Utils.newGuid(),
items: items,
key: key,
};
const response = await this.offscreenDocumentService.withDocument(
[chrome.offscreen.Reason.WORKERS],
"Use web worker to decrypt items.",
async () => {
return (await BrowserApi.sendMessageWithResponse("offscreenDecryptItems", {
decryptRequest: JSON.stringify(request),
})) as string;
},
);
if (!response) {
return [];
}
const responseItems = JSON.parse(response);
if (responseItems?.length < 1) {
return [];
}
return this.initializeItems(responseItems);
}
/**
* Checks if the offscreen document api is supported.
*/
private isOffscreenDocumentSupported() {
return (
BrowserApi.isManifestVersion(3) &&
typeof chrome !== "undefined" &&
typeof chrome.offscreen !== "undefined"
);
}
}

View File

@ -1,3 +1,8 @@
import { mock } from "jest-mock-extended";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
import { import {
@ -20,9 +25,11 @@ describe("ScriptInjectorService", () => {
let scriptInjectorService: BrowserScriptInjectorService; let scriptInjectorService: BrowserScriptInjectorService;
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
jest.spyOn(BrowserApi, "isManifestVersion"); jest.spyOn(BrowserApi, "isManifestVersion");
const platformUtilsService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
beforeEach(() => { beforeEach(() => {
scriptInjectorService = new BrowserScriptInjectorService(); scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
}); });
describe("inject", () => { describe("inject", () => {

View File

@ -1,3 +1,6 @@
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
import { import {
@ -7,6 +10,13 @@ import {
} from "./abstractions/script-injector.service"; } from "./abstractions/script-injector.service";
export class BrowserScriptInjectorService extends ScriptInjectorService { export class BrowserScriptInjectorService extends ScriptInjectorService {
constructor(
private readonly platformUtilsService: PlatformUtilsService,
private readonly logService: LogService,
) {
super();
}
/** /**
* Facilitates the injection of a script into a tab context. Will adjust * Facilitates the injection of a script into a tab context. Will adjust
* behavior between manifest v2 and v3 based on the passed configuration. * behavior between manifest v2 and v3 based on the passed configuration.
@ -23,9 +33,26 @@ export class BrowserScriptInjectorService extends ScriptInjectorService {
const injectionDetails = this.buildInjectionDetails(injectDetails, file); const injectionDetails = this.buildInjectionDetails(injectDetails, file);
if (BrowserApi.isManifestVersion(3)) { if (BrowserApi.isManifestVersion(3)) {
await BrowserApi.executeScriptInTab(tabId, injectionDetails, { try {
world: mv3Details?.world ?? "ISOLATED", await BrowserApi.executeScriptInTab(tabId, injectionDetails, {
}); world: mv3Details?.world ?? "ISOLATED",
});
} catch (error) {
// Swallow errors for host permissions, since this is believed to be a Manifest V3 Chrome bug
// @TODO remove when the bugged behaviour is resolved
if (
error.message !==
"Cannot access contents of the page. Extension manifest must request permission to access the respective host."
) {
throw error;
}
if (this.platformUtilsService.isDev()) {
this.logService.warning(
`BrowserApi.executeScriptInTab exception for ${injectDetails.file} in tab ${tabId}: ${error.message}`,
);
}
}
return; return;
} }

View File

@ -26,6 +26,7 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
@ -51,7 +52,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"
import { FoldersComponent } from "./settings/folders.component"; import { FoldersComponent } from "./settings/folders.component";
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
import { OptionsComponent } from "./settings/options.component"; import { OptionsComponent } from "./settings/options.component";
import { PremiumComponent } from "./settings/premium.component";
import { SettingsComponent } from "./settings/settings.component"; import { SettingsComponent } from "./settings/settings.component";
import { SyncComponent } from "./settings/sync.component"; import { SyncComponent } from "./settings/sync.component";
import { TabsComponent } from "./tabs.component"; import { TabsComponent } from "./tabs.component";

View File

@ -35,6 +35,7 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component";
import { HeaderComponent } from "../platform/popup/header.component"; import { HeaderComponent } from "../platform/popup/header.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
@ -76,7 +77,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"
import { FoldersComponent } from "./settings/folders.component"; import { FoldersComponent } from "./settings/folders.component";
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
import { OptionsComponent } from "./settings/options.component"; import { OptionsComponent } from "./settings/options.component";
import { PremiumComponent } from "./settings/premium.component";
import { SettingsComponent } from "./settings/settings.component"; import { SettingsComponent } from "./settings/settings.component";
import { SyncComponent } from "./settings/sync.component"; import { SyncComponent } from "./settings/sync.component";
import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component";

View File

@ -1,22 +0,0 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service";
export class PopupSearchService extends SearchService {
constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) {
super(logService, i18nService, stateProvider);
}
clearIndex(): Promise<void> {
throw new Error("Not available.");
}
indexCiphers(): Promise<void> {
throw new Error("Not available.");
}
async getIndexForSearch() {
return await super.getIndexForSearch();
}
}

View File

@ -19,7 +19,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -125,7 +124,6 @@ import { VaultFilterService } from "../../vault/services/vault-filter.service";
import { DebounceNavigationService } from "./debounce-navigation.service"; import { DebounceNavigationService } from "./debounce-navigation.service";
import { InitService } from "./init.service"; import { InitService } from "./init.service";
import { PopupCloseWarningService } from "./popup-close-warning.service"; import { PopupCloseWarningService } from "./popup-close-warning.service";
import { PopupSearchService } from "./popup-search.service";
const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService AbstractStorageService & ObservableStorageService
@ -182,26 +180,11 @@ const safeProviders: SafeProvider[] = [
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"), useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
deps: [], deps: [],
}), }),
safeProvider({
provide: SearchServiceAbstraction,
useClass: PopupSearchService,
deps: [LogService, I18nServiceAbstraction, StateProvider],
}),
safeProvider({
provide: CipherService,
useFactory: getBgService<CipherService>("cipherService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: CryptoFunctionService, provide: CryptoFunctionService,
useFactory: () => new WebCryptoFunctionService(window), useFactory: () => new WebCryptoFunctionService(window),
deps: [], deps: [],
}), }),
safeProvider({
provide: CollectionService,
useFactory: getBgService<CollectionService>("collectionService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: LogService, provide: LogService,
useFactory: (platformUtilsService: PlatformUtilsService) => useFactory: (platformUtilsService: PlatformUtilsService) =>
@ -363,7 +346,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: ScriptInjectorService, provide: ScriptInjectorService,
useClass: BrowserScriptInjectorService, useClass: BrowserScriptInjectorService,
deps: [], deps: [PlatformUtilsService, LogService],
}), }),
safeProvider({ safeProvider({
provide: KeyConnectorService, provide: KeyConnectorService,

View File

@ -5,6 +5,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core";
@ -38,10 +40,12 @@ describe("FilelessImporterBackground ", () => {
const notificationBackground = mock<NotificationBackground>(); const notificationBackground = mock<NotificationBackground>();
const importService = mock<ImportServiceAbstraction>(); const importService = mock<ImportServiceAbstraction>();
const syncService = mock<SyncService>(); const syncService = mock<SyncService>();
const platformUtilsService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
let scriptInjectorService: BrowserScriptInjectorService; let scriptInjectorService: BrowserScriptInjectorService;
beforeEach(() => { beforeEach(() => {
scriptInjectorService = new BrowserScriptInjectorService(); scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
filelessImporterBackground = new FilelessImporterBackground( filelessImporterBackground = new FilelessImporterBackground(
configService, configService,
authService, authService,

View File

@ -70,13 +70,13 @@ export class Fido2Background implements Fido2BackgroundInterface {
*/ */
async injectFido2ContentScriptsInAllTabs() { async injectFido2ContentScriptsInAllTabs() {
const tabs = await BrowserApi.tabsQuery({}); const tabs = await BrowserApi.tabsQuery({});
for (let index = 0; index < tabs.length; index++) { for (let index = 0; index < tabs.length; index++) {
const tab = tabs[index]; const tab = tabs[index];
if (!tab.url?.startsWith("https")) {
continue;
}
void this.injectFido2ContentScripts(tab); if (tab.url?.startsWith("https")) {
void this.injectFido2ContentScripts(tab);
}
} }
} }

View File

@ -15,17 +15,43 @@ jest.mock("../../../autofill/utils", () => ({
}), }),
})); }));
const originalGlobalThis = globalThis;
const mockGlobalThisDocument = {
...originalGlobalThis.document,
contentType: "text/html",
location: {
...originalGlobalThis.document.location,
href: "https://localhost",
origin: "https://localhost",
protocol: "https:",
},
};
describe("Fido2 Content Script", () => { describe("Fido2 Content Script", () => {
beforeAll(() => {
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(
() => mockGlobalThisDocument,
);
});
afterEach(() => {
jest.resetModules();
});
afterAll(() => {
jest.clearAllMocks();
});
let messenger: Messenger; let messenger: Messenger;
const messengerForDOMCommunicationSpy = jest const messengerForDOMCommunicationSpy = jest
.spyOn(Messenger, "forDOMCommunication") .spyOn(Messenger, "forDOMCommunication")
.mockImplementation((window) => { .mockImplementation((context) => {
const windowOrigin = window.location.origin; const windowOrigin = context.location.origin;
messenger = new Messenger({ messenger = new Messenger({
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]),
addEventListener: (listener) => window.addEventListener("message", listener), addEventListener: (listener) => context.addEventListener("message", listener),
removeEventListener: (listener) => window.removeEventListener("message", listener), removeEventListener: (listener) => context.removeEventListener("message", listener),
}); });
messenger.destroy = jest.fn(); messenger.destroy = jest.fn();
return messenger; return messenger;
@ -33,16 +59,6 @@ describe("Fido2 Content Script", () => {
const portSpy: MockProxy<chrome.runtime.Port> = createPortSpyMock(Fido2PortName.InjectedScript); const portSpy: MockProxy<chrome.runtime.Port> = createPortSpyMock(Fido2PortName.InjectedScript);
chrome.runtime.connect = jest.fn(() => portSpy); chrome.runtime.connect = jest.fn(() => portSpy);
afterEach(() => {
Object.defineProperty(document, "contentType", {
value: "text/html",
writable: true,
});
jest.clearAllMocks();
jest.resetModules();
});
it("destroys the messenger when the port is disconnected", () => { it("destroys the messenger when the port is disconnected", () => {
require("./content-script"); require("./content-script");
@ -151,11 +167,31 @@ describe("Fido2 Content Script", () => {
await expect(result).rejects.toEqual(errorMessage); await expect(result).rejects.toEqual(errorMessage);
}); });
it("skips initializing the content script if the document content type is not 'text/html'", () => { it("skips initializing if the document content type is not 'text/html'", () => {
Object.defineProperty(document, "contentType", { jest.clearAllMocks();
value: "application/json",
writable: true, (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
}); ...mockGlobalThisDocument,
contentType: "application/json",
}));
require("./content-script");
expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled();
});
it("skips initializing if the document location protocol is not 'https'", () => {
jest.clearAllMocks();
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
...mockGlobalThisDocument,
location: {
...mockGlobalThisDocument.location,
href: "http://localhost",
origin: "http://localhost",
protocol: "http:",
},
}));
require("./content-script"); require("./content-script");

View File

@ -15,7 +15,11 @@ import {
import { MessageWithMetadata, Messenger } from "./messaging/messenger"; import { MessageWithMetadata, Messenger } from "./messaging/messenger";
(function (globalContext) { (function (globalContext) {
if (globalContext.document.contentType !== "text/html") { const shouldExecuteContentScript =
globalContext.document.contentType === "text/html" &&
globalContext.document.location.protocol === "https:";
if (!shouldExecuteContentScript) {
return; return;
} }

View File

@ -6,9 +6,14 @@ import { MessageType } from "./messaging/message";
import { Messenger } from "./messaging/messenger"; import { Messenger } from "./messaging/messenger";
(function (globalContext) { (function (globalContext) {
if (globalContext.document.contentType !== "text/html") { const shouldExecuteContentScript =
globalContext.document.contentType === "text/html" &&
globalContext.document.location.protocol === "https:";
if (!shouldExecuteContentScript) {
return; return;
} }
const BrowserPublicKeyCredential = globalContext.PublicKeyCredential; const BrowserPublicKeyCredential = globalContext.PublicKeyCredential;
const BrowserNavigatorCredentials = navigator.credentials; const BrowserNavigatorCredentials = navigator.credentials;
const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse; const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse;

View File

@ -10,17 +10,29 @@ import { WebauthnUtils } from "../webauthn-utils";
import { MessageType } from "./messaging/message"; import { MessageType } from "./messaging/message";
import { Messenger } from "./messaging/messenger"; import { Messenger } from "./messaging/messenger";
const originalGlobalThis = globalThis;
const mockGlobalThisDocument = {
...originalGlobalThis.document,
contentType: "text/html",
location: {
...originalGlobalThis.document.location,
href: "https://localhost",
origin: "https://localhost",
protocol: "https:",
},
};
let messenger: Messenger; let messenger: Messenger;
jest.mock("./messaging/messenger", () => { jest.mock("./messaging/messenger", () => {
return { return {
Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { Messenger: class extends jest.requireActual("./messaging/messenger").Messenger {
static forDOMCommunication: any = jest.fn((window) => { static forDOMCommunication: any = jest.fn((context) => {
const windowOrigin = window.location.origin; const windowOrigin = context.location.origin;
messenger = new Messenger({ messenger = new Messenger({
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]),
addEventListener: (listener) => window.addEventListener("message", listener), addEventListener: (listener) => context.addEventListener("message", listener),
removeEventListener: (listener) => window.removeEventListener("message", listener), removeEventListener: (listener) => context.removeEventListener("message", listener),
}); });
messenger.destroy = jest.fn(); messenger.destroy = jest.fn();
return messenger; return messenger;
@ -31,6 +43,10 @@ jest.mock("./messaging/messenger", () => {
jest.mock("../webauthn-utils"); jest.mock("../webauthn-utils");
describe("Fido2 page script with native WebAuthn support", () => { describe("Fido2 page script with native WebAuthn support", () => {
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(
() => mockGlobalThisDocument,
);
const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); const mockCredentialCreationOptions = createCredentialCreationOptionsMock();
const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCreateCredentialsResult = createCreateCredentialResultMock();
const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
@ -39,9 +55,12 @@ describe("Fido2 page script with native WebAuthn support", () => {
require("./page-script"); require("./page-script");
afterEach(() => {
jest.resetModules();
});
afterAll(() => { afterAll(() => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.resetModules();
}); });
describe("creating WebAuthn credentials", () => { describe("creating WebAuthn credentials", () => {
@ -118,4 +137,42 @@ describe("Fido2 page script with native WebAuthn support", () => {
expect(messenger.destroy).toHaveBeenCalled(); expect(messenger.destroy).toHaveBeenCalled();
}); });
}); });
describe("content script execution", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
it("skips initializing if the document content type is not 'text/html'", () => {
jest.spyOn(Messenger, "forDOMCommunication");
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
...mockGlobalThisDocument,
contentType: "json/application",
}));
require("./content-script");
expect(Messenger.forDOMCommunication).not.toHaveBeenCalled();
});
it("skips initializing if the document location protocol is not 'https'", () => {
jest.spyOn(Messenger, "forDOMCommunication");
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
...mockGlobalThisDocument,
location: {
...mockGlobalThisDocument.location,
href: "http://localhost",
origin: "http://localhost",
protocol: "http:",
},
}));
require("./content-script");
expect(Messenger.forDOMCommunication).not.toHaveBeenCalled();
});
});
}); });

View File

@ -9,17 +9,29 @@ import { WebauthnUtils } from "../webauthn-utils";
import { MessageType } from "./messaging/message"; import { MessageType } from "./messaging/message";
import { Messenger } from "./messaging/messenger"; import { Messenger } from "./messaging/messenger";
const originalGlobalThis = globalThis;
const mockGlobalThisDocument = {
...originalGlobalThis.document,
contentType: "text/html",
location: {
...originalGlobalThis.document.location,
href: "https://localhost",
origin: "https://localhost",
protocol: "https:",
},
};
let messenger: Messenger; let messenger: Messenger;
jest.mock("./messaging/messenger", () => { jest.mock("./messaging/messenger", () => {
return { return {
Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { Messenger: class extends jest.requireActual("./messaging/messenger").Messenger {
static forDOMCommunication: any = jest.fn((window) => { static forDOMCommunication: any = jest.fn((context) => {
const windowOrigin = window.location.origin; const windowOrigin = context.location.origin;
messenger = new Messenger({ messenger = new Messenger({
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]),
addEventListener: (listener) => window.addEventListener("message", listener), addEventListener: (listener) => context.addEventListener("message", listener),
removeEventListener: (listener) => window.removeEventListener("message", listener), removeEventListener: (listener) => context.removeEventListener("message", listener),
}); });
messenger.destroy = jest.fn(); messenger.destroy = jest.fn();
return messenger; return messenger;
@ -30,15 +42,22 @@ jest.mock("./messaging/messenger", () => {
jest.mock("../webauthn-utils"); jest.mock("../webauthn-utils");
describe("Fido2 page script without native WebAuthn support", () => { describe("Fido2 page script without native WebAuthn support", () => {
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(
() => mockGlobalThisDocument,
);
const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); const mockCredentialCreationOptions = createCredentialCreationOptionsMock();
const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCreateCredentialsResult = createCreateCredentialResultMock();
const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
const mockCredentialAssertResult = createAssertCredentialResultMock(); const mockCredentialAssertResult = createAssertCredentialResultMock();
require("./page-script"); require("./page-script");
afterEach(() => {
jest.resetModules();
});
afterAll(() => { afterAll(() => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.resetModules();
}); });
describe("creating WebAuthn credentials", () => { describe("creating WebAuthn credentials", () => {

View File

@ -292,8 +292,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
const ciphers = await this.cipherService.getAllDecryptedForUrl( const ciphers = await this.cipherService.getAllDecryptedForUrl(
this.url, this.url,
otherTypes.length > 0 ? otherTypes : null, otherTypes.length > 0 ? otherTypes : null,
null,
false,
); );
this.loginCiphers = []; this.loginCiphers = [];

View File

@ -136,7 +136,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe( private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe(
shareReplay({ refCount: false }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> { private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {

View File

@ -21,9 +21,10 @@
ariaCurrentWhenActive="page" ariaCurrentWhenActive="page"
> >
<i class="bwi {{ product.icon }} tw-text-4xl !tw-m-0 !tw-mb-1"></i> <i class="bwi {{ product.icon }} tw-text-4xl !tw-m-0 !tw-mb-1"></i>
<span class="tw-text-center tw-text-sm tw-leading-snug group-hover:tw-underline">{{ <span
product.name class="tw-max-w-24 tw-text-center tw-text-sm tw-leading-snug group-hover:tw-underline"
}}</span> >{{ product.name }}</span
>
</a> </a>
</section> </section>

View File

@ -97,7 +97,7 @@ export class ProductSwitcherContentComponent {
}, },
ac: { ac: {
name: "Admin Console", name: "Admin Console",
icon: "bwi-business", icon: "bwi-user-monitor",
appRoute: ["/organizations", acOrg?.id], appRoute: ["/organizations", acOrg?.id],
marketingRoute: "https://bitwarden.com/products/business/", marketingRoute: "https://bitwarden.com/products/business/",
isActive: this.router.url.includes("/organizations/"), isActive: this.router.url.includes("/organizations/"),

View File

@ -67,3 +67,7 @@ export class ServiceAccountProjectPolicyPermissionDetailsView {
export class ServiceAccountGrantedPoliciesView { export class ServiceAccountGrantedPoliciesView {
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[];
} }
export class ProjectServiceAccountsAccessPoliciesView {
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
}

View File

@ -1,17 +1,27 @@
<div class="tw-w-2/5"> <form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
<p class="tw-mt-8"> <div class="tw-w-2/5">
{{ "projectMachineAccountsDescription" | i18n }} <p class="tw-mt-8" *ngIf="!loading">
</p> {{ "projectMachineAccountsDescription" | i18n }}
<sm-access-selector </p>
[rows]="rows$ | async" <sm-access-policy-selector
granteeType="serviceAccounts" [loading]="loading"
[label]="'machineAccounts' | i18n" formControlName="accessPolicies"
[hint]="'projectMachineAccountsSelectHint' | i18n" [addButtonMode]="true"
[columnTitle]="'machineAccounts' | i18n" [items]="items"
[emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n" [label]="'machineAccounts' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" [hint]="'projectMachineAccountsSelectHint' | i18n"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" [columnTitle]="'machineAccounts' | i18n"
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" [emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n"
> >
</sm-access-selector> </sm-access-policy-selector>
</div> <button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7">
{{ "save" | i18n }}
</button>
</div>
</form>
<ng-template #spinner>
<div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center">
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
</div>
</ng-template>

View File

@ -1,93 +1,69 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; import { combineLatest, Subject, switchMap, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { SelectItemView } from "@bitwarden/components";
import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policy.view";
import { import {
ProjectAccessPoliciesView, ApItemValueType,
ServiceAccountProjectAccessPolicyView, convertToProjectServiceAccountsAccessPoliciesView,
} from "../../models/view/access-policy.view"; } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
import {
ApItemViewType,
convertPotentialGranteesToApItemViewType,
convertProjectServiceAccountsViewToApItemViews,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
import {
AccessSelectorComponent,
AccessSelectorRowView,
} from "../../shared/access-policies/access-selector.component";
@Component({ @Component({
selector: "sm-project-service-accounts", selector: "sm-project-service-accounts",
templateUrl: "./project-service-accounts.component.html", templateUrl: "./project-service-accounts.component.html",
}) })
export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
private currentAccessPolicies: ApItemViewType[];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private organizationId: string; private organizationId: string;
private projectId: string; private projectId: string;
protected rows$: Observable<AccessSelectorRowView[]> = private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
this.accessPolicyService.projectAccessPolicyChanges$.pipe( switchMap(([params]) =>
startWith(null), this.accessPolicyService
switchMap(() => .getProjectServiceAccountsAccessPolicies(params.organizationId, params.projectId)
this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId), .then((policies) => {
), return convertProjectServiceAccountsViewToApItemViews(policies);
map((policies) => }),
policies.serviceAccountAccessPolicies.map((policy) => ({ ),
type: "serviceAccount", );
name: policy.serviceAccountName,
id: policy.serviceAccountId,
accessPolicyId: policy.id,
read: policy.read,
write: policy.write,
icon: AccessSelectorComponent.serviceAccountIcon,
static: false,
})),
),
);
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) { private potentialGrantees$ = combineLatest([this.route.params]).pipe(
try { switchMap(([params]) =>
return await this.accessPolicyService.updateAccessPolicy( this.accessPolicyService
AccessSelectorComponent.getBaseAccessPolicyView(policy), .getServiceAccountsPotentialGrantees(params.organizationId)
); .then((grantees) => {
} catch (e) { return convertPotentialGranteesToApItemViewType(grantees);
this.validationService.showError(e); }),
} ),
} );
protected handleCreateAccessPolicies(selected: SelectItemView[]) { protected formGroup = new FormGroup({
const projectAccessPoliciesView = new ProjectAccessPoliciesView(); accessPolicies: new FormControl([] as ApItemValueType[]),
projectAccessPoliciesView.serviceAccountAccessPolicies = selected });
.filter(
(selection) => AccessSelectorComponent.getAccessItemType(selection) === "serviceAccount",
)
.map((filtered) => {
const view = new ServiceAccountProjectAccessPolicyView();
view.grantedProjectId = this.projectId;
view.serviceAccountId = filtered.id;
view.read = true;
view.write = false;
return view;
});
return this.accessPolicyService.createProjectAccessPolicies( protected loading = true;
this.organizationId, protected potentialGrantees: ApItemViewType[];
this.projectId, protected items: ApItemViewType[];
projectAccessPoliciesView,
);
}
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
try {
await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
} catch (e) {
this.validationService.showError(e);
}
}
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef,
private validationService: ValidationService, private validationService: ValidationService,
private accessPolicyService: AccessPolicyService, private accessPolicyService: AccessPolicyService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -95,10 +71,97 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
this.projectId = params.projectId; this.projectId = params.projectId;
}); });
combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
.pipe(takeUntil(this.destroy$))
.subscribe(([potentialGrantees, currentAccessPolicies]) => {
this.potentialGrantees = potentialGrantees;
this.items = this.getItems(potentialGrantees, currentAccessPolicies);
this.setSelected(currentAccessPolicies);
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
submit = async () => {
if (this.isFormInvalid()) {
return;
}
const formValues = this.formGroup.value.accessPolicies;
this.formGroup.disable();
try {
const accessPoliciesView = await this.updateProjectServiceAccountsAccessPolicies(
this.organizationId,
this.projectId,
formValues,
);
const updatedView = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView);
this.items = this.getItems(this.potentialGrantees, updatedView);
this.setSelected(updatedView);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("projectAccessUpdated"),
);
} catch (e) {
this.validationService.showError(e);
this.setSelected(this.currentAccessPolicies);
}
this.formGroup.enable();
};
private setSelected(policiesToSelect: ApItemViewType[]) {
this.loading = true;
this.currentAccessPolicies = policiesToSelect;
if (policiesToSelect != undefined) {
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
// potentialGrantees, otherwise no selected values will be patched below
this.changeDetectorRef.detectChanges();
this.formGroup.patchValue({
accessPolicies: policiesToSelect.map((m) => ({
type: m.type,
id: m.id,
permission: m.permission,
})),
});
}
this.loading = false;
}
private isFormInvalid(): boolean {
this.formGroup.markAllAsTouched();
return this.formGroup.invalid;
}
private async updateProjectServiceAccountsAccessPolicies(
organizationId: string,
projectId: string,
selectedPolicies: ApItemValueType[],
): Promise<ProjectServiceAccountsAccessPoliciesView> {
const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies);
return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies(
organizationId,
projectId,
view,
);
}
private getItems(potentialGrantees: ApItemViewType[], currentAccessPolicies: ApItemViewType[]) {
// If the user doesn't have access to the service account, they won't be in the potentialGrantees list.
// Add them to the potentialGrantees list if they are selected.
const items = [...potentialGrantees];
for (const policy of currentAccessPolicies) {
const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
if (!exists) {
items.push(policy);
}
}
return items;
}
} }

View File

@ -8,6 +8,7 @@ import {
ServiceAccountGrantedPoliciesView, ServiceAccountGrantedPoliciesView,
ServiceAccountProjectPolicyPermissionDetailsView, ServiceAccountProjectPolicyPermissionDetailsView,
ServiceAccountProjectAccessPolicyView, ServiceAccountProjectAccessPolicyView,
ProjectServiceAccountsAccessPoliciesView,
} from "../../../../models/view/access-policy.view"; } from "../../../../models/view/access-policy.view";
import { ApItemEnum } from "./enums/ap-item.enum"; import { ApItemEnum } from "./enums/ap-item.enum";
@ -102,3 +103,23 @@ export function convertToServiceAccountGrantedPoliciesView(
return view; return view;
} }
export function convertToProjectServiceAccountsAccessPoliciesView(
projectId: string,
selectedPolicyValues: ApItemValueType[],
): ProjectServiceAccountsAccessPoliciesView {
const view = new ProjectServiceAccountsAccessPoliciesView();
view.serviceAccountAccessPolicies = selectedPolicyValues
.filter((x) => x.type == ApItemEnum.ServiceAccount)
.map((filtered) => {
const policyView = new ServiceAccountProjectAccessPolicyView();
policyView.serviceAccountId = filtered.id;
policyView.grantedProjectId = projectId;
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
return policyView;
});
return view;
}

View File

@ -4,6 +4,7 @@ import { SelectItemView } from "@bitwarden/components";
import { import {
ProjectPeopleAccessPoliciesView, ProjectPeopleAccessPoliciesView,
ServiceAccountGrantedPoliciesView, ServiceAccountGrantedPoliciesView,
ProjectServiceAccountsAccessPoliciesView,
ServiceAccountPeopleAccessPoliciesView, ServiceAccountPeopleAccessPoliciesView,
} from "../../../../models/view/access-policy.view"; } from "../../../../models/view/access-policy.view";
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
@ -98,6 +99,29 @@ export function convertGrantedPoliciesToAccessPolicyItemViews(
return accessPolicies; return accessPolicies;
} }
export function convertProjectServiceAccountsViewToApItemViews(
value: ProjectServiceAccountsAccessPoliciesView,
): ApItemViewType[] {
const accessPolicies: ApItemViewType[] = [];
value.serviceAccountAccessPolicies.forEach((accessPolicyView) => {
accessPolicies.push({
type: ApItemEnum.ServiceAccount,
icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount),
id: accessPolicyView.serviceAccountId,
accessPolicyId: accessPolicyView.id,
labelName: accessPolicyView.serviceAccountName,
listName: accessPolicyView.serviceAccountName,
permission: ApPermissionEnumUtil.toApPermissionEnum(
accessPolicyView.read,
accessPolicyView.write,
),
readOnly: false,
});
});
return accessPolicies;
}
export function convertPotentialGranteesToApItemViewType( export function convertPotentialGranteesToApItemViewType(
grantees: PotentialGranteeView[], grantees: PotentialGranteeView[],
): ApItemViewType[] { ): ApItemViewType[] {

View File

@ -19,6 +19,7 @@ import {
UserServiceAccountAccessPolicyView, UserServiceAccountAccessPolicyView,
ServiceAccountPeopleAccessPoliciesView, ServiceAccountPeopleAccessPoliciesView,
ServiceAccountGrantedPoliciesView, ServiceAccountGrantedPoliciesView,
ProjectServiceAccountsAccessPoliciesView,
ServiceAccountProjectPolicyPermissionDetailsView, ServiceAccountProjectPolicyPermissionDetailsView,
} from "../../models/view/access-policy.view"; } from "../../models/view/access-policy.view";
import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
@ -29,6 +30,7 @@ import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request";
import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request";
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
import { import {
GroupServiceAccountAccessPolicyResponse, GroupServiceAccountAccessPolicyResponse,
UserServiceAccountAccessPolicyResponse, UserServiceAccountAccessPolicyResponse,
@ -38,6 +40,7 @@ import {
} from "./models/responses/access-policy.response"; } from "./models/responses/access-policy.response";
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
import { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-access-policies.response";
import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response"; import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response";
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response";
import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response"; import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response";
@ -175,6 +178,40 @@ export class AccessPolicyService {
return await this.createServiceAccountGrantedPoliciesView(result, organizationId); return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
} }
async getProjectServiceAccountsAccessPolicies(
organizationId: string,
projectId: string,
): Promise<ProjectServiceAccountsAccessPoliciesView> {
const r = await this.apiService.send(
"GET",
"/projects/" + projectId + "/access-policies/service-accounts",
null,
true,
true,
);
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
}
async putProjectServiceAccountsAccessPolicies(
organizationId: string,
projectId: string,
policies: ProjectServiceAccountsAccessPoliciesView,
): Promise<ProjectServiceAccountsAccessPoliciesView> {
const request = this.getProjectServiceAccountsAccessPoliciesRequest(policies);
const r = await this.apiService.send(
"PUT",
"/projects/" + projectId + "/access-policies/service-accounts",
request,
true,
true,
);
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
}
async createProjectAccessPolicies( async createProjectAccessPolicies(
organizationId: string, organizationId: string,
projectId: string, projectId: string,
@ -325,6 +362,18 @@ export class AccessPolicyService {
return request; return request;
} }
private getProjectServiceAccountsAccessPoliciesRequest(
policies: ProjectServiceAccountsAccessPoliciesView,
): ProjectServiceAccountsAccessPoliciesRequest {
const request = new ProjectServiceAccountsAccessPoliciesRequest();
request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => {
return this.getAccessPolicyRequest(ap.serviceAccountId, ap);
});
return request;
}
private async createServiceAccountGrantedPoliciesView( private async createServiceAccountGrantedPoliciesView(
response: ServiceAccountGrantedPoliciesPermissionDetailsResponse, response: ServiceAccountGrantedPoliciesPermissionDetailsResponse,
organizationId: string, organizationId: string,
@ -535,4 +584,19 @@ export class AccessPolicyService {
currentUserInGroup: response.currentUserInGroup, currentUserInGroup: response.currentUserInGroup,
}; };
} }
private async createProjectServiceAccountsAccessPoliciesView(
response: ProjectServiceAccountsAccessPoliciesResponse,
organizationId: string,
): Promise<ProjectServiceAccountsAccessPoliciesView> {
const orgKey = await this.getOrganizationKey(organizationId);
const view = new ProjectServiceAccountsAccessPoliciesView();
view.serviceAccountAccessPolicies = await Promise.all(
response.serviceAccountAccessPolicies.map(async (ap) => {
return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap);
}),
);
return view;
}
} }

View File

@ -0,0 +1,5 @@
import { AccessPolicyRequest } from "./access-policy.request";
export class ProjectServiceAccountsAccessPoliciesRequest {
serviceAccountAccessPolicyRequests?: AccessPolicyRequest[];
}

View File

@ -0,0 +1,15 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response";
export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse {
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[];
constructor(response: any) {
super(response);
const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies");
this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map(
(k: any) => new ServiceAccountProjectAccessPolicyResponse(k),
);
}
}

View File

@ -19,17 +19,36 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
private clear$ = new Subject<void>(); private clear$ = new Subject<void>();
/** /**
* Sends items to a web worker to decrypt them. * Decrypts items using a web worker if the environment supports it.
* This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI). * Will fall back to the main thread if the window object is not available.
*/ */
async decryptItems<T extends InitializerMetadata>( async decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[], items: Decryptable<T>[],
key: SymmetricCryptoKey, key: SymmetricCryptoKey,
): Promise<T[]> { ): Promise<T[]> {
if (typeof window === "undefined") {
return super.decryptItems(items, key);
}
if (items == null || items.length < 1) { if (items == null || items.length < 1) {
return []; return [];
} }
const decryptedItems = await this.getDecryptedItemsFromWorker(items, key);
const parsedItems = JSON.parse(decryptedItems);
return this.initializeItems(parsedItems);
}
/**
* Sends items to a web worker to decrypt them. This utilizes multithreading to decrypt items
* faster without interrupting other operations (e.g. updating UI). This method returns values
* prior to deserialization to support forwarding results to another party
*/
async getDecryptedItemsFromWorker<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<string> {
this.logService.info("Starting decryption using multithreading"); this.logService.info("Starting decryption using multithreading");
this.worker ??= new Worker( this.worker ??= new Worker(
@ -53,19 +72,20 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
return await firstValueFrom( return await firstValueFrom(
fromEvent(this.worker, "message").pipe( fromEvent(this.worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === request.id), filter((response: MessageEvent) => response.data?.id === request.id),
map((response) => JSON.parse(response.data.items)), map((response) => response.data.items),
map((items) =>
items.map((jsonItem: Jsonify<T>) => {
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
return initializer(jsonItem);
}),
),
takeUntil(this.clear$), takeUntil(this.clear$),
defaultIfEmpty([]), defaultIfEmpty("[]"),
), ),
); );
} }
protected initializeItems<T extends InitializerMetadata>(items: Jsonify<T>[]): T[] {
return items.map((jsonItem: Jsonify<T>) => {
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
return initializer(jsonItem);
});
}
private clear() { private clear() {
this.clear$.next(); this.clear$.next();
this.worker?.terminate(); this.worker?.terminate();

View File

@ -33,7 +33,6 @@ export abstract class CipherService {
url: string, url: string,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchStrategySetting, defaultMatch?: UriMatchStrategySetting,
reindexCiphers?: boolean,
) => Promise<CipherView[]>; ) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>; getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
/** /**

View File

@ -441,7 +441,6 @@ export class CipherService implements CipherServiceAbstraction {
url: string, url: string,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch: UriMatchStrategySetting = null, defaultMatch: UriMatchStrategySetting = null,
reindexCiphers = true,
): Promise<CipherView[]> { ): Promise<CipherView[]> {
if (url == null && includeOtherTypes == null) { if (url == null && includeOtherTypes == null) {
return Promise.resolve([]); return Promise.resolve([]);
@ -450,9 +449,7 @@ export class CipherService implements CipherServiceAbstraction {
const equivalentDomains = await firstValueFrom( const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(url), this.domainSettingsService.getUrlEquivalentDomains(url),
); );
const ciphers = reindexCiphers const ciphers = await this.getAllDecrypted();
? await this.getAllDecrypted()
: await this.getDecryptedCiphers();
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$); defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
return ciphers.filter((cipher) => { return ciphers.filter((cipher) => {