mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 02:19:18 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
{{ "open" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'unpaid'">
|
||||
<i class="bwi bwi-exclamation-circle tw-text-muted" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'paid'">
|
||||
|
||||
@@ -4,48 +4,48 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
DefaultOrganizationUserApiService,
|
||||
CollectionService,
|
||||
DefaultCollectionService,
|
||||
DefaultOrganizationUserApiService,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
SetPasswordJitService,
|
||||
DefaultSetPasswordJitService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
DefaultRegistrationFinishService,
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
LoginComponentService,
|
||||
DefaultLoginApprovalComponentService,
|
||||
DefaultLoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
TwoFactorAuthComponentService,
|
||||
DefaultRegistrationFinishService,
|
||||
DefaultSetPasswordJitService,
|
||||
DefaultTwoFactorAuthComponentService,
|
||||
DefaultTwoFactorAuthEmailComponentService,
|
||||
TwoFactorAuthEmailComponentService,
|
||||
DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
LoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SetPasswordJitService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthEmailComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
DefaultLoginApprovalComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
AuthRequestService,
|
||||
PinServiceAbstraction,
|
||||
PinService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginEmailService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
LogoutReason,
|
||||
AuthRequestApiService,
|
||||
AuthRequestService,
|
||||
AuthRequestServiceAbstraction,
|
||||
DefaultAuthRequestApiService,
|
||||
DefaultLoginSuccessHandlerService,
|
||||
LoginSuccessHandlerService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
LoginEmailService,
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginSuccessHandlerService,
|
||||
LogoutReason,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
@@ -118,16 +118,16 @@ import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauth
|
||||
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import {
|
||||
AutofillSettingsServiceAbstraction,
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
} from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import {
|
||||
BadgeSettingsServiceAbstraction,
|
||||
BadgeSettingsService,
|
||||
BadgeSettingsServiceAbstraction,
|
||||
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import {
|
||||
DomainSettingsService,
|
||||
DefaultDomainSettingsService,
|
||||
DomainSettingsService,
|
||||
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import {
|
||||
BillingApiServiceAbstraction,
|
||||
@@ -200,8 +200,8 @@ import {
|
||||
WebPushNotificationsApiService,
|
||||
} from "@bitwarden/common/platform/notifications/internal";
|
||||
import {
|
||||
TaskSchedulerService,
|
||||
DefaultTaskSchedulerService,
|
||||
TaskSchedulerService,
|
||||
} from "@bitwarden/common/platform/scheduling";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
@@ -222,10 +222,10 @@ import { ValidationService } from "@bitwarden/common/platform/services/validatio
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
SingleUserStateProvider,
|
||||
StateProvider,
|
||||
DerivedStateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
/* eslint-disable import/no-restricted-paths -- We need the implementations to inject, but generally these should not be accessed */
|
||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||
@@ -280,6 +280,7 @@ import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
|
||||
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
GeneratorHistoryService,
|
||||
@@ -292,34 +293,32 @@ import {
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KeyService,
|
||||
DefaultKeyService,
|
||||
BiometricsService,
|
||||
BiometricStateService,
|
||||
DefaultBiometricStateService,
|
||||
BiometricsService,
|
||||
DefaultKdfConfigService,
|
||||
KdfConfigService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
DefaultUserAsymmetricKeysRegenerationService,
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
DefaultKeyService,
|
||||
DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
DefaultUserAsymmetricKeysRegenerationService,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
import {
|
||||
DefaultTaskService,
|
||||
DefaultEndUserNotificationService,
|
||||
EndUserNotificationService,
|
||||
NewDeviceVerificationNoticeService,
|
||||
PasswordRepromptService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
import {
|
||||
VaultExportService,
|
||||
VaultExportServiceAbstraction,
|
||||
OrganizationVaultExportService,
|
||||
OrganizationVaultExportServiceAbstraction,
|
||||
IndividualVaultExportService,
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
OrganizationVaultExportService,
|
||||
OrganizationVaultExportServiceAbstraction,
|
||||
VaultExportService,
|
||||
VaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
@@ -334,24 +333,24 @@ import { AbstractThemingService } from "../platform/services/theming/theming.ser
|
||||
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
|
||||
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
LOCALES_DIRECTORY,
|
||||
LOCKED_CALLBACK,
|
||||
LOGOUT_CALLBACK,
|
||||
LOG_MAC_FAILURES,
|
||||
LOGOUT_CALLBACK,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
|
||||
SECURE_STORAGE,
|
||||
STATE_FACTORY,
|
||||
SUPPORTS_SECURE_STORAGE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
WINDOW,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
CLIENT_TYPE,
|
||||
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
} from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
|
||||
|
||||
38
libs/common/src/enums/feature-flag.enum.spec.ts
Normal file
38
libs/common/src/enums/feature-flag.enum.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
|
||||
import { getFeatureFlagValue, FeatureFlag, DefaultFeatureFlagValue } from "./feature-flag.enum";
|
||||
|
||||
describe("getFeatureFlagValue", () => {
|
||||
const testFlag = Object.values(FeatureFlag)[0];
|
||||
const testFlagDefaultValue = DefaultFeatureFlagValue[testFlag];
|
||||
|
||||
it("returns default flag value when serverConfig is null", () => {
|
||||
const result = getFeatureFlagValue(null, testFlag);
|
||||
expect(result).toBe(testFlagDefaultValue);
|
||||
});
|
||||
|
||||
it("returns default flag value when serverConfig.featureStates is undefined", () => {
|
||||
const serverConfig = {} as ServerConfig;
|
||||
const result = getFeatureFlagValue(serverConfig, testFlag);
|
||||
expect(result).toBe(testFlagDefaultValue);
|
||||
});
|
||||
|
||||
it("returns default flag value when the feature flag is not in serverConfig.featureStates", () => {
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
serverConfig.featureStates = {};
|
||||
|
||||
const result = getFeatureFlagValue(serverConfig, testFlag);
|
||||
expect(result).toBe(testFlagDefaultValue);
|
||||
});
|
||||
|
||||
it("returns the flag value from serverConfig.featureStates when the feature flag exists", () => {
|
||||
const expectedValue = true;
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
serverConfig.featureStates = { [testFlag]: expectedValue };
|
||||
|
||||
const result = getFeatureFlagValue(serverConfig, testFlag);
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
|
||||
/**
|
||||
* Feature flags.
|
||||
*
|
||||
@@ -125,3 +127,14 @@ export const DefaultFeatureFlagValue = {
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag];
|
||||
|
||||
export function getFeatureFlagValue<Flag extends FeatureFlag>(
|
||||
serverConfig: ServerConfig | null,
|
||||
flag: Flag,
|
||||
) {
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
|
||||
return DefaultFeatureFlagValue[flag];
|
||||
}
|
||||
|
||||
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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 { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class BulkEncryptService {
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "../../../platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class EncryptService {
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
@@ -54,4 +55,6 @@ export abstract class EncryptService {
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512",
|
||||
): Promise<string>;
|
||||
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import * as rxjs from "rxjs";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
import { BulkEncryptServiceImplementation } from "./bulk-encrypt.service.implementation";
|
||||
|
||||
describe("BulkEncryptServiceImplementation", () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<LogService>();
|
||||
|
||||
let sut: BulkEncryptServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new BulkEncryptServiceImplementation(cryptoFunctionService, logService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
const mockWorker = mock<Worker>();
|
||||
let globalWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
globalWindow = global.window;
|
||||
|
||||
// Mock creating a worker.
|
||||
global.Worker = jest.fn().mockImplementation(() => mockWorker);
|
||||
global.URL = jest.fn().mockImplementation(() => "url") as unknown as typeof URL;
|
||||
global.URL.createObjectURL = jest.fn().mockReturnValue("blob:url");
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
global.URL.canParse = jest.fn().mockReturnValue(true);
|
||||
|
||||
// Mock the workers returned response.
|
||||
const mockMessageEvent = {
|
||||
id: "mock-guid",
|
||||
data: ["decrypted1", "decrypted2"],
|
||||
};
|
||||
const mockMessageEvent$ = rxjs.from([mockMessageEvent]);
|
||||
jest.spyOn(rxjs, "fromEvent").mockReturnValue(mockMessageEvent$);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.window = globalWindow;
|
||||
});
|
||||
|
||||
it("throws error if key is null", async () => {
|
||||
const nullKey = null as unknown as SymmetricCryptoKey;
|
||||
await expect(sut.decryptItems([], nullKey)).rejects.toThrow("No encryption key provided.");
|
||||
});
|
||||
|
||||
it("returns an empty array when items is null", async () => {
|
||||
const result = await sut.decryptItems(null as any, key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array when items is empty", async () => {
|
||||
const result = await sut.decryptItems([], key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("decrypts items sequentially when window is undefined", async () => {
|
||||
// Make global window undefined.
|
||||
delete (global as any).window;
|
||||
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(logService.info).toHaveBeenCalledWith(
|
||||
"Window not available in BulkEncryptService, decrypting sequentially",
|
||||
);
|
||||
expect(result).toEqual(["item1", "item2"]);
|
||||
expect(mockItems[0].decrypt).toHaveBeenCalledWith(key);
|
||||
expect(mockItems[1].decrypt).toHaveBeenCalledWith(key);
|
||||
});
|
||||
|
||||
it("uses workers for decryption when window is available", async () => {
|
||||
const mockDecryptedItems = ["decrypted1", "decrypted2"];
|
||||
jest
|
||||
.spyOn<any, any>(sut, "getDecryptedItemsFromWorkers")
|
||||
.mockResolvedValue(mockDecryptedItems);
|
||||
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(sut["getDecryptedItemsFromWorkers"]).toHaveBeenCalledWith(mockItems, key);
|
||||
expect(result).toEqual(mockDecryptedItems);
|
||||
});
|
||||
|
||||
it("creates new worker when none exist", async () => {
|
||||
(sut as any).currentServerConfig = undefined;
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a SetConfigMessage to the new worker when there is a current server config", async () => {
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(2);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not create worker if one exists", async () => {
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
(sut as any).workers = [mockWorker];
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(global.Worker).not.toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onServerConfigChange", () => {
|
||||
it("updates internal currentServerConfig to new config", () => {
|
||||
const newConfig = mock<ServerConfig>();
|
||||
|
||||
sut.onServerConfigChange(newConfig);
|
||||
|
||||
expect((sut as any).currentServerConfig).toBe(newConfig);
|
||||
});
|
||||
|
||||
it("does send a SetConfigMessage to workers when there is a worker", () => {
|
||||
const newConfig = mock<ServerConfig>();
|
||||
const mockWorker = mock<Worker>();
|
||||
(sut as any).workers = [mockWorker];
|
||||
|
||||
sut.onServerConfigChange(newConfig);
|
||||
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(buildSetConfigMessage({ newConfig }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockDecryptable<T extends InitializerMetadata>(
|
||||
returnValue: any,
|
||||
): MockProxy<Decryptable<T>> {
|
||||
const mockDecryptable = mock<Decryptable<T>>();
|
||||
mockDecryptable.decrypt.mockResolvedValue(returnValue);
|
||||
return mockDecryptable;
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
const workerTTL = 60000; // 1 minute
|
||||
const maxWorkers = 8;
|
||||
@@ -20,6 +23,7 @@ const minNumberOfItemsForMultithreading = 400;
|
||||
export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
private workers: Worker[] = [];
|
||||
private timeout: any;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
private clear$ = new Subject<void>();
|
||||
|
||||
@@ -57,6 +61,11 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.currentServerConfig = newConfig;
|
||||
this.updateWorkerServerConfigs(newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items
|
||||
* faster without interrupting other operations (e.g. updating UI).
|
||||
@@ -93,6 +102,9 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (this.currentServerConfig != undefined) {
|
||||
this.updateWorkerServerConfigs(this.currentServerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
const itemsPerWorker = Math.floor(items.length / this.workers.length);
|
||||
@@ -108,17 +120,18 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
itemsForWorker.push(...items.slice(end));
|
||||
}
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
const id = Utils.newGuid();
|
||||
const request = buildDecryptMessage({
|
||||
id,
|
||||
items: itemsForWorker,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
worker.postMessage(JSON.stringify(request));
|
||||
worker.postMessage(request);
|
||||
results.push(
|
||||
firstValueFrom(
|
||||
fromEvent(worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
filter((response: MessageEvent) => response.data?.id === id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
@@ -143,6 +156,13 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
private updateWorkerServerConfigs(newConfig: ServerConfig) {
|
||||
this.workers.forEach((worker) => {
|
||||
const request = buildSetConfigMessage({ newConfig });
|
||||
worker.postMessage(request);
|
||||
});
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
for (const worker of this.workers) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export class EncryptServiceImplementation implements EncryptService {
|
||||
@@ -24,6 +25,11 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
protected logMacFailures: boolean,
|
||||
) {}
|
||||
|
||||
// Handle updating private properties to turn on/off feature flags.
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
return;
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "../../../platform/services/console-log.service";
|
||||
import { ContainerService } from "../../../platform/services/container.service";
|
||||
import { getClassInitializer } from "../../../platform/services/cryptography/get-class-initializer";
|
||||
import { WebCryptoFunctionService } from "../../../platform/services/web-crypto-function.service";
|
||||
import {
|
||||
DECRYPT_COMMAND,
|
||||
SET_CONFIG_COMMAND,
|
||||
ParsedDecryptCommandData,
|
||||
} from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
|
||||
@@ -15,13 +22,14 @@ const workerApi: Worker = self as any;
|
||||
|
||||
let inited = false;
|
||||
let encryptService: EncryptServiceImplementation;
|
||||
let logService: LogService;
|
||||
|
||||
/**
|
||||
* Bootstrap the worker environment with services required for decryption
|
||||
*/
|
||||
export function init() {
|
||||
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
const logService = new ConsoleLogService(false);
|
||||
logService = new ConsoleLogService(false);
|
||||
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
|
||||
const bitwardenContainerService = new ContainerService(null, encryptService);
|
||||
@@ -39,11 +47,22 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
}
|
||||
|
||||
const request: {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
command: string;
|
||||
} = JSON.parse(event.data);
|
||||
|
||||
switch (request.command) {
|
||||
case DECRYPT_COMMAND:
|
||||
return await handleDecrypt(request as unknown as ParsedDecryptCommandData);
|
||||
case SET_CONFIG_COMMAND: {
|
||||
const newConfig = (request as unknown as { newConfig: Jsonify<ServerConfig> }).newConfig;
|
||||
return await handleSetConfig(newConfig);
|
||||
}
|
||||
default:
|
||||
logService.error(`[EncryptWorker] unknown worker command`, request.command, request);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleDecrypt(request: ParsedDecryptCommandData) {
|
||||
const key = SymmetricCryptoKey.fromJSON(request.key);
|
||||
const items = request.items.map((jsonItem) => {
|
||||
const initializer = getClassInitializer<Decryptable<any>>(jsonItem.initializerKey);
|
||||
@@ -55,4 +74,8 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
id: request.id,
|
||||
items: JSON.stringify(result),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSetConfig(newConfig: Jsonify<ServerConfig>) {
|
||||
encryptService.onServerConfigChange(ServerConfig.fromJSON(newConfig));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { BulkEncryptService } from "../abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
import { FallbackBulkEncryptService } from "./fallback-bulk-encrypt.service";
|
||||
|
||||
describe("FallbackBulkEncryptService", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
const featureFlagEncryptService = mock<BulkEncryptService>();
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
|
||||
let sut: FallbackBulkEncryptService;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new FallbackBulkEncryptService(encryptService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const mockItems = [{ id: "guid", name: "encryptedValue" }] as any[];
|
||||
const mockDecryptedItems = [{ id: "guid", name: "decryptedValue" }] as any[];
|
||||
|
||||
it("calls decryptItems on featureFlagEncryptService when it is set", async () => {
|
||||
featureFlagEncryptService.decryptItems.mockResolvedValue(mockDecryptedItems);
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(featureFlagEncryptService.decryptItems).toHaveBeenCalledWith(mockItems, key);
|
||||
expect(encryptService.decryptItems).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockDecryptedItems);
|
||||
});
|
||||
|
||||
it("calls decryptItems on encryptService when featureFlagEncryptService is not set", async () => {
|
||||
encryptService.decryptItems.mockResolvedValue(mockDecryptedItems);
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(encryptService.decryptItems).toHaveBeenCalledWith(mockItems, key);
|
||||
expect(result).toEqual(mockDecryptedItems);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setFeatureFlagEncryptService", () => {
|
||||
it("sets the featureFlagEncryptService property", async () => {
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
|
||||
});
|
||||
|
||||
it("does not call onServerConfigChange when currentServerConfig is undefined", async () => {
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
expect(featureFlagEncryptService.onServerConfigChange).not.toHaveBeenCalled();
|
||||
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
|
||||
});
|
||||
|
||||
it("calls onServerConfigChange with currentServerConfig when it is defined", async () => {
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
expect(featureFlagEncryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
|
||||
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onServerConfigChange", () => {
|
||||
it("updates internal currentServerConfig to new config", async () => {
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect((sut as any).currentServerConfig).toBe(serverConfig);
|
||||
});
|
||||
|
||||
it("calls onServerConfigChange on featureFlagEncryptService when it is set", async () => {
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(featureFlagEncryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
|
||||
expect(encryptService.onServerConfigChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onServerConfigChange on encryptService when featureFlagEncryptService is not set", () => {
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(encryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.i
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ import { EncryptService } from "../abstractions/encrypt.service";
|
||||
*/
|
||||
export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||
private featureFlagEncryptService: BulkEncryptService;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
constructor(protected encryptService: EncryptService) {}
|
||||
|
||||
@@ -31,6 +33,14 @@ export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||
}
|
||||
|
||||
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {
|
||||
if (this.currentServerConfig !== undefined) {
|
||||
featureFlagEncryptService.onServerConfigChange(this.currentServerConfig);
|
||||
}
|
||||
this.featureFlagEncryptService = featureFlagEncryptService;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.currentServerConfig = newConfig;
|
||||
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import * as rxjs from "rxjs";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "./multithread-encrypt.service.implementation";
|
||||
|
||||
describe("MultithreadEncryptServiceImplementation", () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<LogService>();
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
|
||||
let sut: MultithreadEncryptServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MultithreadEncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const mockWorker = mock<Worker>();
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock creating a worker.
|
||||
global.Worker = jest.fn().mockImplementation(() => mockWorker);
|
||||
global.URL = jest.fn().mockImplementation(() => "url") as unknown as typeof URL;
|
||||
global.URL.createObjectURL = jest.fn().mockReturnValue("blob:url");
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
global.URL.canParse = jest.fn().mockReturnValue(true);
|
||||
|
||||
// Mock the workers returned response.
|
||||
const mockMessageEvent = {
|
||||
id: "mock-guid",
|
||||
data: ["decrypted1", "decrypted2"],
|
||||
};
|
||||
const mockMessageEvent$ = rxjs.from([mockMessageEvent]);
|
||||
jest.spyOn(rxjs, "fromEvent").mockReturnValue(mockMessageEvent$);
|
||||
});
|
||||
|
||||
it("returns empty array if items is null", async () => {
|
||||
const items = null as unknown as Decryptable<any>[];
|
||||
const result = await sut.decryptItems(items, key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array if items is empty", async () => {
|
||||
const result = await sut.decryptItems([], key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("creates worker if none exists", async () => {
|
||||
// Make sure currentServerConfig is undefined so a SetConfigMessage is not sent.
|
||||
(sut as any).currentServerConfig = undefined;
|
||||
|
||||
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a SetConfigMessage to the new worker when there is a current server config", async () => {
|
||||
// Populate currentServerConfig so a SetConfigMessage is sent.
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
|
||||
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(2);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not create worker if one exists", async () => {
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
(sut as any).worker = mockWorker;
|
||||
|
||||
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
|
||||
|
||||
expect(global.Worker).not.toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onServerConfigChange", () => {
|
||||
it("updates internal currentServerConfig to new config and calls super", () => {
|
||||
const superSpy = jest.spyOn(EncryptServiceImplementation.prototype, "onServerConfigChange");
|
||||
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(superSpy).toHaveBeenCalledWith(serverConfig);
|
||||
expect((sut as any).currentServerConfig).toBe(serverConfig);
|
||||
});
|
||||
|
||||
it("sends config update to worker if worker exists", () => {
|
||||
const mockWorker = mock<Worker>();
|
||||
(sut as any).worker = mockWorker;
|
||||
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
@@ -20,6 +23,7 @@ const workerTTL = 3 * 60000; // 3 minutes
|
||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||
private worker: Worker;
|
||||
private timeout: any;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
private clear$ = new Subject<void>();
|
||||
|
||||
@@ -37,27 +41,33 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
||||
|
||||
this.logService.info("Starting decryption using multithreading");
|
||||
|
||||
this.worker ??= new Worker(
|
||||
new URL(
|
||||
/* webpackChunkName: 'encrypt-worker' */
|
||||
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
if (this.worker == null) {
|
||||
this.worker = new Worker(
|
||||
new URL(
|
||||
/* webpackChunkName: 'encrypt-worker' */
|
||||
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
if (this.currentServerConfig !== undefined) {
|
||||
this.updateWorkerServerConfig(this.currentServerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
this.restartTimeout();
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
const id = Utils.newGuid();
|
||||
const request = buildDecryptMessage({
|
||||
id,
|
||||
items: items,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
this.worker.postMessage(JSON.stringify(request));
|
||||
this.worker.postMessage(request);
|
||||
|
||||
return await firstValueFrom(
|
||||
fromEvent(this.worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
filter((response: MessageEvent) => response.data?.id === id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
@@ -71,6 +81,19 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
||||
);
|
||||
}
|
||||
|
||||
override onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.currentServerConfig = newConfig;
|
||||
super.onServerConfigChange(newConfig);
|
||||
this.updateWorkerServerConfig(newConfig);
|
||||
}
|
||||
|
||||
private updateWorkerServerConfig(newConfig: ServerConfig) {
|
||||
if (this.worker != null) {
|
||||
const request = buildSetConfigMessage({ newConfig });
|
||||
this.worker.postMessage(request);
|
||||
}
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
this.worker?.terminate();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import {
|
||||
DECRYPT_COMMAND,
|
||||
DecryptCommandData,
|
||||
SET_CONFIG_COMMAND,
|
||||
buildDecryptMessage,
|
||||
buildSetConfigMessage,
|
||||
} from "./worker-command.type";
|
||||
|
||||
describe("Worker command types", () => {
|
||||
describe("buildDecryptMessage", () => {
|
||||
it("builds a message with the correct command", () => {
|
||||
const commandData = createDecryptCommandData();
|
||||
|
||||
const result = buildDecryptMessage(commandData);
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(DECRYPT_COMMAND);
|
||||
});
|
||||
|
||||
it("includes the provided data in the message", () => {
|
||||
const mockItems = [{ encrypted: "test-encrypted" } as unknown as Decryptable<any>];
|
||||
const commandData = createDecryptCommandData(mockItems);
|
||||
|
||||
const result = buildDecryptMessage(commandData);
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(DECRYPT_COMMAND);
|
||||
expect(parsedResult.id).toBe("test-id");
|
||||
expect(parsedResult.items).toEqual(mockItems);
|
||||
expect(SymmetricCryptoKey.fromJSON(parsedResult.key)).toEqual(commandData.key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSetConfigMessage", () => {
|
||||
it("builds a message with the correct command", () => {
|
||||
const result = buildSetConfigMessage({ newConfig: mock<ServerConfig>() });
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(SET_CONFIG_COMMAND);
|
||||
});
|
||||
|
||||
it("includes the provided data in the message", () => {
|
||||
const serverConfig = { version: "test-version" } as unknown as ServerConfig;
|
||||
|
||||
const result = buildSetConfigMessage({ newConfig: serverConfig });
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(SET_CONFIG_COMMAND);
|
||||
expect(ServerConfig.fromJSON(parsedResult.newConfig).version).toEqual(serverConfig.version);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createDecryptCommandData(items?: Decryptable<any>[]): DecryptCommandData {
|
||||
return {
|
||||
id: "test-id",
|
||||
items: items ?? [],
|
||||
key: new SymmetricCryptoKey(makeStaticByteArray(64)),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export const DECRYPT_COMMAND = "decrypt";
|
||||
export const SET_CONFIG_COMMAND = "updateConfig";
|
||||
|
||||
export type DecryptCommandData = {
|
||||
id: string;
|
||||
items: Decryptable<any>[];
|
||||
key: SymmetricCryptoKey;
|
||||
};
|
||||
|
||||
export type ParsedDecryptCommandData = {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
};
|
||||
|
||||
type SetConfigCommandData = { newConfig: ServerConfig };
|
||||
|
||||
export function buildDecryptMessage(data: DecryptCommandData): string {
|
||||
return JSON.stringify({
|
||||
command: DECRYPT_COMMAND,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSetConfigMessage(data: SetConfigCommandData): string {
|
||||
return JSON.stringify({
|
||||
command: SET_CONFIG_COMMAND,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
@@ -17,11 +17,7 @@ import { SemVer } from "semver";
|
||||
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import {
|
||||
DefaultFeatureFlagValue,
|
||||
FeatureFlag,
|
||||
FeatureFlagValueType,
|
||||
} from "../../../enums/feature-flag.enum";
|
||||
import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
@@ -123,26 +119,13 @@ export class DefaultConfigService implements ConfigService {
|
||||
}
|
||||
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
|
||||
return this.serverConfig$.pipe(
|
||||
map((serverConfig) => this.getFeatureFlagValue(serverConfig, key)),
|
||||
);
|
||||
}
|
||||
|
||||
private getFeatureFlagValue<Flag extends FeatureFlag>(
|
||||
serverConfig: ServerConfig | null,
|
||||
flag: Flag,
|
||||
) {
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
|
||||
return DefaultFeatureFlagValue[flag];
|
||||
}
|
||||
|
||||
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
|
||||
return this.serverConfig$.pipe(map((serverConfig) => getFeatureFlagValue(serverConfig, key)));
|
||||
}
|
||||
|
||||
userCachedFeatureFlag$<Flag extends FeatureFlag>(key: Flag, userId: UserId) {
|
||||
return this.stateProvider
|
||||
.getUser(userId, USER_SERVER_CONFIG)
|
||||
.state$.pipe(map((config) => this.getFeatureFlagValue(config, key)));
|
||||
.state$.pipe(map((config) => getFeatureFlagValue(config, key)));
|
||||
}
|
||||
|
||||
async getFeatureFlag<Flag extends FeatureFlag>(key: Flag) {
|
||||
|
||||
@@ -206,3 +206,4 @@ export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
|
||||
export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
|
||||
export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk");
|
||||
export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk");
|
||||
export const VAULT_NUDGES_DISK = new StateDefinition("vaultNudges", "disk");
|
||||
|
||||
@@ -31,7 +31,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
abstract failedToDecryptCiphers$(userId: UserId): Observable<CipherView[]>;
|
||||
abstract failedToDecryptCiphers$(userId: UserId): Observable<CipherView[] | null>;
|
||||
abstract clearCache(userId: UserId): Promise<void>;
|
||||
abstract encrypt(
|
||||
model: CipherView,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTask } from "@bitwarden/vault";
|
||||
|
||||
import { SecurityTask } from "../models";
|
||||
|
||||
export abstract class TaskService {
|
||||
/**
|
||||
@@ -1,19 +1,18 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { DefaultTaskService, SecurityTaskStatus } from "@bitwarden/vault";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../common/spec";
|
||||
import { SecurityTaskData } from "../models/security-task.data";
|
||||
import { SecurityTaskResponse } from "../models/security-task.response";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { SecurityTaskStatus } from "../enums";
|
||||
import { SecurityTaskData, SecurityTaskResponse } from "../models";
|
||||
import { SECURITY_TASKS } from "../state/security-task.state";
|
||||
|
||||
import { DefaultTaskService } from "./default-task.service";
|
||||
|
||||
describe("Default task service", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
@@ -21,7 +20,7 @@ describe("Default task service", () => {
|
||||
const mockGetAllOrgs$ = jest.fn();
|
||||
const mockGetFeatureFlag$ = jest.fn();
|
||||
|
||||
let testBed: TestBed;
|
||||
let service: DefaultTaskService;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockApiSend.mockClear();
|
||||
@@ -29,34 +28,12 @@ describe("Default task service", () => {
|
||||
mockGetFeatureFlag$.mockClear();
|
||||
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
testBed = TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
DefaultTaskService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
getFeatureFlag$: mockGetFeatureFlag$,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: {
|
||||
send: mockApiSend,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: {
|
||||
organizations$: mockGetAllOrgs$,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
service = new DefaultTaskService(
|
||||
fakeStateProvider,
|
||||
{ send: mockApiSend } as unknown as ApiService,
|
||||
{ organizations$: mockGetAllOrgs$ } as unknown as OrganizationService,
|
||||
{ getFeatureFlag$: mockGetFeatureFlag$ } as unknown as ConfigService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("tasksEnabled$", () => {
|
||||
@@ -73,7 +50,7 @@ describe("Default task service", () => {
|
||||
] as Organization[]),
|
||||
);
|
||||
|
||||
const { tasksEnabled$ } = testBed.inject(DefaultTaskService);
|
||||
const { tasksEnabled$ } = service;
|
||||
|
||||
const result = await firstValueFrom(tasksEnabled$("user-id" as UserId));
|
||||
|
||||
@@ -93,7 +70,7 @@ describe("Default task service", () => {
|
||||
] as Organization[]),
|
||||
);
|
||||
|
||||
const { tasksEnabled$ } = testBed.inject(DefaultTaskService);
|
||||
const { tasksEnabled$ } = service;
|
||||
|
||||
const result = await firstValueFrom(tasksEnabled$("user-id" as UserId));
|
||||
|
||||
@@ -110,7 +87,7 @@ describe("Default task service", () => {
|
||||
] as Organization[]),
|
||||
);
|
||||
|
||||
const { tasksEnabled$ } = testBed.inject(DefaultTaskService);
|
||||
const { tasksEnabled$ } = service;
|
||||
|
||||
const result = await firstValueFrom(tasksEnabled$("user-id" as UserId));
|
||||
|
||||
@@ -130,7 +107,7 @@ describe("Default task service", () => {
|
||||
|
||||
fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null as any);
|
||||
|
||||
const { tasks$ } = testBed.inject(DefaultTaskService);
|
||||
const { tasks$ } = service;
|
||||
|
||||
const result = await firstValueFrom(tasks$("user-id" as UserId));
|
||||
|
||||
@@ -145,7 +122,7 @@ describe("Default task service", () => {
|
||||
} as SecurityTaskData,
|
||||
]);
|
||||
|
||||
const { tasks$ } = testBed.inject(DefaultTaskService);
|
||||
const { tasks$ } = service;
|
||||
|
||||
const result = await firstValueFrom(tasks$("user-id" as UserId));
|
||||
|
||||
@@ -154,7 +131,7 @@ describe("Default task service", () => {
|
||||
});
|
||||
|
||||
it("should share the same observable for the same user", async () => {
|
||||
const { tasks$ } = testBed.inject(DefaultTaskService);
|
||||
const { tasks$ } = service;
|
||||
|
||||
const first = tasks$("user-id" as UserId);
|
||||
const second = tasks$("user-id" as UserId);
|
||||
@@ -176,7 +153,7 @@ describe("Default task service", () => {
|
||||
},
|
||||
] as SecurityTaskData[]);
|
||||
|
||||
const { pendingTasks$ } = testBed.inject(DefaultTaskService);
|
||||
const { pendingTasks$ } = service;
|
||||
|
||||
const result = await firstValueFrom(pendingTasks$("user-id" as UserId));
|
||||
|
||||
@@ -195,8 +172,6 @@ describe("Default task service", () => {
|
||||
] as SecurityTaskResponse[],
|
||||
});
|
||||
|
||||
const service = testBed.inject(DefaultTaskService);
|
||||
|
||||
await service.refreshTasks("user-id" as UserId);
|
||||
|
||||
expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true);
|
||||
@@ -217,8 +192,6 @@ describe("Default task service", () => {
|
||||
null as any,
|
||||
);
|
||||
|
||||
const service = testBed.inject(DefaultTaskService);
|
||||
|
||||
await service.refreshTasks("user-id" as UserId);
|
||||
|
||||
expect(mock.nextMock).toHaveBeenCalledWith([
|
||||
@@ -237,8 +210,6 @@ describe("Default task service", () => {
|
||||
} as SecurityTaskData,
|
||||
]);
|
||||
|
||||
const service = testBed.inject(DefaultTaskService);
|
||||
|
||||
await service.clear("user-id" as UserId);
|
||||
|
||||
expect(mock.nextMock).toHaveBeenCalledWith([]);
|
||||
@@ -247,8 +218,6 @@ describe("Default task service", () => {
|
||||
|
||||
describe("markAsComplete()", () => {
|
||||
it("should send an API request to mark the task as complete", async () => {
|
||||
const service = testBed.inject(DefaultTaskService);
|
||||
|
||||
await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId);
|
||||
|
||||
expect(mockApiSend).toHaveBeenCalledWith(
|
||||
@@ -278,8 +247,6 @@ describe("Default task service", () => {
|
||||
} as SecurityTaskData,
|
||||
]);
|
||||
|
||||
const service = testBed.inject(DefaultTaskService);
|
||||
|
||||
await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId);
|
||||
|
||||
expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true);
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, map, switchMap } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -8,14 +7,13 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/vault";
|
||||
|
||||
import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities";
|
||||
import { SecurityTaskData } from "../models/security-task.data";
|
||||
import { SecurityTaskResponse } from "../models/security-task.response";
|
||||
import { TaskService } from "../abstractions/task.service";
|
||||
import { SecurityTaskStatus } from "../enums";
|
||||
import { SecurityTask, SecurityTaskData, SecurityTaskResponse } from "../models";
|
||||
import { SECURITY_TASKS } from "../state/security-task.state";
|
||||
|
||||
@Injectable()
|
||||
export class DefaultTaskService implements TaskService {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
@@ -48,11 +48,12 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
overlayX: "start",
|
||||
overlayY: "top",
|
||||
},
|
||||
// Fallback position: show above the trigger
|
||||
{
|
||||
originX: "end",
|
||||
originY: "bottom",
|
||||
overlayX: "end",
|
||||
overlayY: "top",
|
||||
originX: "start",
|
||||
originY: "top",
|
||||
overlayX: "start",
|
||||
overlayY: "bottom",
|
||||
},
|
||||
])
|
||||
.withLockedPosition(true)
|
||||
|
||||
@@ -14,11 +14,8 @@ or an options menu icon.
|
||||
|
||||
| Icon | bwi-name | Usage |
|
||||
| -------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <i class="bwi bwi-ban"></i> | bwi-ban | option or feature not available. Example: send maximum access count was reached |
|
||||
| <i class="bwi bwi-check"></i> | bwi-check | confirmation action (Example: "confirm member"), successful confirmation (toast or callout), or shows currently selected option in a menu. Use with success color variable if applicable. |
|
||||
| <i class="bwi bwi-error"></i> | bwi-error | error; used in form field error states and error toasts, banners, and callouts. Do not use as a close or clear icon. Use with danger color variable. |
|
||||
| <i class="bwi bwi-expired"></i> | bwi-expired | - |
|
||||
| <i class="bwi bwi-exclamation-circle"></i> | bwi-exclamation-circle | deprecated error icon; use bwi-error |
|
||||
| <i class="bwi bwi-exclamation-triangle"></i> | bwi-exclamation-triangle | warning; used in warning callouts, banners, and toasts. Use with warning color variable. |
|
||||
| <i class="bwi bwi-info-circle"></i> | bwi-info-circle | information; used in info callouts, banners, and toasts. Use with info color variable. |
|
||||
| <i class="bwi bwi-question-circle"></i> | bwi-question-circle | link to help documentation or hover tooltip |
|
||||
@@ -26,34 +23,31 @@ or an options menu icon.
|
||||
|
||||
## Bitwarden Objects
|
||||
|
||||
| Icon | bwi-name | Usage |
|
||||
| ------------------------------------- | ----------------- | --------------------------------------------------- |
|
||||
| <i class="bwi bwi-authenticator"></i> | bwi-authenticator | authenticator app |
|
||||
| <i class="bwi bwi-business"></i> | bwi-business | organization or vault for Free, Teams or Enterprise |
|
||||
| <i class="bwi bwi-collection"></i> | bwi-collection | collection |
|
||||
| <i class="bwi bwi-credit-card"></i> | bwi-credit-card | card item type |
|
||||
| <i class="bwi bwi-family"></i> | bwi-family | family vault or organization |
|
||||
| <i class="bwi bwi-folder"></i> | bwi-folder | folder |
|
||||
| <i class="bwi bwi-globe"></i> | bwi-globe | login item type |
|
||||
| <i class="bwi bwi-id-card"></i> | bwi-id-card | identity item type |
|
||||
| <i class="bwi bwi-send"></i> | bwi-send | send action or feature |
|
||||
| <i class="bwi bwi-send-f"></i> | bwi-send-f | - |
|
||||
| <i class="bwi bwi-sticky-note"></i> | bwi-sticky-note | secure note item type |
|
||||
| <i class="bwi bwi-users"></i> | bwi-users | user group |
|
||||
| <i class="bwi bwi-vault"></i> | bwi-vault | general vault |
|
||||
| <i class="bwi bwi-vault-f"></i> | bwi-vault-f | general vault |
|
||||
| Icon | bwi-name | Usage |
|
||||
| ----------------------------------- | --------------- | --------------------------------------------------- |
|
||||
| <i class="bwi bwi-business"></i> | bwi-business | organization or vault for Free, Teams or Enterprise |
|
||||
| <i class="bwi bwi-collection"></i> | bwi-collection | collection |
|
||||
| <i class="bwi bwi-credit-card"></i> | bwi-credit-card | card item type |
|
||||
| <i class="bwi bwi-family"></i> | bwi-family | family vault or organization |
|
||||
| <i class="bwi bwi-folder"></i> | bwi-folder | folder |
|
||||
| <i class="bwi bwi-globe"></i> | bwi-globe | login item type |
|
||||
| <i class="bwi bwi-id-card"></i> | bwi-id-card | identity item type |
|
||||
| <i class="bwi bwi-send"></i> | bwi-send | send action or feature |
|
||||
| <i class="bwi bwi-send-f"></i> | bwi-send-f | - |
|
||||
| <i class="bwi bwi-sticky-note"></i> | bwi-sticky-note | secure note item type |
|
||||
| <i class="bwi bwi-users"></i> | bwi-users | user group |
|
||||
| <i class="bwi bwi-vault"></i> | bwi-vault | general vault |
|
||||
| <i class="bwi bwi-vault-f"></i> | bwi-vault-f | general vault |
|
||||
|
||||
## Actions
|
||||
|
||||
| Icon | bwi-name | Usage |
|
||||
| -------------------------------------- | ------------------ | -------------------------------------------- |
|
||||
| <i class="bwi bwi-check-circle"></i> | bwi-check-circle | check if password has been exposed |
|
||||
| <i class="bwi bwi-check-square"></i> | bwi-check-square | select all action |
|
||||
| <i class="bwi bwi-clone"></i> | bwi-clone | copy to clipboard action |
|
||||
| <i class="bwi bwi-close"></i> | bwi-close | close action |
|
||||
| <i class="bwi bwi-cog"></i> | bwi-cog | settings |
|
||||
| <i class="bwi bwi-cog-f"></i> | bwi-cog-f | settings |
|
||||
| <i class="bwi bwi-cogs"></i> | bwi-cogs | deprecated; do not use in app. |
|
||||
| <i class="bwi bwi-download"></i> | bwi-download | download or export |
|
||||
| <i class="bwi bwi-envelope"></i> | bwi-envelope | action related to emailing a user |
|
||||
| <i class="bwi bwi-external-link"></i> | bwi-external-link | open in new window or popout |
|
||||
@@ -66,141 +60,81 @@ or an options menu icon.
|
||||
| <i class="bwi bwi-lock-encrypted"></i> | bwi-lock-encrypted | - |
|
||||
| <i class="bwi bwi-lock-f"></i> | bwi-lock-f | - |
|
||||
| <i class="bwi bwi-minus-circle"></i> | bwi-minus-circle | remove action |
|
||||
| <i class="bwi bwi-minus-square"></i> | bwi-minus-square | unselect all action |
|
||||
| <i class="bwi bwi-paste"></i> | bwi-paste | paste from clipboard action |
|
||||
| <i class="bwi bwi-pencil-square"></i> | bwi-pencil-square | edit action |
|
||||
| <i class="bwi bwi-popout"></i> | bwi-popout | popout action |
|
||||
| <i class="bwi bwi-play"></i> | bwi-play | start or play action |
|
||||
| <i class="bwi bwi-plus"></i> | bwi-plus | new or add option in contained buttons/links |
|
||||
| <i class="bwi bwi-plus-f"></i> | bwi-plus-f | new or add option in contained buttons/links |
|
||||
| <i class="bwi bwi-plus-circle"></i> | bwi-plus-circle | new or add option in text buttons/links |
|
||||
| <i class="bwi bwi-plus-square"></i> | bwi-plus-square | - |
|
||||
| <i class="bwi bwi-refresh"></i> | bwi-refresh | "re"-action; such as refresh or regenerate |
|
||||
| <i class="bwi bwi-refresh-tab"></i> | bwi-refresh-tab | - |
|
||||
| <i class="bwi bwi-save"></i> | bwi-save | alternate download action |
|
||||
| <i class="bwi bwi-save-changes"></i> | bwi-save-changes | save changes action |
|
||||
| <i class="bwi bwi-search"></i> | bwi-search | search action |
|
||||
| <i class="bwi bwi-share"></i> | bwi-share | - |
|
||||
| <i class="bwi bwi-share-arrow"></i> | bwi-share-arrow | - |
|
||||
| <i class="bwi bwi-share-square"></i> | bwi-share-square | avoid using; use external-link instead |
|
||||
| <i class="bwi bwi-sign-in"></i> | bwi-sign-in | sign-in action |
|
||||
| <i class="bwi bwi-sign-out"></i> | bwi-sign-out | sign-out action |
|
||||
| <i class="bwi bwi-star"></i> | bwi-star | favorite action |
|
||||
| <i class="bwi bwi-star-f"></i> | bwi-star-f | favorited / unfavorite action |
|
||||
| <i class="bwi bwi-stop"></i> | bwi-stop | stop action |
|
||||
| <i class="bwi bwi-trash"></i> | bwi-trash | delete action or trash area |
|
||||
| <i class="bwi bwi-undo"></i> | bwi-undo | restore action |
|
||||
| <i class="bwi bwi-unlock"></i> | bwi-unlock | unlocked |
|
||||
|
||||
## Directional and Menu Indicators
|
||||
|
||||
| Icon | bwi-name | Usage |
|
||||
| ------------------------------------------ | ---------------------- | ------------------------------------------------------- |
|
||||
| <i class="bwi bwi-angle-down"></i> | bwi-angle-down | closed dropdown or open expandable section |
|
||||
| <i class="bwi bwi-angle-left"></i> | bwi-angle-left | - |
|
||||
| <i class="bwi bwi-angle-right"></i> | bwi-angle-right | closed expandable section |
|
||||
| <i class="bwi bwi-angle-up"></i> | bwi-angle-up | open dropdown |
|
||||
| <i class="bwi bwi-arrow-circle-down"></i> | bwi-arrow-circle-down | - |
|
||||
| <i class="bwi bwi-arrow-circle-left"></i> | bwi-arrow-circle-left | - |
|
||||
| <i class="bwi bwi-arrow-circle-right"></i> | bwi-arrow-circle-right | - |
|
||||
| <i class="bwi bwi-arrow-circle-up"></i> | bwi-arrow-circle-up | - |
|
||||
| <i class="bwi bwi-back"></i> | bwi-back | back arrow |
|
||||
| <i class="bwi bwi-caret-down"></i> | bwi-caret-down | table sort order |
|
||||
| <i class="bwi bwi-caret-right"></i> | bwi-caret-right | - |
|
||||
| <i class="bwi bwi-caret-up"></i> | bwi-caret-up | table sort order |
|
||||
| <i class="bwi bwi-dbl-angle-left"></i> | bwi-dbl-angle-left | - |
|
||||
| <i class="bwi bwi-dbl-angle-right"></i> | bwi-dbl-angle-right | - |
|
||||
| <i class="bwi bwi-down-solid"></i> | bwi-down-solid | table sort order |
|
||||
| <i class="bwi bwi-ellipsis-h"></i> | bwi-ellipsis-h | more options menu horizontal; used in mobile list items |
|
||||
| <i class="bwi bwi-ellipsis-v"></i> | bwi-ellipsis-v | more options menu vertical; used primarily in tables |
|
||||
| <i class="bwi bwi-filter"></i> | bwi-filter | Product switcher |
|
||||
| <i class="bwi bwi-hamburger"></i> | bwi-hamburger | navigation indicator |
|
||||
| <i class="bwi bwi-list"></i> | bwi-list | toggle list/grid view |
|
||||
| <i class="bwi bwi-list-alt"></i> | bwi-list-alt | view item action in extension |
|
||||
| <i class="bwi bwi-long-arrow-right"></i> | bwi-long-arrow-right | - |
|
||||
| <i class="bwi bwi-numbered-list"></i> | bwi-numbered-list | toggle numbered list view |
|
||||
| <i class="bwi bwi-up-down-btn"></i> | bwi-up-down-btn | table sort order |
|
||||
| <i class="bwi bwi-up-solid"></i> | bwi-up-solid | table sort order |
|
||||
| Icon | bwi-name | Usage |
|
||||
| ------------------------------------- | ----------------- | ------------------------------------------------------- |
|
||||
| <i class="bwi bwi-angle-down"></i> | bwi-angle-down | closed dropdown or open expandable section |
|
||||
| <i class="bwi bwi-angle-left"></i> | bwi-angle-left | - |
|
||||
| <i class="bwi bwi-angle-right"></i> | bwi-angle-right | closed expandable section |
|
||||
| <i class="bwi bwi-angle-up"></i> | bwi-angle-up | open dropdown |
|
||||
| <i class="bwi bwi-back"></i> | bwi-back | back arrow |
|
||||
| <i class="bwi bwi-down-solid"></i> | bwi-down-solid | table sort order |
|
||||
| <i class="bwi bwi-ellipsis-h"></i> | bwi-ellipsis-h | more options menu horizontal; used in mobile list items |
|
||||
| <i class="bwi bwi-ellipsis-v"></i> | bwi-ellipsis-v | more options menu vertical; used primarily in tables |
|
||||
| <i class="bwi bwi-filter"></i> | bwi-filter | Product switcher |
|
||||
| <i class="bwi bwi-hamburger"></i> | bwi-hamburger | navigation indicator |
|
||||
| <i class="bwi bwi-list"></i> | bwi-list | toggle list/grid view |
|
||||
| <i class="bwi bwi-list-alt"></i> | bwi-list-alt | view item action in extension |
|
||||
| <i class="bwi bwi-numbered-list"></i> | bwi-numbered-list | toggle numbered list view |
|
||||
| <i class="bwi bwi-up-down-btn"></i> | bwi-up-down-btn | table sort order |
|
||||
| <i class="bwi bwi-up-solid"></i> | bwi-up-solid | table sort order |
|
||||
|
||||
## Misc Objects
|
||||
|
||||
| Icon | bwi-name | Usage |
|
||||
| ----------------------------------------- | --------------------- | ---------------------------------------------- |
|
||||
| <i class="bwi bwi-bank"></i> | bwi-bank | - |
|
||||
| <i class="bwi bwi-billing"></i> | bwi-billing | billing options |
|
||||
| <i class="bwi bwi-bitcoin"></i> | bwi-bitcoin | crypto |
|
||||
| <i class="bwi bwi-bolt"></i> | bwi-bolt | deprecated "danger" icon |
|
||||
| <i class="bwi bwi-bookmark"></i> | bwi-bookmark | bookmark or save related actions |
|
||||
| <i class="bwi bwi-browser"></i> | bwi-browser | web browser |
|
||||
| <i class="bwi bwi-browser-alt"></i> | bwi-browser-alt | web browser |
|
||||
| <i class="bwi bwi-bug"></i> | bwi-bug | test or debug action |
|
||||
| <i class="bwi bwi-camera"></i> | bwi-camera | actions related to camera use |
|
||||
| <i class="bwi bwi-chain-broken"></i> | bwi-chain-broken | unlink action |
|
||||
| <i class="bwi bwi-chat"></i> | bwi-chat | - |
|
||||
| <i class="bwi bwi-cli"></i> | bwi-cli | cli client or code |
|
||||
| <i class="bwi bwi-clock"></i> | bwi-clock | use for time based actions or views |
|
||||
| <i class="bwi bwi-community"></i> | bwi-community | - |
|
||||
| <i class="bwi bwi-cut"></i> | bwi-cut | cut or omit actions |
|
||||
| <i class="bwi bwi-dashboard"></i> | bwi-dashboard | statuses or dashboard views |
|
||||
| <i class="bwi bwi-desktop"></i> | bwi-desktop | desktop client |
|
||||
| <i class="bwi bwi-desktop-alt"></i> | bwi-desktop-alt | desktop client |
|
||||
| <i class="bwi bwi-dollar"></i> | bwi-dollar | account credit |
|
||||
| <i class="bwi bwi-file"></i> | bwi-file | file related objects or actions |
|
||||
| <i class="bwi bwi-file-pdf"></i> | bwi-file-pdf | PDF related object or actions |
|
||||
| <i class="bwi bwi-file-text"></i> | bwi-file-text | text related objects or actions |
|
||||
| <i class="bwi bwi-fingerprint"></i> | bwi-fingerprint | - |
|
||||
| <i class="bwi bwi-bw-folder-open-f1"></i> | bwi-bw-folder-open-f1 | - |
|
||||
| <i class="bwi bwi-folder-closed-f"></i> | bwi-folder-closed-f | - |
|
||||
| <i class="bwi bwi-folder-open"></i> | bwi-folder-open | - |
|
||||
| <i class="bwi bwi-frown"></i> | bwi-frown | - |
|
||||
| <i class="bwi bwi-hashtag"></i> | bwi-hashtag | link to specific id |
|
||||
| <i class="bwi bwi-icon-1"></i> | bwi-icon-1 | - |
|
||||
| <i class="bwi bwi-icon-2"></i> | bwi-icon-2 | - |
|
||||
| <i class="bwi bwi-icon-3"></i> | bwi-icon-3 | - |
|
||||
| <i class="bwi bwi-icon-4"></i> | bwi-icon-4 | - |
|
||||
| <i class="bwi bwi-icon-5"></i> | bwi-icon-5 | - |
|
||||
| <i class="bwi bwi-icon-6"></i> | bwi-icon-6 | - |
|
||||
| <i class="bwi bwi-icon-7"></i> | bwi-icon-7 | - |
|
||||
| <i class="bwi bwi-icon-8"></i> | bwi-icon-8 | - |
|
||||
| <i class="bwi bwi-icon-9"></i> | bwi-icon-9 | - |
|
||||
| <i class="bwi bwi-insurance"></i> | bwi-insurance | - |
|
||||
| <i class="bwi bwi-key"></i> | bwi-key | key or password related objects or actions |
|
||||
| <i class="bwi bwi-learning"></i> | bwi-learning | learning center |
|
||||
| <i class="bwi bwi-lightbulb"></i> | bwi-lightbulb | - |
|
||||
| <i class="bwi bwi-link"></i> | bwi-link | link action |
|
||||
| <i class="bwi bwi-mobile"></i> | bwi-mobile | mobile client |
|
||||
| <i class="bwi bwi-mobile-alt"></i> | bwi-mobile-alt | mobile client |
|
||||
| <i class="bwi bwi-money"></i> | bwi-money | - |
|
||||
| <i class="bwi bwi-msp"></i> | bwi-msp | - |
|
||||
| <i class="bwi bwi-paperclip"></i> | bwi-paperclip | attachments |
|
||||
| <i class="bwi bwi-passkey"></i> | bwi-passkey | passkey |
|
||||
| <i class="bwi bwi-pencil"></i> | bwi-pencil | editing |
|
||||
| <i class="bwi bwi-provider"></i> | bwi-provider | relates to provider or provider portal |
|
||||
| <i class="bwi bwi-providers"></i> | bwi-providers | - |
|
||||
| <i class="bwi bwi-puzzle"></i> | bwi-puzzle | - |
|
||||
| <i class="bwi bwi-rocket"></i> | bwi-rocket | - |
|
||||
| <i class="bwi bwi-rss"></i> | bwi-rss | - |
|
||||
| <i class="bwi bwi-search-book"></i> | bwi-search-book | - |
|
||||
| <i class="bwi bwi-server"></i> | bwi-server | - |
|
||||
| <i class="bwi bwi-shield"></i> | bwi-shield | - |
|
||||
| <i class="bwi bwi-sitemap"></i> | bwi-sitemap | - |
|
||||
| <i class="bwi bwi-sliders"></i> | bwi-sliders | reporting or filtering |
|
||||
| <i class="bwi bwi-software-license"></i> | bwi-software-license | - |
|
||||
| <i class="bwi bwi-square"></i> | bwi-square | - |
|
||||
| <i class="bwi bwi-tag"></i> | bwi-tag | labels |
|
||||
| <i class="bwi bwi-thumb-tack"></i> | bwi-thumb-tack | - |
|
||||
| <i class="bwi bwi-thumbs-up"></i> | bwi-thumbs-up | - |
|
||||
| <i class="bwi bwi-totp-codes"></i> | bwi-totp-codes | - |
|
||||
| <i class="bwi bwi-totp-codes-alt"></i> | bwi-totp-codes-alt | - |
|
||||
| <i class="bwi bwi-totp-codes-alt2"></i> | bwi-totp-codes-alt2 | - |
|
||||
| <i class="bwi bwi-universal-access"></i> | bwi-universal-access | use for accessibility related actions |
|
||||
| <i class="bwi bwi-user"></i> | bwi-user | relates to current user or organization member |
|
||||
| <i class="bwi bwi-user-circle"></i> | bwi-user-circle | - |
|
||||
| <i class="bwi bwi-user-f"></i> | bwi-user-f | - |
|
||||
| <i class="bwi bwi-user-monitor"></i> | bwi-user-monitor | - |
|
||||
| <i class="bwi bwi-wand"></i> | bwi-wand | - |
|
||||
| <i class="bwi bwi-wireless"></i> | bwi-wireless | - |
|
||||
| <i class="bwi bwi-wrench"></i> | bwi-wrench | tools or additional configuration options |
|
||||
| Icon | bwi-name | Usage |
|
||||
| ---------------------------------------- | -------------------- | ---------------------------------------------- |
|
||||
| <i class="bwi bwi-billing"></i> | bwi-billing | billing options |
|
||||
| <i class="bwi bwi-bitcoin"></i> | bwi-bitcoin | crypto |
|
||||
| <i class="bwi bwi-browser"></i> | bwi-browser | web browser |
|
||||
| <i class="bwi bwi-browser-alt"></i> | bwi-browser-alt | web browser |
|
||||
| <i class="bwi bwi-bug"></i> | bwi-bug | test or debug action |
|
||||
| <i class="bwi bwi-camera"></i> | bwi-camera | actions related to camera use |
|
||||
| <i class="bwi bwi-cli"></i> | bwi-cli | cli client or code |
|
||||
| <i class="bwi bwi-clock"></i> | bwi-clock | use for time based actions or views |
|
||||
| <i class="bwi bwi-community"></i> | bwi-community | - |
|
||||
| <i class="bwi bwi-desktop"></i> | bwi-desktop | desktop client |
|
||||
| <i class="bwi bwi-dollar"></i> | bwi-dollar | account credit |
|
||||
| <i class="bwi bwi-file"></i> | bwi-file | file related objects or actions |
|
||||
| <i class="bwi bwi-file-text"></i> | bwi-file-text | text related objects or actions |
|
||||
| <i class="bwi bwi-hashtag"></i> | bwi-hashtag | link to specific id |
|
||||
| <i class="bwi bwi-key"></i> | bwi-key | key or password related objects or actions |
|
||||
| <i class="bwi bwi-link"></i> | bwi-link | link action |
|
||||
| <i class="bwi bwi-mobile"></i> | bwi-mobile | mobile client |
|
||||
| <i class="bwi bwi-msp"></i> | bwi-msp | - |
|
||||
| <i class="bwi bwi-paperclip"></i> | bwi-paperclip | attachments |
|
||||
| <i class="bwi bwi-passkey"></i> | bwi-passkey | passkey |
|
||||
| <i class="bwi bwi-pencil"></i> | bwi-pencil | editing |
|
||||
| <i class="bwi bwi-provider"></i> | bwi-provider | relates to provider or provider portal |
|
||||
| <i class="bwi bwi-puzzle"></i> | bwi-puzzle | - |
|
||||
| <i class="bwi bwi-shield"></i> | bwi-shield | - |
|
||||
| <i class="bwi bwi-sliders"></i> | bwi-sliders | reporting or filtering |
|
||||
| <i class="bwi bwi-tag"></i> | bwi-tag | labels |
|
||||
| <i class="bwi bwi-totp-codes"></i> | bwi-totp-codes | - |
|
||||
| <i class="bwi bwi-totp-codes-alt"></i> | bwi-totp-codes-alt | - |
|
||||
| <i class="bwi bwi-totp-codes-alt2"></i> | bwi-totp-codes-alt2 | - |
|
||||
| <i class="bwi bwi-universal-access"></i> | bwi-universal-access | use for accessibility related actions |
|
||||
| <i class="bwi bwi-user"></i> | bwi-user | relates to current user or organization member |
|
||||
| <i class="bwi bwi-user-monitor"></i> | bwi-user-monitor | - |
|
||||
| <i class="bwi bwi-wireless"></i> | bwi-wireless | - |
|
||||
| <i class="bwi bwi-wrench"></i> | bwi-wrench | tools or additional configuration options |
|
||||
|
||||
## Platforms and Logos
|
||||
|
||||
|
||||
@@ -736,6 +736,52 @@ describe("keyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrDeriveMasterKey", () => {
|
||||
it("returns the master key if it is already available", async () => {
|
||||
const getMasterKey = jest
|
||||
.spyOn(masterPasswordService, "masterKey$")
|
||||
.mockReturnValue(of("masterKey" as any));
|
||||
|
||||
const result = await keyService.getOrDeriveMasterKey("password", mockUserId);
|
||||
|
||||
expect(getMasterKey).toHaveBeenCalledWith(mockUserId);
|
||||
expect(result).toEqual("masterKey");
|
||||
});
|
||||
|
||||
it("derives the master key if it is not available", async () => {
|
||||
const getMasterKey = jest
|
||||
.spyOn(masterPasswordService, "masterKey$")
|
||||
.mockReturnValue(of(null as any));
|
||||
|
||||
const deriveKeyFromPassword = jest
|
||||
.spyOn(keyGenerationService, "deriveKeyFromPassword")
|
||||
.mockResolvedValue("mockMasterKey" as any);
|
||||
|
||||
kdfConfigService.getKdfConfig$.mockReturnValue(of("mockKdfConfig" as any));
|
||||
|
||||
const result = await keyService.getOrDeriveMasterKey("password", mockUserId);
|
||||
|
||||
expect(getMasterKey).toHaveBeenCalledWith(mockUserId);
|
||||
expect(deriveKeyFromPassword).toHaveBeenCalledWith("password", "email", "mockKdfConfig");
|
||||
expect(result).toEqual("mockMasterKey");
|
||||
});
|
||||
|
||||
it("throws an error if no user is found", async () => {
|
||||
accountService.activeAccountSubject.next(null);
|
||||
|
||||
await expect(keyService.getOrDeriveMasterKey("password")).rejects.toThrow("No user found");
|
||||
});
|
||||
|
||||
it("throws an error if no kdf config is found", async () => {
|
||||
jest.spyOn(masterPasswordService, "masterKey$").mockReturnValue(of(null as any));
|
||||
kdfConfigService.getKdfConfig$.mockReturnValue(of(null));
|
||||
|
||||
await expect(keyService.getOrDeriveMasterKey("password", mockUserId)).rejects.toThrow(
|
||||
"No kdf found for user",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareKeyHash", () => {
|
||||
type TestCase = {
|
||||
masterKey: MasterKey;
|
||||
|
||||
@@ -287,10 +287,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
),
|
||||
);
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
|
||||
return (
|
||||
masterKey ||
|
||||
(await this.makeMasterKey(password, email, await this.kdfConfigService.getKdfConfig()))
|
||||
);
|
||||
if (masterKey != null) {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
const kdf = await firstValueFrom(this.kdfConfigService.getKdfConfig$(resolvedUserId));
|
||||
if (kdf == null) {
|
||||
throw new Error("No kdf found for user");
|
||||
}
|
||||
return await this.makeMasterKey(password, email, kdf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<vault-autofill-uri-option
|
||||
*ngFor="let uri of uriControls; let i = index"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="uriControls.length <= 1"
|
||||
[formControlName]="i"
|
||||
(remove)="removeUri(i)"
|
||||
(onKeydown)="onUriItemKeydown($event, i)"
|
||||
|
||||
@@ -20,10 +20,10 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { AnchorLinkDirective, CalloutModule, SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service";
|
||||
import { TaskService, SecurityTaskType } from "../tasks";
|
||||
|
||||
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
|
||||
import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component";
|
||||
|
||||
@@ -6,8 +6,6 @@ export { OrgIconDirective } from "./components/org-icon.directive";
|
||||
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
|
||||
export { DarkImageSourceDirective } from "./components/dark-image-source.directive";
|
||||
|
||||
export * from "./utils/observable-utilities";
|
||||
|
||||
export * from "./cipher-view";
|
||||
export * from "./cipher-form";
|
||||
export {
|
||||
@@ -25,8 +23,8 @@ export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.compon
|
||||
export * from "./components/carousel";
|
||||
|
||||
export * as VaultIcons from "./icons";
|
||||
export * from "./tasks";
|
||||
export * from "./notifications";
|
||||
export * from "./services/vault-nudges.service";
|
||||
|
||||
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||
|
||||
@@ -7,8 +7,11 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
filterOutNullish,
|
||||
perUserCache$,
|
||||
} from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
|
||||
import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities";
|
||||
import { EndUserNotificationService } from "../abstractions/end-user-notification.service";
|
||||
import { NotificationView, NotificationViewData, NotificationViewResponse } from "../models";
|
||||
import { NOTIFICATIONS } from "../state/end-user-notification.state";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { VaultNudgeType } from "../vault-nudges.service";
|
||||
|
||||
/**
|
||||
* Custom Nudge Service to use for the Onboarding Nudges in the Vault
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class HasItemsNudgeService extends DefaultSingleNudgeService {
|
||||
cipherService = inject(CipherService);
|
||||
|
||||
shouldShowNudge$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.isDismissed$(nudgeType, userId).pipe(
|
||||
switchMap((dismissed) =>
|
||||
dismissed
|
||||
? of(false)
|
||||
: this.cipherService
|
||||
.cipherViews$(userId)
|
||||
.pipe(map((ciphers) => ciphers == null || ciphers.length === 0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
libs/vault/src/services/custom-nudges-services/index.ts
Normal file
1
libs/vault/src/services/custom-nudges-services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./has-items-nudge.service";
|
||||
52
libs/vault/src/services/default-single-nudge.service.ts
Normal file
52
libs/vault/src/services/default-single-nudge.service.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { VAULT_NUDGE_DISMISSED_DISK_KEY, VaultNudgeType } from "./vault-nudges.service";
|
||||
|
||||
/**
|
||||
* Base interface for handling a nudge's status
|
||||
*/
|
||||
export interface SingleNudgeService {
|
||||
shouldShowNudge$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean>;
|
||||
|
||||
setNudgeStatus(nudgeType: VaultNudgeType, dismissed: boolean, userId: UserId): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation for nudges. Set and Show Nudge dismissed state
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class DefaultSingleNudgeService implements SingleNudgeService {
|
||||
stateProvider = inject(StateProvider);
|
||||
|
||||
protected isDismissed$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.stateProvider
|
||||
.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY)
|
||||
.state$.pipe(map((nudges) => nudges?.includes(nudgeType) ?? false));
|
||||
}
|
||||
|
||||
shouldShowNudge$(nudgeType: VaultNudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.isDismissed$(nudgeType, userId).pipe(map((dismissed) => !dismissed));
|
||||
}
|
||||
|
||||
async setNudgeStatus(
|
||||
nudgeType: VaultNudgeType,
|
||||
dismissed: boolean,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
|
||||
nudges ??= [];
|
||||
if (dismissed) {
|
||||
nudges.push(nudgeType);
|
||||
} else {
|
||||
nudges = nudges.filter((n) => n !== nudgeType);
|
||||
}
|
||||
return nudges;
|
||||
});
|
||||
}
|
||||
}
|
||||
102
libs/vault/src/services/vault-nudges.service.spec.ts
Normal file
102
libs/vault/src/services/vault-nudges.service.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec";
|
||||
|
||||
import { HasItemsNudgeService } from "./custom-nudges-services/has-items-nudge.service";
|
||||
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
|
||||
import { VaultNudgesService, VaultNudgeType } from "./vault-nudges.service";
|
||||
|
||||
describe("Vault Nudges Service", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
let testBed: TestBed;
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
|
||||
testBed = TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultNudgesService,
|
||||
},
|
||||
{
|
||||
provide: DefaultSingleNudgeService,
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: HasItemsNudgeService,
|
||||
useValue: mock<HasItemsNudgeService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultSingleNudgeService", () => {
|
||||
it("should return shouldShowNudge === false when IntroCaourselDismissal dismissed is true", async () => {
|
||||
const service = testBed.inject(DefaultSingleNudgeService);
|
||||
|
||||
await service.setNudgeStatus(
|
||||
VaultNudgeType.IntroCarouselDismissal,
|
||||
true,
|
||||
"user-id" as UserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.shouldShowNudge$(VaultNudgeType.IntroCarouselDismissal, "user-id" as UserId),
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return shouldShowNudge === true when IntroCaourselDismissal dismissed is false", async () => {
|
||||
const service = testBed.inject(DefaultSingleNudgeService);
|
||||
|
||||
await service.setNudgeStatus(
|
||||
VaultNudgeType.IntroCarouselDismissal,
|
||||
false,
|
||||
"user-id" as UserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.shouldShowNudge$(VaultNudgeType.IntroCarouselDismissal, "user-id" as UserId),
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VaultNudgesService", () => {
|
||||
it("should return true, the proper value from the custom nudge service shouldShowNudge$", async () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: { shouldShowNudge$: () => of(true) },
|
||||
});
|
||||
const service = testBed.inject(VaultNudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudge$(VaultNudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false, the proper value for the custom nudge service shouldShowNudge$", async () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: { shouldShowNudge$: () => of(false) },
|
||||
});
|
||||
const service = testBed.inject(VaultNudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudge$(VaultNudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
70
libs/vault/src/services/vault-nudges.service.ts
Normal file
70
libs/vault/src/services/vault-nudges.service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
|
||||
import { UserKeyDefinition, VAULT_NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { HasItemsNudgeService } from "./custom-nudges-services/has-items-nudge.service";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
|
||||
/**
|
||||
* Enum to list the various nudge types, to be used by components/badges to show/hide the nudge
|
||||
*/
|
||||
export enum VaultNudgeType {
|
||||
/** Nudge to show when user has no items in their vault
|
||||
* Add future nudges here
|
||||
*/
|
||||
HasVaultItems = "has-vault-items",
|
||||
IntroCarouselDismissal = "intro-carousel-dismissal",
|
||||
}
|
||||
|
||||
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<VaultNudgeType[]>(
|
||||
VAULT_NUDGES_DISK,
|
||||
"vaultNudgeDismissed",
|
||||
{
|
||||
deserializer: (nudgeDismissed) => nudgeDismissed,
|
||||
clearOn: [], // Do not clear dismissals
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class VaultNudgesService {
|
||||
/**
|
||||
* Custom nudge services to use for specific nudge types
|
||||
* Each nudge type can have its own service to determine when to show the nudge
|
||||
* @private
|
||||
*/
|
||||
private customNudgeServices: any = {
|
||||
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||
};
|
||||
|
||||
/**
|
||||
* Default nudge service to use when no custom service is available
|
||||
* Simply stores the dismissed state in the user's state
|
||||
* @private
|
||||
*/
|
||||
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
||||
|
||||
private getNudgeService(nudge: VaultNudgeType): SingleNudgeService {
|
||||
return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a nudge should be shown to the user
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
showNudge$(nudge: VaultNudgeType, userId: UserId) {
|
||||
return this.getNudgeService(nudge).shouldShowNudge$(nudge, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a nudge for the user so that it is not shown again
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
dismissNudge(nudge: VaultNudgeType, userId: UserId) {
|
||||
return this.getNudgeService(nudge).setNudgeStatus(nudge, true, userId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user