mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
@@ -1309,6 +1309,13 @@ export default class MainBackground {
|
||||
// Only the "true" background should run migrations
|
||||
await this.stateService.init({ runMigrations: true });
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
// This is here instead of in in the InitService b/c we don't plan for
|
||||
// side effects to run in the Browser InitService.
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
|
||||
@@ -3,6 +3,9 @@ import { inject, Inject, Injectable } from "@angular/core";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -27,6 +30,9 @@ export class InitService {
|
||||
private themingService: AbstractThemingService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private viewCacheService: PopupViewCacheService,
|
||||
private configService: ConfigService,
|
||||
private encryptService: EncryptService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -34,6 +40,12 @@ export class InitService {
|
||||
return async () => {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
await this.viewCacheService.init();
|
||||
|
||||
@@ -4,8 +4,9 @@ import { switchMap, tap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { filterOutNullish, TaskService } from "@bitwarden/vault";
|
||||
|
||||
export const canAccessAtRiskPasswords: CanActivateFn = () => {
|
||||
const accountService = inject(AccountService);
|
||||
|
||||
@@ -4,9 +4,10 @@ import { RouterModule } from "@angular/router";
|
||||
import { map, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { AnchorLinkDirective, CalloutModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { filterOutNullish, SecurityTaskType, TaskService } from "@bitwarden/vault";
|
||||
|
||||
// TODO: This component will need to be reworked to use the new EndUserNotificationService in PM-10609
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
PasswordRepromptService,
|
||||
SecurityTask,
|
||||
SecurityTaskType,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||
|
||||
@@ -15,6 +15,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
@@ -28,10 +30,7 @@ import {
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
filterOutNullish,
|
||||
PasswordRepromptService,
|
||||
SecurityTaskType,
|
||||
TaskService,
|
||||
VaultCarouselModule,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.cipherService
|
||||
.failedToDecryptCiphers$(activeUserId)
|
||||
.pipe(
|
||||
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
|
||||
map((ciphers) => (ciphers ? ciphers.filter((c) => !c.isDeleted) : [])),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
|
||||
@@ -33,19 +33,17 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
SearchModule,
|
||||
ToastService,
|
||||
CalloutModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
CopyCipherFieldService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
@@ -95,7 +93,6 @@ type LoadAction =
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ export class VaultPopupItemsService {
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
]),
|
||||
),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [...(failedToDecryptCiphers || []), ...ciphers]),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
@@ -360,10 +359,10 @@ export class VaultPopupListFiltersService {
|
||||
switchMap((userId) => {
|
||||
// Observable of cipher views
|
||||
const cipherViews$ = this.cipherService.cipherViews$(userId).pipe(
|
||||
tap((cipherViews) => {
|
||||
this.cipherViews = Object.values(cipherViews);
|
||||
map((ciphers) => {
|
||||
this.cipherViews = ciphers ? Object.values(ciphers) : [];
|
||||
return this.cipherViews;
|
||||
}),
|
||||
map((ciphers) => Object.values(ciphers)),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
|
||||
@@ -283,6 +283,7 @@ export class ServiceContainer {
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
|
||||
bulkEncryptService: FallbackBulkEncryptService;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -314,6 +315,7 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
|
||||
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
|
||||
this.secureStorageService = new NodeEnvSecureStorageService(
|
||||
this.storageService,
|
||||
@@ -686,7 +688,7 @@ export class ServiceContainer {
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.encryptService,
|
||||
new FallbackBulkEncryptService(this.encryptService),
|
||||
this.bulkEncryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
@@ -885,6 +887,12 @@ export class ServiceContainer {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.storageService.init();
|
||||
await this.stateService.init();
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
this.containerService.attachToGlobal(global);
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
|
||||
@@ -7,8 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
@@ -49,6 +51,8 @@ export class InitService {
|
||||
private sshAgentService: SshAgentService,
|
||||
private autofillService: DesktopAutofillService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -59,6 +63,13 @@ export class InitService {
|
||||
this.nativeMessagingService.init();
|
||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const setUserKeyInMemoryPromises = [];
|
||||
for (const userId of Object.keys(accounts) as UserId[]) {
|
||||
|
||||
@@ -282,14 +282,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary btn-submit"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
*ngIf="!disableSend"
|
||||
>
|
||||
<button type="submit" class="primary btn-submit" *ngIf="!disableSend">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span><i class="bwi bwi-save-changes bwi-lg bwi-fw" aria-hidden="true"></i></span>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.maxAccessCountReached">
|
||||
<i
|
||||
class="bwi bwi-ban"
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -3561,5 +3561,8 @@
|
||||
},
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Change at-risk password"
|
||||
},
|
||||
"move": {
|
||||
"message": "Move"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,17 +773,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="$any(form).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="$any(form).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="$any(form).loading">
|
||||
<span [hidden]="$any(form).loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(form).loading"
|
||||
@@ -797,10 +788,9 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="share()"
|
||||
appA11yTitle="{{ 'moveToOrganization' | i18n }}"
|
||||
*ngIf="editMode && cipher && !cipher.organizationId && !cloneMode"
|
||||
>
|
||||
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
{{ "move" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
#deleteBtn
|
||||
|
||||
@@ -54,17 +54,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -28,17 +28,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -21,17 +21,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -58,15 +58,10 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading || !canSave"
|
||||
*ngIf="organizations && organizations.length"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngFor="let field of cipher.fields; index as i"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
*ngIf="field.type != fieldType.Linked"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, field.value)"
|
||||
>{{ field.name }}</span
|
||||
[id]="'customField-' + i"
|
||||
>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">{{ field.name }}</span>
|
||||
{{ field.name }}
|
||||
</span>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">
|
||||
{{ "cfTypeLinked" | i18n }}: {{ field.name }}
|
||||
</span>
|
||||
<div *ngIf="field.type === fieldType.Text">
|
||||
{{ field.value || " " }}
|
||||
</div>
|
||||
@@ -29,19 +36,14 @@
|
||||
></span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Boolean">
|
||||
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ field.value }}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="field.value === 'true'"
|
||||
disabled="true"
|
||||
[attr.aria-labelledby]="'customField-' + i"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
|
||||
<div class="icon icon-small">
|
||||
<i
|
||||
class="bwi bwi-link"
|
||||
aria-hidden="true"
|
||||
appA11yTitle="{{ 'linkedValue' | i18n }}"
|
||||
></i>
|
||||
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
|
||||
</div>
|
||||
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -535,7 +535,7 @@
|
||||
*ngIf="u.canLaunch"
|
||||
(click)="launch(u)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -59,7 +59,7 @@ export function isEnterpriseOrgGuard(showError: boolean = true): CanActivateFn {
|
||||
content: { key: "onlyAvailableForEnterpriseOrganization" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
icon: "bwi-arrow-circle-up",
|
||||
icon: "bwi-plus-circle",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
|
||||
@@ -58,7 +58,7 @@ export function isPaidOrgGuard(): CanActivateFn {
|
||||
content: { key: "upgradeOrganizationCloseSecurityGapsDesc" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
icon: "bwi-arrow-circle-up",
|
||||
icon: "bwi-plus-circle",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</ng-container>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-providers"
|
||||
icon="bwi-msp"
|
||||
[text]="'integrations' | i18n"
|
||||
route="integrations"
|
||||
*ngIf="integrationPageEnabled$ | async"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class DeleteManagedMemberWarningService {
|
||||
key: "deleteManagedUserWarningDesc",
|
||||
},
|
||||
type: "danger",
|
||||
icon: "bwi-exclamation-circle",
|
||||
icon: "bwi-exclamation-triangle",
|
||||
acceptButtonText: { key: "continue" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{ "keyConnectorPolicyRestriction" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
<bit-callout type="success" [title]="'prerequisite' | i18n" icon="bwi-lightbulb">
|
||||
<bit-callout type="info" [title]="'prerequisite' | i18n">
|
||||
{{ "accountRecoverySingleOrgRequirementDesc" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import mock, { MockProxy } from "jest-mock-extended/lib/Mock";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { ChangeEmailComponent } from "@bitwarden/web-vault/app/auth/settings/account/change-email.component";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
describe("ChangeEmailComponent", () => {
|
||||
let component: ChangeEmailComponent;
|
||||
let fixture: ComponentFixture<ChangeEmailComponent>;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let accountService: FakeAccountService;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
apiService = mock<ApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
accountService = mockAccountServiceWith("UserId" as UserId);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChangeEmailComponent],
|
||||
imports: [ReactiveFormsModule, SharedModule],
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
{ provide: KdfConfigService, useValue: kdfConfigService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: FormBuilder, useClass: FormBuilder },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChangeEmailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
apiService.getTwoFactorProviders.mockResolvedValue({
|
||||
data: [{ type: TwoFactorProviderType.Email, enabled: true } as TwoFactorProviderResponse],
|
||||
} as ListResponse<TwoFactorProviderResponse>);
|
||||
});
|
||||
|
||||
it("initializes userId", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(component.userId).toBe("UserId");
|
||||
});
|
||||
|
||||
it("errors if there is no active user", async () => {
|
||||
// clear active account
|
||||
await firstValueFrom(accountService.activeAccount$);
|
||||
accountService.activeAccountSubject.next(null);
|
||||
|
||||
await expect(() => component.ngOnInit()).rejects.toThrow("Null or undefined account");
|
||||
});
|
||||
|
||||
it("initializes showTwoFactorEmailWarning", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(component.showTwoFactorEmailWarning).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit", () => {
|
||||
beforeEach(() => {
|
||||
component.formGroup.controls.step1.setValue({
|
||||
masterPassword: "password",
|
||||
newEmail: "test@example.com",
|
||||
});
|
||||
|
||||
keyService.getOrDeriveMasterKey
|
||||
.calledWith("password", "UserId")
|
||||
.mockResolvedValue("getOrDeriveMasterKey" as any);
|
||||
keyService.hashMasterKey
|
||||
.calledWith("password", "getOrDeriveMasterKey" as any)
|
||||
.mockResolvedValue("existingHash");
|
||||
});
|
||||
|
||||
it("throws if userId is null on submit", async () => {
|
||||
component.userId = undefined;
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Can't find user");
|
||||
});
|
||||
|
||||
describe("step 1", () => {
|
||||
it("does not submit if step 1 is invalid", async () => {
|
||||
component.formGroup.controls.step1.setValue({
|
||||
masterPassword: "",
|
||||
newEmail: "",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmailToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends email token in step 1 if tokenSent is false", async () => {
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmailToken).toHaveBeenCalledWith({
|
||||
newEmail: "test@example.com",
|
||||
masterPasswordHash: "existingHash",
|
||||
});
|
||||
// should activate step 2
|
||||
expect(component.tokenSent).toBe(true);
|
||||
expect(component.formGroup.controls.step1.disabled).toBe(true);
|
||||
expect(component.formGroup.controls.token.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("step 2", () => {
|
||||
beforeEach(() => {
|
||||
component.tokenSent = true;
|
||||
component.formGroup.controls.step1.disable();
|
||||
component.formGroup.controls.token.enable();
|
||||
component.formGroup.controls.token.setValue("token");
|
||||
|
||||
kdfConfigService.getKdfConfig$
|
||||
.calledWith("UserId" as any)
|
||||
.mockReturnValue(of("kdfConfig" as any));
|
||||
keyService.userKey$.calledWith("UserId" as any).mockReturnValue(of("userKey" as any));
|
||||
|
||||
keyService.makeMasterKey
|
||||
.calledWith("password", "test@example.com", "kdfConfig" as any)
|
||||
.mockResolvedValue("newMasterKey" as any);
|
||||
keyService.hashMasterKey
|
||||
.calledWith("password", "newMasterKey" as any)
|
||||
.mockResolvedValue("newMasterKeyHash");
|
||||
|
||||
// Important: make sure this is called with new master key, not existing
|
||||
keyService.encryptUserKeyWithMasterKey
|
||||
.calledWith("newMasterKey" as any, "userKey" as any)
|
||||
.mockResolvedValue(["userKey" as any, { encryptedString: "newEncryptedUserKey" } as any]);
|
||||
});
|
||||
|
||||
it("does not post email if token is missing on submit", async () => {
|
||||
component.formGroup.controls.token.setValue("");
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if kdfConfig is missing on submit", async () => {
|
||||
kdfConfigService.getKdfConfig$.mockReturnValue(of(null));
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Missing kdf config");
|
||||
});
|
||||
|
||||
it("throws if userKey can't be found", async () => {
|
||||
keyService.userKey$.mockReturnValue(of(null));
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Can't find UserKey");
|
||||
});
|
||||
|
||||
it("throws if encryptedUserKey is missing", async () => {
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(["userKey" as any, null as any]);
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Missing Encrypted User Key");
|
||||
});
|
||||
|
||||
it("submits if step 2 is valid", async () => {
|
||||
await component.submit();
|
||||
|
||||
// validate that hashes are correct
|
||||
expect(apiService.postEmail).toHaveBeenCalledWith({
|
||||
masterPasswordHash: "existingHash",
|
||||
newMasterPasswordHash: "newMasterKeyHash",
|
||||
token: "token",
|
||||
newEmail: "test@example.com",
|
||||
key: "newEncryptedUserKey",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -22,8 +21,9 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
tokenSent = false;
|
||||
showTwoFactorEmailWarning = false;
|
||||
userId: UserId | undefined;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
formGroup = this.formBuilder.group({
|
||||
step1: this.formBuilder.group({
|
||||
masterPassword: ["", [Validators.required]],
|
||||
newEmail: ["", [Validators.required, Validators.email]],
|
||||
@@ -32,26 +32,30 @@ export class ChangeEmailComponent implements OnInit {
|
||||
});
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private keyService: KeyService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
|
||||
(p) => p.type === TwoFactorProviderType.Email && p.enabled,
|
||||
);
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
submit = async () => {
|
||||
if (this.userId == null) {
|
||||
throw new Error("Can't find user");
|
||||
}
|
||||
|
||||
// This form has multiple steps, so we need to mark all the groups as touched.
|
||||
this.formGroup.controls.step1.markAllAsTouched();
|
||||
|
||||
@@ -65,37 +69,54 @@ export class ChangeEmailComponent implements OnInit {
|
||||
}
|
||||
|
||||
const step1Value = this.formGroup.controls.step1.value;
|
||||
const newEmail = step1Value.newEmail.trim().toLowerCase();
|
||||
const newEmail = step1Value.newEmail?.trim().toLowerCase();
|
||||
const masterPassword = step1Value.masterPassword;
|
||||
|
||||
if (newEmail == null || masterPassword == null) {
|
||||
throw new Error("Missing email or password");
|
||||
}
|
||||
|
||||
const existingHash = await this.keyService.hashMasterKey(
|
||||
masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(masterPassword, this.userId),
|
||||
);
|
||||
|
||||
if (!this.tokenSent) {
|
||||
const request = new EmailTokenRequest();
|
||||
request.newEmail = newEmail;
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
request.masterPasswordHash = existingHash;
|
||||
await this.apiService.postEmailToken(request);
|
||||
this.activateStep2();
|
||||
} else {
|
||||
const token = this.formGroup.value.token;
|
||||
if (token == null) {
|
||||
throw new Error("Missing token");
|
||||
}
|
||||
const request = new EmailRequest();
|
||||
request.token = this.formGroup.value.token;
|
||||
request.token = token;
|
||||
request.newEmail = newEmail;
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
step1Value.masterPassword,
|
||||
newEmail,
|
||||
kdfConfig,
|
||||
);
|
||||
request.masterPasswordHash = existingHash;
|
||||
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
|
||||
if (kdfConfig == null) {
|
||||
throw new Error("Missing kdf config");
|
||||
}
|
||||
const newMasterKey = await this.keyService.makeMasterKey(masterPassword, newEmail, kdfConfig);
|
||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(this.userId));
|
||||
if (userKey == null) {
|
||||
throw new Error("Can't find UserKey");
|
||||
}
|
||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey, userKey);
|
||||
const encryptedUserKey = newUserKey[1]?.encryptedString;
|
||||
if (encryptedUserKey == null) {
|
||||
throw new Error("Missing Encrypted User Key");
|
||||
}
|
||||
request.key = encryptedUserKey;
|
||||
|
||||
await this.apiService.postEmail(request);
|
||||
this.reset();
|
||||
|
||||
@@ -17,8 +17,9 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { ChangeLoginPasswordService, TaskService } from "@bitwarden/vault";
|
||||
import { ChangeLoginPasswordService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@@ -55,6 +56,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
{ provide: DialogRef, useValue: { close } },
|
||||
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(EmergencyViewDialogComponent, {
|
||||
@@ -71,10 +73,6 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
@@ -39,7 +37,6 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -965,9 +965,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -222,9 +222,8 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
class="tw-mr-2"
|
||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
||||
<i class="bwi bwi-file-text" aria-hidden="true"></i
|
||||
></a>
|
||||
<a
|
||||
bitLink
|
||||
@@ -34,7 +34,7 @@
|
||||
{{ "paid" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="!i.paid">
|
||||
<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 }}
|
||||
</span>
|
||||
</td>
|
||||
@@ -59,7 +59,7 @@
|
||||
class="tw-mr-2"
|
||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
||||
<i class="bwi bwi-file-text" aria-hidden="true"></i
|
||||
></a>
|
||||
<a
|
||||
bitLink
|
||||
@@ -78,7 +78,7 @@
|
||||
{{ "paid" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="!i.paid">
|
||||
<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 }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -31,7 +31,7 @@ export class BillingHistoryComponent {
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
case PaymentMethodType.WireTransfer:
|
||||
return ["bwi-bank"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.BitPay:
|
||||
return ["bwi-bitcoin text-warning"];
|
||||
case PaymentMethodType.PayPal:
|
||||
|
||||
@@ -237,9 +237,8 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*ngIf="showBankAccount"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-billing" aria-hidden="true"></i>
|
||||
{{ "bankAccount" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
@@ -499,45 +499,45 @@ export class EventService {
|
||||
|
||||
switch (ev.deviceType) {
|
||||
case DeviceType.Android:
|
||||
return ["bwi-android", this.i18nService.t("mobile") + " - Android"];
|
||||
return ["bwi-mobile", this.i18nService.t("mobile") + " - Android"];
|
||||
case DeviceType.iOS:
|
||||
return ["bwi-apple", this.i18nService.t("mobile") + " - iOS"];
|
||||
return ["bwi-mobile", this.i18nService.t("mobile") + " - iOS"];
|
||||
case DeviceType.UWP:
|
||||
return ["bwi-windows", this.i18nService.t("mobile") + " - Windows"];
|
||||
return ["bwi-mobile", this.i18nService.t("mobile") + " - Windows"];
|
||||
case DeviceType.ChromeExtension:
|
||||
return ["bwi-chrome", this.i18nService.t("extension") + " - Chrome"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Chrome"];
|
||||
case DeviceType.FirefoxExtension:
|
||||
return ["bwi-firefox", this.i18nService.t("extension") + " - Firefox"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Firefox"];
|
||||
case DeviceType.OperaExtension:
|
||||
return ["bwi-opera", this.i18nService.t("extension") + " - Opera"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Opera"];
|
||||
case DeviceType.EdgeExtension:
|
||||
return ["bwi-edge", this.i18nService.t("extension") + " - Edge"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Edge"];
|
||||
case DeviceType.VivaldiExtension:
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Vivaldi"];
|
||||
case DeviceType.SafariExtension:
|
||||
return ["bwi-safari", this.i18nService.t("extension") + " - Safari"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Safari"];
|
||||
case DeviceType.WindowsDesktop:
|
||||
return ["bwi-windows", this.i18nService.t("desktop") + " - Windows"];
|
||||
return ["bwi-desktop", this.i18nService.t("desktop") + " - Windows"];
|
||||
case DeviceType.MacOsDesktop:
|
||||
return ["bwi-apple", this.i18nService.t("desktop") + " - macOS"];
|
||||
return ["bwi-desktop", this.i18nService.t("desktop") + " - macOS"];
|
||||
case DeviceType.LinuxDesktop:
|
||||
return ["bwi-linux", this.i18nService.t("desktop") + " - Linux"];
|
||||
return ["bwi-desktop", this.i18nService.t("desktop") + " - Linux"];
|
||||
case DeviceType.ChromeBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Chrome"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Chrome"];
|
||||
case DeviceType.FirefoxBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Firefox"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Firefox"];
|
||||
case DeviceType.OperaBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Opera"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Opera"];
|
||||
case DeviceType.SafariBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Safari"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Safari"];
|
||||
case DeviceType.VivaldiBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Vivaldi"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Vivaldi"];
|
||||
case DeviceType.EdgeBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Edge"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Edge"];
|
||||
case DeviceType.IEBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - IE"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - IE"];
|
||||
case DeviceType.Server:
|
||||
return ["bwi-server", this.i18nService.t("server")];
|
||||
return ["bwi-user-monitor", this.i18nService.t("server")];
|
||||
case DeviceType.WindowsCLI:
|
||||
return ["bwi-cli", this.i18nService.t("cli") + " - Windows"];
|
||||
case DeviceType.MacOsCLI:
|
||||
@@ -546,7 +546,7 @@ export class EventService {
|
||||
return ["bwi-cli", this.i18nService.t("cli") + " - Linux"];
|
||||
case DeviceType.UnknownBrowser:
|
||||
return [
|
||||
"bwi-globe",
|
||||
"bwi-browser",
|
||||
this.i18nService.t("webVault") + " - " + this.i18nService.t("unknown"),
|
||||
];
|
||||
default:
|
||||
|
||||
@@ -7,8 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -37,6 +39,8 @@ export class InitService {
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -45,6 +49,13 @@ export class InitService {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init();
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount) {
|
||||
// If there is an active account, we must await the process of setting the user key in memory
|
||||
|
||||
@@ -59,22 +59,22 @@
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-cog"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="secondary"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
class="tw-border-0 tw-bg-transparent tw-p-0"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<a href="#" bitMenuItem appStopClick (click)="toggleExcluded(d)" *ngIf="!d.excluded">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
{{ "exclude" | i18n }}
|
||||
</a>
|
||||
<a href="#" bitMenuItem appStopClick (click)="toggleExcluded(d)" *ngIf="d.excluded">
|
||||
<i class="bwi bwi-fw bwi-plus" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
{{ "include" | i18n }}
|
||||
</a>
|
||||
<a href="#" bitMenuItem appStopClick (click)="customize(d)">
|
||||
<i class="bwi bwi-fw bwi-cut" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "customize" | i18n }}
|
||||
</a>
|
||||
</bit-menu>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<details #details class="tw-rounded-sm tw-bg-background-alt tw-text-main" (toggle)="toggle()" open>
|
||||
<summary class="tw-list-none tw-p-2 tw-px-4">
|
||||
<div class="tw-flex tw-select-none tw-items-center tw-gap-4">
|
||||
<i class="bwi bwi-dashboard tw-text-3xl tw-text-primary-600" aria-hidden="true"></i>
|
||||
<div class="tw-text-lg">{{ title }}</div>
|
||||
<bit-progress class="tw-flex-1" [showText]="false" [barWidth]="barWidth"></bit-progress>
|
||||
<span *ngIf="tasks.length > 0; else spinner">
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.maxAccessCountReached">
|
||||
<i
|
||||
class="bwi bwi-ban"
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -15,12 +15,14 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
let fixture: ComponentFixture<BrowserExtensionPromptComponent>;
|
||||
let component: BrowserExtensionPromptComponent;
|
||||
const start = jest.fn();
|
||||
const openExtension = jest.fn();
|
||||
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
|
||||
const setAttribute = jest.fn();
|
||||
const getAttribute = jest.fn().mockReturnValue("width=1010");
|
||||
|
||||
beforeEach(async () => {
|
||||
start.mockClear();
|
||||
openExtension.mockClear();
|
||||
setAttribute.mockClear();
|
||||
getAttribute.mockClear();
|
||||
|
||||
@@ -39,7 +41,7 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
providers: [
|
||||
{
|
||||
provide: BrowserExtensionPromptService,
|
||||
useValue: { start, pageState$ },
|
||||
useValue: { start, openExtension, pageState$ },
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
@@ -83,6 +85,15 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
const errorText = fixture.debugElement.query(By.css("p")).nativeElement;
|
||||
expect(errorText.textContent.trim()).toBe("openingExtensionError");
|
||||
});
|
||||
|
||||
it("opens extension on button click", () => {
|
||||
const button = fixture.debugElement.query(By.css("button")).nativeElement;
|
||||
|
||||
button.click();
|
||||
|
||||
expect(openExtension).toHaveBeenCalledTimes(1);
|
||||
expect(openExtension).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("success state", () => {
|
||||
|
||||
@@ -61,6 +61,6 @@ export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
openExtension(): void {
|
||||
this.browserExtensionPromptService.openExtension();
|
||||
this.browserExtensionPromptService.openExtension(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherAttachmentsComponent,
|
||||
CipherFormComponent,
|
||||
CipherFormConfig,
|
||||
CipherFormGenerationService,
|
||||
@@ -47,8 +46,6 @@ import {
|
||||
CipherViewComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
@@ -132,17 +129,14 @@ export enum VaultItemDialogResult {
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CipherFormModule,
|
||||
CipherAttachmentsComponent,
|
||||
AsyncActionsModule,
|
||||
ItemModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
|
||||
RoutedVaultFilterService,
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-share-square" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-external-link" aria-hidden="true"></i>
|
||||
{{ "launch" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
@@ -52,10 +52,12 @@
|
||||
{{ "permission" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-12 tw-text-right">
|
||||
@let featureFlaggedDisable =
|
||||
(limitItemDeletion$ | async) ? (disableMenu$ | async) : disableMenu;
|
||||
<button
|
||||
[disabled]="disabled || isEmpty || disableMenu"
|
||||
[disabled]="disabled || isEmpty || featureFlaggedDisable"
|
||||
[bitMenuTriggerFor]="headerMenu"
|
||||
[attr.title]="disableMenu ? ('missingPermissions' | i18n) : ''"
|
||||
[attr.title]="featureFlaggedDisable ? ('missingPermissions' | i18n) : ''"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
type="button"
|
||||
|
||||
@@ -85,6 +85,7 @@ export class VaultItemsComponent {
|
||||
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
||||
protected canDeleteSelected$: Observable<boolean>;
|
||||
protected canRestoreSelected$: Observable<boolean>;
|
||||
protected disableMenu$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
@@ -140,6 +141,20 @@ export class VaultItemsComponent {
|
||||
}),
|
||||
map((canRestore) => canRestore && this.showBulkTrashOptions),
|
||||
);
|
||||
|
||||
this.disableMenu$ = combineLatest([this.limitItemDeletion$, this.canDeleteSelected$]).pipe(
|
||||
map(([enabled, canDelete]) => {
|
||||
if (enabled) {
|
||||
return (
|
||||
!this.bulkMoveAllowed &&
|
||||
!this.showAssignToCollections() &&
|
||||
!canDelete &&
|
||||
!this.showBulkEditCollectionAccess
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
get showExtraColumn() {
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
(click)="launch(u)"
|
||||
[disabled]="!u.canLaunch"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -46,12 +46,10 @@
|
||||
bitMenuItem
|
||||
(click)="unlinkSso(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||
{{ "unlinkSso" | i18n }}
|
||||
</button>
|
||||
<ng-template #linkSso>
|
||||
<button type="button" bitMenuItem (click)="handleLinkSso(organization)">
|
||||
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
|
||||
{{ "linkSso" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -20,9 +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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ChangeLoginPasswordService, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
import { ChangeLoginPasswordService } from "@bitwarden/vault";
|
||||
|
||||
import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component";
|
||||
|
||||
@@ -83,12 +84,12 @@ describe("ViewComponent", () => {
|
||||
canDeleteCipher$: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(ViewComponent, {
|
||||
remove: {
|
||||
providers: [
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: PlatformUtilsService, useValue: PlatformUtilsService },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
@@ -98,10 +99,6 @@ describe("ViewComponent", () => {
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
import { CipherViewComponent } from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
|
||||
@@ -74,7 +74,6 @@ export interface ViewCipherDialogCloseResult {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
],
|
||||
})
|
||||
export class ViewComponent implements OnInit {
|
||||
|
||||
@@ -169,5 +169,32 @@ describe("BrowserExtensionPromptService", () => {
|
||||
pageTitle: { key: "somethingWentWrong" },
|
||||
});
|
||||
});
|
||||
|
||||
it("sets manual open state when open extension is called", (done) => {
|
||||
service.openExtension(true);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
service.pageState$.subscribe((state) => {
|
||||
expect(state).toBe(BrowserPromptState.ManualOpen);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows success state when extension auto opens", (done) => {
|
||||
service.openExtension(true);
|
||||
|
||||
jest.advanceTimersByTime(500); // don't let timeout occur
|
||||
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", { data: { command: VaultMessages.PopupOpened } }),
|
||||
);
|
||||
|
||||
service.pageState$.subscribe((state) => {
|
||||
expect(state).toBe(BrowserPromptState.Success);
|
||||
expect(service["extensionCheckTimeout"]).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,8 +54,18 @@ export class BrowserExtensionPromptService {
|
||||
}
|
||||
|
||||
/** Post a message to the extension to open */
|
||||
openExtension() {
|
||||
openExtension(setManualErrorTimeout = false) {
|
||||
window.postMessage({ command: VaultMessages.OpenPopup });
|
||||
|
||||
// Optionally, configure timeout to show the manual open error state if
|
||||
// the extension does not open within one second.
|
||||
if (setManualErrorTimeout) {
|
||||
this.clearExtensionCheckTimeout();
|
||||
|
||||
this.extensionCheckTimeout = window.setTimeout(() => {
|
||||
this.setErrorState(BrowserPromptState.ManualOpen);
|
||||
}, 750);
|
||||
}
|
||||
}
|
||||
|
||||
/** Send message checking for the browser extension */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'providerPortal' | i18n"></bit-nav-logo>
|
||||
|
||||
<bit-nav-item
|
||||
icon="bwi-bank"
|
||||
icon="bwi-provider"
|
||||
[text]="'clients' | i18n"
|
||||
[route]="(isBillable | async) ? 'manage-client-organizations' : 'clients'"
|
||||
></bit-nav-item>
|
||||
@@ -30,7 +30,7 @@
|
||||
<bit-nav-item [text]="'billingHistory' | i18n" route="billing/history"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-cogs"
|
||||
icon="bwi-cog"
|
||||
[text]="'settings' | i18n"
|
||||
route="settings"
|
||||
*ngIf="showSettingsTab(provider)"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{{ "newClient" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addExistingOrganization()">
|
||||
<i aria-hidden="true" class="bwi bwi-sitemap"></i>
|
||||
<i aria-hidden="true" class="bwi bwi-filter"></i>
|
||||
{{ "existingOrganization" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
|
||||
@@ -182,9 +182,8 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-providers"
|
||||
icon="bwi-msp"
|
||||
[text]="'integrations' | i18n"
|
||||
route="integrations"
|
||||
[relativeTo]="route.parent"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<bit-menu #newMenu>
|
||||
<button type="button" bitMenuItem (click)="openProjectDialog()">
|
||||
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "project" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="openSecretDialog()">
|
||||
|
||||
@@ -19,6 +19,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
import {
|
||||
Icons,
|
||||
NoItemsModule,
|
||||
@@ -27,7 +28,6 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CardComponent } from "@bitwarden/tools-card";
|
||||
import { SecurityTaskType } from "@bitwarden/vault";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault";
|
||||
import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
/**
|
||||
* Request type for creating tasks.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault";
|
||||
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { CreateTasksRequest } from "./abstractions/admin-task.abstraction";
|
||||
import { DefaultAdminTaskService } from "./default-admin-task.service";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SecurityTaskData,
|
||||
SecurityTaskResponse,
|
||||
SecurityTaskStatus,
|
||||
} from "@bitwarden/vault";
|
||||
} from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { AdminTaskService, CreateTasksRequest } from "./abstractions/admin-task.abstraction";
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user