mirror of
https://github.com/bitwarden/browser
synced 2026-02-26 01:23:24 +00:00
Merge branch 'main' into tools/pm-18793/port-credential-generator-service-to-providers
This commit is contained in:
@@ -5,6 +5,7 @@ import { firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -48,6 +49,7 @@ export class VaultFilterComponent
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
protected accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
vaultFilterService,
|
||||
@@ -58,6 +60,7 @@ export class VaultFilterComponent
|
||||
billingApiService,
|
||||
dialogService,
|
||||
configService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,8 +89,8 @@ export class VaultFilterComponent
|
||||
const collapsedNodes = await firstValueFrom(this.vaultFilterService.collapsedFilterNodes$);
|
||||
|
||||
collapsedNodes.delete("AllCollections");
|
||||
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes);
|
||||
const userId = await firstValueFrom(this.activeUserId$);
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes, userId);
|
||||
}
|
||||
|
||||
protected async addCollectionFilter(): Promise<VaultFilterSection> {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'tw-w-4/5' : 'tw-w-3/4'">
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
|
||||
@@ -45,7 +45,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -196,7 +195,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
|
||||
private resellerManagedOrgAlert: boolean;
|
||||
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||
|
||||
private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe(
|
||||
@@ -264,10 +262,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ResellerManagedOrgAlert,
|
||||
);
|
||||
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
@@ -654,7 +648,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.resellerWarning$ = organization$.pipe(
|
||||
filter((org) => org.isOwner && this.resellerManagedOrgAlert),
|
||||
filter((org) => org.isOwner),
|
||||
switchMap((org) =>
|
||||
from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe(
|
||||
map((metadata) => ({ org, metadata })),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -118,7 +118,10 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
),
|
||||
);
|
||||
|
||||
this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg);
|
||||
this.hideNewOrgButton$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
);
|
||||
|
||||
const provider$ = this.organization$.pipe(
|
||||
switchMap((organization) => this.providerService.get$(organization.providerId)),
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
|
||||
<ng-container *ngIf="loaded && usePlaceHolderEvents">
|
||||
<div
|
||||
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center"
|
||||
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center tw-h-[19rem]"
|
||||
>
|
||||
<div
|
||||
class="tw-bg-[#ffffff] tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<input bitInput appAutofocus type="text" formControlName="name" />
|
||||
<bit-hint>{{ "characterMaximum" | i18n: 100 }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -215,6 +217,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
this.groupDetails$,
|
||||
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(map((isEnabled) => !isEnabled || !!this.groupForm.get("externalId")?.value));
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
||||
@@ -231,6 +237,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -81,12 +83,16 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<ResetPasswordDialogResult>,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions = enforcedPasswordPolicyOptions),
|
||||
|
||||
@@ -43,6 +43,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -168,15 +169,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager));
|
||||
|
||||
const policies$ = organization$.pipe(
|
||||
switchMap((organization) => {
|
||||
const policies$ = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
organization$,
|
||||
]).pipe(
|
||||
switchMap(([userId, organization]) => {
|
||||
if (organization.isProviderUser) {
|
||||
return from(this.policyApiService.getPolicies(organization.id)).pipe(
|
||||
map((response) => Policy.fromListResponse(response)),
|
||||
);
|
||||
}
|
||||
|
||||
return this.policyService.policies$;
|
||||
return this.policyService.policies$(userId);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
{{ "learnMoreAboutApi" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<form
|
||||
*ngIf="org && !loading"
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
</p>
|
||||
<app-user-verification formControlName="secret"> </app-user-verification>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="danger" [disabled]="!loaded">
|
||||
{{ "deleteOrganization" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
filter,
|
||||
} from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
@@ -189,10 +191,29 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
|
||||
this.organizationSelected.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((_) => {
|
||||
this.organizationSelected.markAsTouched();
|
||||
this.formGroup.updateValueAndValidity();
|
||||
});
|
||||
this.organizationSelected.valueChanges
|
||||
.pipe(
|
||||
tap((_) => {
|
||||
if (this.organizationSelected.errors?.cannotCreateCollections) {
|
||||
this.buttonDisplayName = ButtonType.Upgrade;
|
||||
} else {
|
||||
this.buttonDisplayName = ButtonType.Save;
|
||||
}
|
||||
}),
|
||||
filter(() => this.organizationSelected.errors?.cannotCreateCollections),
|
||||
switchMap((value) => this.findOrganizationById(value)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((org) => {
|
||||
this.orgExceedingCollectionLimit = org;
|
||||
this.organizationSelected.markAsTouched();
|
||||
this.formGroup.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
|
||||
async findOrganizationById(orgId: string): Promise<Organization | undefined> {
|
||||
const organizations = await firstValueFrom(this.organizations$);
|
||||
return organizations.find((org) => org.id === orgId);
|
||||
}
|
||||
|
||||
async loadOrg(orgId: string) {
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
export * from "./two-factor-auth";
|
||||
export * from "./link-sso.service";
|
||||
|
||||
154
apps/web/src/app/auth/core/services/link-sso.service.spec.ts
Normal file
154
apps/web/src/app/auth/core/services/link-sso.service.spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
import { LinkSsoService } from "./link-sso.service";
|
||||
|
||||
describe("LinkSsoService", () => {
|
||||
let sut: LinkSsoService;
|
||||
|
||||
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
|
||||
const mockEnvironment$ = new BehaviorSubject<any>({
|
||||
getIdentityUrl: jest.fn().mockReturnValue("https://identity.bitwarden.com"),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock implementations
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockCryptoFunctionService = mock<CryptoFunctionService>();
|
||||
mockEnvironmentService = mock<EnvironmentService>();
|
||||
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
// Set up environment service to return our mock environment
|
||||
mockEnvironmentService.environment$ = mockEnvironment$;
|
||||
|
||||
// Set up API service mocks
|
||||
const mockResponse = { Token: "mockSsoToken" };
|
||||
mockApiService.preValidateSso.mockResolvedValue(new SsoPreValidateResponse(mockResponse));
|
||||
mockApiService.getSsoUserIdentifier.mockResolvedValue("mockUserIdentifier");
|
||||
|
||||
// Set up password generation service mock
|
||||
mockPasswordGenerationService.generatePassword.mockImplementation(
|
||||
async (options: PasswordGeneratorOptions) => {
|
||||
return "mockGeneratedPassword";
|
||||
},
|
||||
);
|
||||
|
||||
// Set up crypto function service mock
|
||||
mockCryptoFunctionService.hash.mockResolvedValue(new Uint8Array([1, 2, 3, 4]));
|
||||
|
||||
// Create the service under test with mock dependencies
|
||||
sut = new LinkSsoService(
|
||||
mockSsoLoginService,
|
||||
mockApiService,
|
||||
mockCryptoFunctionService,
|
||||
mockEnvironmentService,
|
||||
mockPasswordGenerationService,
|
||||
mockPlatformUtilsService,
|
||||
);
|
||||
|
||||
// Mock Utils.fromBufferToUrlB64
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue("mockCodeChallenge");
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
origin: "https://bitwarden.com",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("linkSso", () => {
|
||||
it("throws an error when identifier is null", async () => {
|
||||
await expect(sut.linkSso(null as unknown as string)).rejects.toThrow(
|
||||
"SSO identifier is required",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error when identifier is empty", async () => {
|
||||
await expect(sut.linkSso("")).rejects.toThrow("SSO identifier is required");
|
||||
});
|
||||
|
||||
it("calls preValidateSso with the provided identifier", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockApiService.preValidateSso).toHaveBeenCalledWith("org123");
|
||||
});
|
||||
|
||||
it("generates a password for code verifier", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockPasswordGenerationService.generatePassword).toHaveBeenCalledWith({
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
number: true,
|
||||
special: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the code verifier in the ssoLoginService", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockSsoLoginService.setCodeVerifier).toHaveBeenCalledWith("mockGeneratedPassword");
|
||||
});
|
||||
|
||||
it("generates a state and sets it in the ssoLoginService", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
const expectedState =
|
||||
"mockGeneratedPassword_returnUri='/settings/organizations'_identifier=org123";
|
||||
expect(mockSsoLoginService.setSsoState).toHaveBeenCalledWith(expectedState);
|
||||
});
|
||||
|
||||
it("gets the SSO user identifier from the API", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockApiService.getSsoUserIdentifier).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("launches the authorize URL with the correct parameters", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockPlatformUtilsService.launchUri).toHaveBeenCalledWith(
|
||||
expect.stringContaining("https://identity.bitwarden.com/connect/authorize"),
|
||||
{ sameWindow: true },
|
||||
);
|
||||
|
||||
const launchUriArg = mockPlatformUtilsService.launchUri.mock.calls[0][0];
|
||||
expect(launchUriArg).toContain("client_id=web");
|
||||
expect(launchUriArg).toContain(
|
||||
"redirect_uri=https%3A%2F%2Fbitwarden.com%2Fsso-connector.html",
|
||||
);
|
||||
expect(launchUriArg).toContain("response_type=code");
|
||||
expect(launchUriArg).toContain("code_challenge=mockCodeChallenge");
|
||||
expect(launchUriArg).toContain("ssoToken=mockSsoToken");
|
||||
expect(launchUriArg).toContain("user_identifier=mockUserIdentifier");
|
||||
});
|
||||
});
|
||||
});
|
||||
91
apps/web/src/app/auth/core/services/link-sso.service.ts
Normal file
91
apps/web/src/app/auth/core/services/link-sso.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
/**
|
||||
* Provides a service for linking SSO.
|
||||
*/
|
||||
export class LinkSsoService {
|
||||
constructor(
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private apiService: ApiService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private environmentService: EnvironmentService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Links SSO to an organization.
|
||||
* Ported from the SsoComponent
|
||||
* @param identifier The identifier of the organization to link to.
|
||||
*/
|
||||
async linkSso(identifier: string) {
|
||||
if (identifier == null || identifier === "") {
|
||||
throw new Error("SSO identifier is required");
|
||||
}
|
||||
|
||||
const redirectUri = window.location.origin + "/sso-connector.html";
|
||||
const clientId = "web";
|
||||
const returnUri = "/settings/organizations";
|
||||
|
||||
const response = await this.apiService.preValidateSso(identifier);
|
||||
|
||||
const passwordOptions: PasswordGeneratorOptions = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
number: true,
|
||||
special: false,
|
||||
};
|
||||
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
|
||||
let state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
state += `_returnUri='${returnUri}'`;
|
||||
state += `_identifier=${identifier}`;
|
||||
|
||||
// Save state
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
let authorizeUrl =
|
||||
env.getIdentityUrl() +
|
||||
"/connect/authorize?" +
|
||||
"client_id=" +
|
||||
clientId +
|
||||
"&redirect_uri=" +
|
||||
encodeURIComponent(redirectUri) +
|
||||
"&" +
|
||||
"response_type=code&scope=api offline_access&" +
|
||||
"state=" +
|
||||
state +
|
||||
"&code_challenge=" +
|
||||
codeChallenge +
|
||||
"&" +
|
||||
"code_challenge_method=S256&response_mode=query&" +
|
||||
"domain_hint=" +
|
||||
encodeURIComponent(identifier) +
|
||||
"&ssoToken=" +
|
||||
encodeURIComponent(response.token);
|
||||
|
||||
const userIdentifier = await this.apiService.getSsoUserIdentifier();
|
||||
authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`;
|
||||
|
||||
this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true });
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,15 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
@@ -38,6 +42,8 @@ describe("WebLoginComponentService", () => {
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
|
||||
@@ -50,6 +56,7 @@ describe("WebLoginComponentService", () => {
|
||||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -65,6 +72,7 @@ describe("WebLoginComponentService", () => {
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(WebLoginComponentService);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
DefaultLoginComponentService,
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -39,6 +41,7 @@ export class WebLoginComponentService
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
@@ -93,7 +96,10 @@ export class WebLoginComponentService
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
const enforcedPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(policies),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,9 +10,12 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -30,6 +33,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
@@ -38,6 +43,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
policyApiService = mock<PolicyApiServiceAbstraction>();
|
||||
logService = mock<LogService>();
|
||||
policyService = mock<PolicyService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
service = new WebRegistrationFinishService(
|
||||
keyService,
|
||||
@@ -46,6 +52,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
policyApiService,
|
||||
logService,
|
||||
policyService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
@@ -30,6 +31,7 @@ export class WebRegistrationFinishService
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(keyService, accountApiService);
|
||||
}
|
||||
@@ -68,7 +70,7 @@ export class WebRegistrationFinishService
|
||||
}
|
||||
|
||||
const masterPasswordPolicyOpts: MasterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(policies),
|
||||
this.policyService.masterPasswordPolicyOptions$(null, policies),
|
||||
);
|
||||
|
||||
return masterPasswordPolicyOpts;
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
{{ (tokenSent ? "changeEmail" : "continue") | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton *ngIf="tokenSent" (click)="reset()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
{{ (tokenSent ? "changeEmail" : "continue") | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton *ngIf="tokenSent" (click)="reset()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
[(ngModel)]="masterPasswordHint"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="loading">
|
||||
{{ "changeMasterPassword" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -12,7 +12,9 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -35,11 +37,13 @@ export class ChangePasswordComponent
|
||||
extends BaseChangePasswordComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
loading = false;
|
||||
rotateUserKey = false;
|
||||
currentMasterPassword: string;
|
||||
masterPasswordHint: string;
|
||||
checkForBreaches = true;
|
||||
characterMinimumMessage = "";
|
||||
userkeyRotationV2 = false;
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
@@ -56,9 +60,10 @@ export class ChangePasswordComponent
|
||||
private userVerificationService: UserVerificationService,
|
||||
private keyRotationService: UserKeyRotationService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -75,6 +80,8 @@ export class ChangePasswordComponent
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userkeyRotationV2 = await this.configService.getFeatureFlag(FeatureFlag.UserKeyRotationV2);
|
||||
|
||||
if (!(await this.userVerificationService.hasMasterPassword())) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@@ -137,6 +144,130 @@ export class ChangePasswordComponent
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.userkeyRotationV2) {
|
||||
this.loading = true;
|
||||
await this.submitNew();
|
||||
this.loading = false;
|
||||
} else {
|
||||
await this.submitOld();
|
||||
}
|
||||
}
|
||||
|
||||
async submitNew() {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.masterPasswordHint != null &&
|
||||
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("hintEqualsPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.leakedPassword = false;
|
||||
if (this.checkForBreaches) {
|
||||
this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0;
|
||||
}
|
||||
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.rotateUserKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
const user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
this.currentMasterPassword,
|
||||
this.masterPassword,
|
||||
user,
|
||||
this.masterPasswordHint,
|
||||
);
|
||||
} else {
|
||||
await this.updatePassword(this.masterPassword);
|
||||
}
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// todo: move this to a service
|
||||
// https://bitwarden.atlassian.net/browse/PM-17108
|
||||
private async updatePassword(newMasterPassword: string) {
|
||||
const currentMasterPassword = this.currentMasterPassword;
|
||||
const { userId, email } = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))),
|
||||
);
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentMasterPassword,
|
||||
email,
|
||||
kdfConfig,
|
||||
);
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
if (decryptedUserKey == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
currentMasterKey,
|
||||
);
|
||||
request.masterPasswordHint = this.masterPasswordHint;
|
||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||
newMasterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
|
||||
try {
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("masterPasswordChangedDesc"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async submitOld() {
|
||||
if (
|
||||
this.masterPasswordHint != null &&
|
||||
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
|
||||
@@ -242,6 +373,6 @@ export class ChangePasswordComponent
|
||||
|
||||
private async updateKey() {
|
||||
const user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword, user);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(this.masterPassword, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
<bit-label> {{ "dontAskFingerprintAgain" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
<span>{{ "confirm" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -42,13 +42,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { takeUntil } from "rxjs";
|
||||
import { switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -79,9 +80,12 @@ export class EmergencyAccessTakeoverComponent
|
||||
const policies = await this.emergencyAccessService.getGrantorPolicies(
|
||||
this.params.emergencyAccessId,
|
||||
);
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
</p>
|
||||
</bit-callout>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" *ngIf="!clientSecret" bitButton bitFormButton>
|
||||
<span>{{ (data.isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<p bitTypography="body1">
|
||||
{{ "userApiKeyDesc" | i18n }}
|
||||
</p>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateUserApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateUserApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -15,11 +15,6 @@ import { ApiKeyComponent } from "./api-key.component";
|
||||
templateUrl: "security-keys.component.html",
|
||||
})
|
||||
export class SecurityKeysComponent implements OnInit {
|
||||
@ViewChild("viewUserApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
viewUserApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild("rotateUserApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
rotateUserApiKeyModalRef: ViewContainerRef;
|
||||
|
||||
showChangeKdf = true;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two
|
||||
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
|
||||
import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
@@ -109,13 +110,17 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.providers.sort((a: any, b: any) => a.sort - b.sort);
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.TwoFactorAuthentication, userId),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||
});
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, HostBinding, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { WebauthnLoginAdminService } from "../../core";
|
||||
@@ -35,6 +37,7 @@ export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
|
||||
private webauthnService: WebauthnLoginAdminService,
|
||||
private dialogService: DialogService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
@HostBinding("attr.aria-busy")
|
||||
@@ -57,9 +60,14 @@ export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
|
||||
requireSsoPolicyEnabled = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.RequireSso)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.RequireSso, userId),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((enabled) => {
|
||||
this.requireSsoPolicyEnabled = enabled;
|
||||
});
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
{{ "twoStepOptions" | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngFor="let p of providers" class="tw-m-2">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-gap-4">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-min-w-[120px]">
|
||||
<auth-two-factor-icon [provider]="p.type" />
|
||||
</div>
|
||||
<div class="tw-flex-1">
|
||||
<h3 bitTypography="h3">{{ p.name }}</h3>
|
||||
<p bitTypography="body1">{{ p.description }}</p>
|
||||
</div>
|
||||
<div class="tw-min-w-20">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="choose(p)">
|
||||
{{ "select" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="tw-m-2" (click)="recover()">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-gap-4">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-min-w-[120px]">
|
||||
<auth-two-factor-icon provider="rc" />
|
||||
</div>
|
||||
<div class="tw-flex-1">
|
||||
<h3 bitTypography="h3">{{ "recoveryCodeTitle" | i18n }}</h3>
|
||||
<p bitTypography="body1">{{ "recoveryCodeDesc" | i18n }}</p>
|
||||
</div>
|
||||
<div class="tw-min-w-20">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="recover()">
|
||||
{{ "select" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -1,52 +0,0 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { TwoFactorOptionsComponentV1 as BaseTwoFactorOptionsComponentV1 } from "@bitwarden/angular/auth/components/two-factor-options-v1.component";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export enum TwoFactorOptionsDialogResult {
|
||||
Provider = "Provider selected",
|
||||
Recover = "Recover selected",
|
||||
}
|
||||
|
||||
export type TwoFactorOptionsDialogResultType = {
|
||||
result: TwoFactorOptionsDialogResult;
|
||||
type: TwoFactorProviderType;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-options",
|
||||
templateUrl: "two-factor-options-v1.component.html",
|
||||
})
|
||||
export class TwoFactorOptionsComponentV1 extends BaseTwoFactorOptionsComponentV1 {
|
||||
constructor(
|
||||
twoFactorService: TwoFactorService,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
private dialogRef: DialogRef,
|
||||
) {
|
||||
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
|
||||
}
|
||||
|
||||
async choose(p: any) {
|
||||
await super.choose(p);
|
||||
this.dialogRef.close({ result: TwoFactorOptionsDialogResult.Provider, type: p.type });
|
||||
}
|
||||
|
||||
async recover() {
|
||||
await super.recover();
|
||||
this.dialogRef.close({ result: TwoFactorOptionsDialogResult.Recover });
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService) {
|
||||
return dialogService.open<TwoFactorOptionsDialogResultType>(TwoFactorOptionsComponentV1);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
|
||||
<div class="tw-min-w-96">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Email ||
|
||||
selectedProviderType === providerType.Authenticator
|
||||
"
|
||||
>
|
||||
<p bitTypography="body1" *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1" *ngIf="selectedProviderType === providerType.Email">
|
||||
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="token" appAutofocus appInputVerbatim />
|
||||
<bit-hint *ngIf="selectedProviderType === providerType.Email">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="sendEmail(true)"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
>
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a></bit-hint
|
||||
>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="tw-rounded img-fluid tw-mb-3" alt="" />
|
||||
</picture>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input type="password" bitInput formControlName="token" appAutofocus appInputVerbatim />
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="tw-mb-3">
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Duo -->
|
||||
<ng-container *ngIf="isDuoProvider">
|
||||
<p
|
||||
bitTypography="body1"
|
||||
*ngIf="selectedProviderType === providerType.OrganizationDuo"
|
||||
class="tw-mb-0"
|
||||
>
|
||||
{{ "duoRequiredByOrgForAccount" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1">{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
|
||||
</ng-container>
|
||||
<bit-form-control *ngIf="selectedProviderType != null">
|
||||
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="remember" />
|
||||
</bit-form-control>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="
|
||||
selectedProviderType != null &&
|
||||
!isDuoProvider &&
|
||||
selectedProviderType !== providerType.WebAuthn
|
||||
"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
<button
|
||||
(click)="launchDuoFrameless()"
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="isDuoProvider"
|
||||
>
|
||||
<span> {{ "launchDuo" | i18n }} </span>
|
||||
</button>
|
||||
<a routerLink="/login" bitButton buttonType="secondary">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="anotherMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,164 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil, lastValueFrom } from "rxjs";
|
||||
|
||||
import { TwoFactorComponentV1 as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor-v1.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
TwoFactorOptionsDialogResult,
|
||||
TwoFactorOptionsComponentV1,
|
||||
TwoFactorOptionsDialogResultType,
|
||||
} from "./two-factor-options-v1.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor",
|
||||
templateUrl: "two-factor-v1.component.html",
|
||||
})
|
||||
export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
|
||||
twoFactorOptionsModal: ViewContainerRef;
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.required],
|
||||
updateOn: "submit",
|
||||
},
|
||||
],
|
||||
remember: [false],
|
||||
});
|
||||
private destroy$ = new Subject<void>();
|
||||
constructor(
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
private dialogService: DialogService,
|
||||
route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
twoFactorService: TwoFactorService,
|
||||
appIdService: AppIdService,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
private formBuilder: FormBuilder,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
) {
|
||||
super(
|
||||
loginStrategyService,
|
||||
router,
|
||||
i18nService,
|
||||
apiService,
|
||||
platformUtilsService,
|
||||
win,
|
||||
environmentService,
|
||||
stateService,
|
||||
route,
|
||||
logService,
|
||||
twoFactorService,
|
||||
appIdService,
|
||||
loginEmailService,
|
||||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
configService,
|
||||
masterPasswordService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.token = value.token;
|
||||
this.remember = value.remember;
|
||||
});
|
||||
}
|
||||
submitForm = async () => {
|
||||
await this.submit();
|
||||
};
|
||||
|
||||
async anotherMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponentV1.open(this.dialogService);
|
||||
const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed);
|
||||
if (response.result === TwoFactorOptionsDialogResult.Provider) {
|
||||
this.selectedProviderType = response.type;
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
|
||||
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
goAfterLogIn = async () => {
|
||||
this.loginEmailService.clearValues();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute], {
|
||||
queryParams: {
|
||||
identifier: this.orgIdentifier,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private duoResultChannel: BroadcastChannel;
|
||||
|
||||
protected override setupDuoResultListener() {
|
||||
if (!this.duoResultChannel) {
|
||||
this.duoResultChannel = new BroadcastChannel("duoResult");
|
||||
this.duoResultChannel.addEventListener("message", this.handleDuoResultMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDuoResultMessage = async (msg: { data: { code: string; state: string } }) => {
|
||||
this.token = msg.data.code + "|" + msg.data.state;
|
||||
await this.submit();
|
||||
};
|
||||
|
||||
async ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
|
||||
if (this.duoResultChannel) {
|
||||
// clean up duo listener if it was initialized.
|
||||
this.duoResultChannel.removeEventListener("message", this.handleDuoResultMessage);
|
||||
this.duoResultChannel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
href="{{ premiumURL }}}"
|
||||
href="{{ premiumURL }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, map, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -28,6 +28,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
BillingApiServiceAbstraction,
|
||||
BillingInformation,
|
||||
@@ -265,9 +266,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.upgradeFlowPrefillForm();
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.SingleOrg)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||
});
|
||||
@@ -727,6 +733,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
submit = async () => {
|
||||
if (this.taxComponent !== undefined && !this.taxComponent.validate()) {
|
||||
this.taxComponent.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -958,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:
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
import { debounceTime, map } from "rxjs/operators";
|
||||
import { debounceTime, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -31,6 +31,7 @@ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/mode
|
||||
import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request";
|
||||
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
@@ -240,9 +241,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.controls.billingEmail.addValidators(Validators.required);
|
||||
}
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.SingleOrg)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -96,7 +96,7 @@ export class FreeFamiliesPolicyService {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId),
|
||||
this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId),
|
||||
),
|
||||
map((policies) => ({
|
||||
isFreeFamilyPolicyEnabled: policies.some(
|
||||
|
||||
@@ -161,6 +161,7 @@ export class StripeService {
|
||||
},
|
||||
},
|
||||
classes: {
|
||||
base: "tw-stripe-form-control",
|
||||
focus: "is-focused",
|
||||
empty: "is-empty",
|
||||
invalid: "is-invalid",
|
||||
@@ -168,7 +169,6 @@ export class StripeService {
|
||||
};
|
||||
|
||||
options.style.base.fontWeight = "500";
|
||||
options.classes.base = "v2";
|
||||
|
||||
// Remove the placeholder for number and CVC fields
|
||||
if (["cardNumber", "cardCvc"].includes(element)) {
|
||||
|
||||
@@ -11,8 +11,6 @@ import { BillingSourceResponse } from "@bitwarden/common/billing/models/response
|
||||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
|
||||
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 { DialogService } from "@bitwarden/components";
|
||||
|
||||
@@ -24,15 +22,12 @@ import { FreeTrial } from "../types/free-trial";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class TrialFlowService {
|
||||
private resellerManagedOrgAlert: boolean;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
protected dialogService: DialogService,
|
||||
private router: Router,
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
checkForOrgsWithUpcomingPaymentIssues(
|
||||
organization: Organization,
|
||||
@@ -98,10 +93,6 @@ export class TrialFlowService {
|
||||
isCanceled: boolean,
|
||||
isUnpaid: boolean,
|
||||
): Promise<boolean> {
|
||||
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ResellerManagedOrgAlert,
|
||||
);
|
||||
|
||||
if (!org?.isOwner && !org.providerId) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("suspendedOrganizationTitle", org?.name),
|
||||
@@ -113,7 +104,7 @@ export class TrialFlowService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (org.providerId && this.resellerManagedOrgAlert) {
|
||||
if (org.providerId) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("suspendedOrganizationTitle", org.name),
|
||||
content: { key: "suspendedManagedOrgMessage", placeholders: [org.providerName] },
|
||||
@@ -134,7 +125,7 @@ export class TrialFlowService {
|
||||
});
|
||||
}
|
||||
|
||||
if (org.isOwner && isCanceled && this.resellerManagedOrgAlert) {
|
||||
if (org.isOwner && isCanceled) {
|
||||
await this.changePlan(org);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.availableSponsorshipOrgs$ = combineLatest([
|
||||
this.organizationService.organizations$(userId),
|
||||
this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId),
|
||||
this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId),
|
||||
]).pipe(
|
||||
map(([organizations, policies]) =>
|
||||
organizations
|
||||
|
||||
@@ -10,7 +10,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
@@ -36,7 +35,6 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
private logService: LogService,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
@@ -54,7 +52,7 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
this.isFreeFamilyPolicyEnabled$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId),
|
||||
this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId),
|
||||
),
|
||||
map(
|
||||
(policies) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<bit-dialog dialogSize="large" [title]="dialogHeader">
|
||||
<bit-dialog dialogSize="large" [title]="dialogHeader" [loading]="loading">
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment
|
||||
[showAccountCredit]="false"
|
||||
|
||||
@@ -47,6 +47,8 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
protected productTier?: ProductTierType;
|
||||
protected providerId?: string;
|
||||
|
||||
protected loading = true;
|
||||
|
||||
protected taxInformation: TaxInformation;
|
||||
|
||||
constructor(
|
||||
@@ -72,16 +74,26 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
.getTaxInfo(this.organizationId)
|
||||
.then((response: TaxInfoResponse) => {
|
||||
this.taxInformation = TaxInformation.from(response);
|
||||
this.toggleBankAccount();
|
||||
})
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
} else if (this.providerId) {
|
||||
this.billingApiService
|
||||
.getProviderTaxInformation(this.providerId)
|
||||
.then((response) => (this.taxInformation = TaxInformation.from(response)))
|
||||
.then((response) => {
|
||||
this.taxInformation = TaxInformation.from(response);
|
||||
this.toggleBankAccount();
|
||||
})
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.apiService
|
||||
@@ -91,21 +103,28 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
})
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
taxInformationChanged(event: TaxInformation) {
|
||||
this.taxInformation = event;
|
||||
if (event.country === "US") {
|
||||
this.paymentComponent.showBankAccount = !!this.organizationId;
|
||||
this.toggleBankAccount();
|
||||
}
|
||||
|
||||
toggleBankAccount = () => {
|
||||
if (this.taxInformation.country === "US") {
|
||||
this.paymentComponent.showBankAccount = !!this.organizationId || !!this.providerId;
|
||||
} else {
|
||||
this.paymentComponent.showBankAccount = false;
|
||||
if (this.paymentComponent.selected === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.select(PaymentMethodType.Card);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (!this.taxInfoComponent.validate()) {
|
||||
|
||||
@@ -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>
|
||||
@@ -46,7 +46,7 @@
|
||||
<app-payment-label for="stripe-card-number" required>
|
||||
{{ "number" | i18n }}
|
||||
</app-payment-label>
|
||||
<div id="stripe-card-number" class="form-control stripe-form-control"></div>
|
||||
<div id="stripe-card-number" class="tw-stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="tw-col-span-1 tw-flex tw-items-end">
|
||||
<img
|
||||
@@ -59,7 +59,7 @@
|
||||
<app-payment-label for="stripe-card-expiry" required>
|
||||
{{ "expiration" | i18n }}
|
||||
</app-payment-label>
|
||||
<div id="stripe-card-expiry" class="form-control stripe-form-control"></div>
|
||||
<div id="stripe-card-expiry" class="tw-stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label for="stripe-card-cvc" required>
|
||||
@@ -74,7 +74,7 @@
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</app-payment-label>
|
||||
<div id="stripe-card-cvc" class="form-control stripe-form-control"></div>
|
||||
<div id="stripe-card-cvc" class="tw-stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||
@@ -12,6 +12,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||
OrganizationInformation,
|
||||
@@ -106,6 +108,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
private validationService: ValidationService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -173,9 +176,12 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (policies !== null) {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
WebLoginDecryptionOptionsService,
|
||||
WebTwoFactorAuthComponentService,
|
||||
WebTwoFactorAuthDuoComponentService,
|
||||
LinkSsoService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
@@ -256,6 +257,7 @@ const safeProviders: SafeProvider[] = [
|
||||
PolicyApiServiceAbstraction,
|
||||
LogService,
|
||||
PolicyService,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -311,6 +313,7 @@ const safeProviders: SafeProvider[] = [
|
||||
PlatformUtilsService,
|
||||
SsoLoginServiceAbstraction,
|
||||
Router,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -343,6 +346,18 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebSsoComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LinkSsoService,
|
||||
useClass: LinkSsoService,
|
||||
deps: [
|
||||
SsoLoginServiceAbstraction,
|
||||
ApiService,
|
||||
CryptoFunctionService,
|
||||
EnvironmentService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PlatformUtilsService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorAuthDuoComponentService,
|
||||
useClass: WebTwoFactorAuthDuoComponentService,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { switchMap } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType, EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
||||
@@ -19,10 +22,16 @@ export class EventService {
|
||||
private i18nService: I18nService,
|
||||
policyService: PolicyService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
policyService.policies$.subscribe((policies) => {
|
||||
this.policies = policies;
|
||||
});
|
||||
accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => policyService.policies$(userId)),
|
||||
)
|
||||
.subscribe((policies) => {
|
||||
this.policies = policies;
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultDateFilters() {
|
||||
@@ -490,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:
|
||||
@@ -537,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:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export class AccountKeysRequest {
|
||||
// Other keys encrypted by the userkey
|
||||
userKeyEncryptedAccountPrivateKey: string;
|
||||
accountPublicKey: string;
|
||||
|
||||
constructor(userKeyEncryptedAccountPrivateKey: string, accountPublicKey: string) {
|
||||
this.userKeyEncryptedAccountPrivateKey = userKeyEncryptedAccountPrivateKey;
|
||||
this.accountPublicKey = accountPublicKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Argon2KdfConfig, KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
export class MasterPasswordUnlockDataRequest {
|
||||
kdfType: KdfType = KdfType.PBKDF2_SHA256;
|
||||
kdfIterations: number = 0;
|
||||
kdfMemory?: number;
|
||||
kdfParallelism?: number;
|
||||
|
||||
email: string;
|
||||
masterKeyAuthenticationHash: string;
|
||||
|
||||
masterKeyEncryptedUserKey: string;
|
||||
|
||||
masterPasswordHint?: string;
|
||||
|
||||
constructor(
|
||||
kdfConfig: KdfConfig,
|
||||
email: string,
|
||||
masterKeyAuthenticationHash: string,
|
||||
masterKeyEncryptedUserKey: string,
|
||||
masterPasswordHash?: string,
|
||||
) {
|
||||
this.kdfType = kdfConfig.kdfType;
|
||||
this.kdfIterations = kdfConfig.iterations;
|
||||
if (kdfConfig.kdfType === KdfType.Argon2id) {
|
||||
this.kdfMemory = (kdfConfig as Argon2KdfConfig).memory;
|
||||
this.kdfParallelism = (kdfConfig as Argon2KdfConfig).parallelism;
|
||||
}
|
||||
|
||||
this.email = email;
|
||||
this.masterKeyAuthenticationHash = masterKeyAuthenticationHash;
|
||||
this.masterKeyEncryptedUserKey = masterKeyEncryptedUserKey;
|
||||
this.masterPasswordHint = masterPasswordHash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AccountKeysRequest } from "./account-keys.request";
|
||||
import { UnlockDataRequest } from "./unlock-data.request";
|
||||
import { UserDataRequest as AccountDataRequest } from "./userdata.request";
|
||||
|
||||
export class RotateUserAccountKeysRequest {
|
||||
constructor(
|
||||
accountUnlockData: UnlockDataRequest,
|
||||
accountKeys: AccountKeysRequest,
|
||||
accountData: AccountDataRequest,
|
||||
oldMasterKeyAuthenticationHash: string,
|
||||
) {
|
||||
this.accountUnlockData = accountUnlockData;
|
||||
this.accountKeys = accountKeys;
|
||||
this.accountData = accountData;
|
||||
this.oldMasterKeyAuthenticationHash = oldMasterKeyAuthenticationHash;
|
||||
}
|
||||
|
||||
// Authentication for the request
|
||||
oldMasterKeyAuthenticationHash: string;
|
||||
|
||||
// All methods to get to the userkey
|
||||
accountUnlockData: UnlockDataRequest;
|
||||
|
||||
// Other keys encrypted by the userkey
|
||||
accountKeys: AccountKeysRequest;
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
accountData: AccountDataRequest;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
|
||||
import { DeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request";
|
||||
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
|
||||
|
||||
import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request";
|
||||
|
||||
import { MasterPasswordUnlockDataRequest } from "./master-password-unlock-data.request";
|
||||
|
||||
export class UnlockDataRequest {
|
||||
// All methods to get to the userkey
|
||||
masterPasswordUnlockData: MasterPasswordUnlockDataRequest;
|
||||
emergencyAccessUnlockData: EmergencyAccessWithIdRequest[];
|
||||
organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[];
|
||||
passkeyUnlockData: WebauthnRotateCredentialRequest[];
|
||||
deviceKeyUnlockData: DeviceKeysUpdateRequest[];
|
||||
|
||||
constructor(
|
||||
masterPasswordUnlockData: MasterPasswordUnlockDataRequest,
|
||||
emergencyAccessUnlockData: EmergencyAccessWithIdRequest[],
|
||||
organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[],
|
||||
passkeyUnlockData: WebauthnRotateCredentialRequest[],
|
||||
deviceTrustUnlockData: DeviceKeysUpdateRequest[],
|
||||
) {
|
||||
this.masterPasswordUnlockData = masterPasswordUnlockData;
|
||||
this.emergencyAccessUnlockData = emergencyAccessUnlockData;
|
||||
this.organizationAccountRecoveryUnlockData = organizationAccountRecoveryUnlockData;
|
||||
this.passkeyUnlockData = passkeyUnlockData;
|
||||
this.deviceKeyUnlockData = deviceTrustUnlockData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
|
||||
export class UserDataRequest {
|
||||
ciphers: CipherWithIdRequest[];
|
||||
folders: FolderWithIdRequest[];
|
||||
sends: SendWithIdRequest[];
|
||||
|
||||
constructor(
|
||||
ciphers: CipherWithIdRequest[],
|
||||
folders: FolderWithIdRequest[],
|
||||
sends: SendWithIdRequest[],
|
||||
) {
|
||||
this.ciphers = ciphers;
|
||||
this.folders = folders;
|
||||
this.sends = sends;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { inject, Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
|
||||
import { UpdateKeyRequest } from "./request/update-key.request";
|
||||
|
||||
@Injectable()
|
||||
@@ -11,4 +12,14 @@ export class UserKeyRotationApiService {
|
||||
postUserKeyUpdate(request: UpdateKeyRequest): Promise<any> {
|
||||
return this.apiService.send("POST", "/accounts/key", request, true, false);
|
||||
}
|
||||
|
||||
postUserKeyUpdateV2(request: RotateUserAccountKeysRequest): Promise<any> {
|
||||
return this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/key-management/rotate-user-account-keys",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,25 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, UserPrivateKey } from "@bitwarden/common/types/key";
|
||||
import { UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../../auth/core";
|
||||
@@ -47,6 +51,9 @@ describe("KeyRotationService", () => {
|
||||
let mockSyncService: MockProxy<SyncService>;
|
||||
let mockWebauthnLoginAdminService: MockProxy<WebauthnLoginAdminService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockVaultTimeoutService: MockProxy<VaultTimeoutService>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
const mockUser = {
|
||||
id: "mockUserId" as UserId,
|
||||
@@ -70,6 +77,9 @@ describe("KeyRotationService", () => {
|
||||
mockSyncService = mock<SyncService>();
|
||||
mockWebauthnLoginAdminService = mock<WebauthnLoginAdminService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockVaultTimeoutService = mock<VaultTimeoutService>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
keyRotationService = new UserKeyRotationService(
|
||||
mockUserVerificationService,
|
||||
@@ -85,6 +95,9 @@ describe("KeyRotationService", () => {
|
||||
mockSyncService,
|
||||
mockWebauthnLoginAdminService,
|
||||
mockLogService,
|
||||
mockVaultTimeoutService,
|
||||
mockToastService,
|
||||
mockI18nService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,6 +107,7 @@ describe("KeyRotationService", () => {
|
||||
|
||||
describe("rotateUserKeyAndEncryptedData", () => {
|
||||
let privateKey: BehaviorSubject<UserPrivateKey | null>;
|
||||
let keyPair: BehaviorSubject<{ privateKey: UserPrivateKey; publicKey: UserPublicKey }>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockKeyService.makeUserKey.mockResolvedValue([
|
||||
@@ -112,6 +126,8 @@ describe("KeyRotationService", () => {
|
||||
// Mock user verification
|
||||
mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue({
|
||||
masterKey: "mockMasterKey" as any,
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
email: "mockEmail",
|
||||
policyOptions: null,
|
||||
});
|
||||
|
||||
@@ -122,6 +138,12 @@ describe("KeyRotationService", () => {
|
||||
privateKey = new BehaviorSubject("mockPrivateKey" as any);
|
||||
mockKeyService.userPrivateKeyWithLegacySupport$.mockReturnValue(privateKey);
|
||||
|
||||
keyPair = new BehaviorSubject({
|
||||
privateKey: "mockPrivateKey",
|
||||
publicKey: "mockPublicKey",
|
||||
} as any);
|
||||
mockKeyService.userEncryptionKeyPair$.mockReturnValue(keyPair);
|
||||
|
||||
// Mock ciphers
|
||||
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
|
||||
mockCipherService.getRotatedData.mockResolvedValue(mockCiphers);
|
||||
@@ -147,8 +169,8 @@ describe("KeyRotationService", () => {
|
||||
mockWebauthnLoginAdminService.getRotatedData.mockResolvedValue(webauthn);
|
||||
});
|
||||
|
||||
it("rotates the user key and encrypted data", async () => {
|
||||
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser);
|
||||
it("rotates the user key and encrypted data legacy", async () => {
|
||||
await keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser);
|
||||
|
||||
expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled();
|
||||
const arg = mockApiService.postUserKeyUpdate.mock.calls[0][0];
|
||||
@@ -162,9 +184,47 @@ describe("KeyRotationService", () => {
|
||||
expect(arg.webauthnKeys.length).toBe(2);
|
||||
});
|
||||
|
||||
it("rotates the user key and encrypted data", async () => {
|
||||
await keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockNewMasterPassword",
|
||||
mockUser,
|
||||
);
|
||||
|
||||
expect(mockApiService.postUserKeyUpdateV2).toHaveBeenCalled();
|
||||
const arg = mockApiService.postUserKeyUpdateV2.mock.calls[0][0];
|
||||
expect(arg.oldMasterKeyAuthenticationHash).toBe("mockMasterPasswordHash");
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.email).toBe("mockEmail");
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.kdfType).toBe(
|
||||
DEFAULT_KDF_CONFIG.kdfType,
|
||||
);
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.kdfIterations).toBe(
|
||||
DEFAULT_KDF_CONFIG.iterations,
|
||||
);
|
||||
expect(arg.accountKeys.accountPublicKey).toBe(Utils.fromUtf8ToB64("mockPublicKey"));
|
||||
expect(arg.accountKeys.userKeyEncryptedAccountPrivateKey).toBe("mockEncryptedData");
|
||||
expect(arg.accountData.ciphers.length).toBe(2);
|
||||
expect(arg.accountData.folders.length).toBe(2);
|
||||
expect(arg.accountData.sends.length).toBe(2);
|
||||
});
|
||||
|
||||
it("legacy throws if master password provided is falsey", async () => {
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if master password provided is falsey", async () => {
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData("", "", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if user key creation fails", async () => {
|
||||
mockKeyService.makeUserKey.mockResolvedValueOnce([null, null]);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -175,15 +235,41 @@ describe("KeyRotationService", () => {
|
||||
]);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if no private key is found", async () => {
|
||||
privateKey.next(null);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if no private key is found", async () => {
|
||||
privateKey.next(null);
|
||||
keyPair.next(null);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if master password is incorrect", async () => {
|
||||
mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce(
|
||||
new Error("Invalid master password"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -193,15 +279,31 @@ describe("KeyRotationService", () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if server rotation fails", async () => {
|
||||
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if server rotation fails", async () => {
|
||||
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
|
||||
mockApiService.postUserKeyUpdateV2.mockRejectedValueOnce(new Error("mockError"));
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ import { VerificationType } from "@bitwarden/common/auth/enums/verification-type
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -15,13 +19,19 @@ import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../../auth/core";
|
||||
import { EmergencyAccessService } from "../../auth/emergency-access";
|
||||
|
||||
import { AccountKeysRequest } from "./request/account-keys.request";
|
||||
import { MasterPasswordUnlockDataRequest } from "./request/master-password-unlock-data.request";
|
||||
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
|
||||
import { UnlockDataRequest } from "./request/unlock-data.request";
|
||||
import { UpdateKeyRequest } from "./request/update-key.request";
|
||||
import { UserDataRequest } from "./request/userdata.request";
|
||||
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
||||
|
||||
@Injectable()
|
||||
@@ -40,14 +50,180 @@ export class UserKeyRotationService {
|
||||
private syncService: SyncService,
|
||||
private webauthnLoginAdminService: WebauthnLoginAdminService,
|
||||
private logService: LogService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with the it.
|
||||
* @param masterPassword current master password (used for validation)
|
||||
* @param oldMasterPassword: The current master password
|
||||
* @param newMasterPassword: The new master password
|
||||
* @param user: The user account
|
||||
* @param newMasterPasswordHint: The hint for the new master password
|
||||
*/
|
||||
async rotateUserKeyAndEncryptedData(masterPassword: string, user: Account): Promise<void> {
|
||||
async rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
oldMasterPassword: string,
|
||||
newMasterPassword: string,
|
||||
user: Account,
|
||||
newMasterPasswordHint?: string,
|
||||
): Promise<void> {
|
||||
this.logService.info("[Userkey rotation] Starting user key rotation...");
|
||||
if (!newMasterPassword) {
|
||||
this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!");
|
||||
throw new Error("Invalid master password");
|
||||
}
|
||||
|
||||
if ((await this.syncService.getLastSync()) === null) {
|
||||
this.logService.info("[Userkey rotation] Client was never synced. Aborting!");
|
||||
throw new Error(
|
||||
"The local vault is de-synced and the keys cannot be rotated. Please log out and log back in to resolve this issue.",
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
masterKey: oldMasterKey,
|
||||
email,
|
||||
kdfConfig,
|
||||
} = await this.userVerificationService.verifyUserByMasterPassword(
|
||||
{
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: oldMasterPassword,
|
||||
},
|
||||
user.id,
|
||||
user.email,
|
||||
);
|
||||
|
||||
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
|
||||
|
||||
const [newUnencryptedUserKey, newMasterKeyEncryptedUserKey] =
|
||||
await this.keyService.makeUserKey(newMasterKey);
|
||||
|
||||
if (!newUnencryptedUserKey || !newMasterKeyEncryptedUserKey) {
|
||||
this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
|
||||
throw new Error("User key could not be created");
|
||||
}
|
||||
|
||||
const newMasterKeyAuthenticationHash = await this.keyService.hashMasterKey(
|
||||
newMasterPassword,
|
||||
newMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
const masterPasswordUnlockData = new MasterPasswordUnlockDataRequest(
|
||||
kdfConfig,
|
||||
email,
|
||||
newMasterKeyAuthenticationHash,
|
||||
newMasterKeyEncryptedUserKey.encryptedString!,
|
||||
newMasterPasswordHint,
|
||||
);
|
||||
|
||||
const keyPair = await firstValueFrom(this.keyService.userEncryptionKeyPair$(user.id));
|
||||
if (keyPair == null) {
|
||||
this.logService.info("[Userkey rotation] Key pair is null. Aborting!");
|
||||
throw new Error("Key pair is null");
|
||||
}
|
||||
const { privateKey, publicKey } = keyPair;
|
||||
|
||||
const accountKeysRequest = new AccountKeysRequest(
|
||||
(await this.encryptService.encrypt(privateKey, newUnencryptedUserKey)).encryptedString!,
|
||||
Utils.fromBufferToB64(publicKey),
|
||||
);
|
||||
|
||||
const originalUserKey = await firstValueFrom(this.keyService.userKey$(user.id));
|
||||
if (originalUserKey == null) {
|
||||
this.logService.info("[Userkey rotation] Userkey is null. Aborting!");
|
||||
throw new Error("Userkey key is null");
|
||||
}
|
||||
|
||||
const rotatedCiphers = await this.cipherService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
const rotatedFolders = await this.folderService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
const rotatedSends = await this.sendService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
if (rotatedCiphers == null || rotatedFolders == null || rotatedSends == null) {
|
||||
this.logService.info("[Userkey rotation] ciphers, folders, or sends are null. Aborting!");
|
||||
throw new Error("ciphers, folders, or sends are null");
|
||||
}
|
||||
const accountDataRequest = new UserDataRequest(rotatedCiphers, rotatedFolders, rotatedSends);
|
||||
|
||||
const emergencyAccessUnlockData = await this.emergencyAccessService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
// Note: Reset password keys request model has user verification
|
||||
// properties, but the rotation endpoint uses its own MP hash.
|
||||
const organizationAccountRecoveryUnlockData = await this.resetPasswordService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
if (organizationAccountRecoveryUnlockData == null) {
|
||||
this.logService.info(
|
||||
"[Userkey rotation] Organization account recovery data is null. Aborting!",
|
||||
);
|
||||
throw new Error("Organization account recovery data is null");
|
||||
}
|
||||
|
||||
const passkeyUnlockData = await this.webauthnLoginAdminService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
|
||||
const trustedDeviceUnlockData = await this.deviceTrustService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
|
||||
const unlockDataRequest = new UnlockDataRequest(
|
||||
masterPasswordUnlockData,
|
||||
emergencyAccessUnlockData,
|
||||
organizationAccountRecoveryUnlockData,
|
||||
passkeyUnlockData,
|
||||
trustedDeviceUnlockData,
|
||||
);
|
||||
|
||||
const request = new RotateUserAccountKeysRequest(
|
||||
unlockDataRequest,
|
||||
accountKeysRequest,
|
||||
accountDataRequest,
|
||||
await this.keyService.hashMasterKey(oldMasterPassword, oldMasterKey),
|
||||
);
|
||||
|
||||
this.logService.info("[Userkey rotation] Posting user key rotation request to server");
|
||||
await this.apiService.postUserKeyUpdateV2(request);
|
||||
this.logService.info("[Userkey rotation] Userkey rotation request posted to server");
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("rotationCompletedTitle"),
|
||||
message: this.i18nService.t("rotationCompletedDesc"),
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// temporary until userkey can be better verified
|
||||
await this.vaultTimeoutService.logOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with the it.
|
||||
* @param masterPassword current master password (used for validation)
|
||||
* @deprecated
|
||||
*/
|
||||
async rotateUserKeyAndEncryptedDataLegacy(masterPassword: string, user: Account): Promise<void> {
|
||||
this.logService.info("[Userkey rotation] Starting legacy user key rotation...");
|
||||
if (!masterPassword) {
|
||||
this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!");
|
||||
throw new Error("Invalid master password");
|
||||
@@ -168,6 +344,7 @@ export class UserKeyRotationService {
|
||||
this.logService.info("[Userkey rotation] Rotating device trust...");
|
||||
await this.deviceTrustService.rotateDevicesTrust(user.id, newUserKey, masterPasswordHash);
|
||||
this.logService.info("[Userkey rotation] Device trust rotation completed");
|
||||
await this.vaultTimeoutService.logOut();
|
||||
}
|
||||
|
||||
private async encryptPrivateKey(
|
||||
|
||||
@@ -64,7 +64,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
try {
|
||||
await this.syncService.fullSync(false, true);
|
||||
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword, activeUser);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(masterPassword, activeUser);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -24,4 +24,8 @@ export class WebBiometricsService extends BiometricsService {
|
||||
}
|
||||
|
||||
async setShouldAutopromptNow(value: boolean): Promise<void> {}
|
||||
|
||||
async canEnableBiometricUnlock(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { NgModule } from "@angular/core";
|
||||
import { Route, RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component";
|
||||
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
|
||||
import {
|
||||
authGuard,
|
||||
lockGuard,
|
||||
@@ -65,7 +64,6 @@ import { AccountComponent } from "./auth/settings/account/account.component";
|
||||
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
|
||||
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
|
||||
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
|
||||
import { TwoFactorComponentV1 } from "./auth/two-factor-v1.component";
|
||||
import { UpdatePasswordComponent } from "./auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component";
|
||||
import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component";
|
||||
@@ -378,51 +376,28 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
TwoFactorComponentV1,
|
||||
TwoFactorAuthComponent,
|
||||
{
|
||||
path: "2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: TwoFactorComponentV1,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "2fa",
|
||||
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: TwoFactorAuthComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
titleAreaMaxWidth: "md",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "2fa",
|
||||
component: TwoFactorAuthComponent,
|
||||
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: TwoFactorAuthComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
titleAreaMaxWidth: "md",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,11 +2,23 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs";
|
||||
import {
|
||||
concatMap,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import {
|
||||
VaultTimeout,
|
||||
@@ -100,7 +112,12 @@ export class PreferencesComponent implements OnInit, OnDestroy {
|
||||
this.availableVaultTimeoutActions$ =
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$();
|
||||
|
||||
this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe(
|
||||
this.vaultTimeoutPolicyCallout = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
filter((policy) => policy != null),
|
||||
map((policy) => {
|
||||
let timeout;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -43,8 +43,6 @@ import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-
|
||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
|
||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { TwoFactorOptionsComponentV1 } from "../auth/two-factor-options-v1.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||
import { UpdatePasswordComponent } from "../auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||
@@ -148,12 +146,10 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
TwoFactorComponentV1,
|
||||
SsoComponentV1,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
TwoFactorSetupEmailComponent,
|
||||
TwoFactorOptionsComponentV1,
|
||||
TwoFactorRecoveryComponent,
|
||||
TwoFactorSetupComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
@@ -210,12 +206,10 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
TwoFactorComponentV1,
|
||||
SsoComponentV1,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
TwoFactorSetupEmailComponent,
|
||||
TwoFactorOptionsComponentV1,
|
||||
TwoFactorSetupComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorSetupWebAuthnComponent,
|
||||
|
||||
@@ -93,58 +93,60 @@
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let s of rows$ | async">
|
||||
<td bitCell (click)="editSend(s)" class="tw-cursor-pointer">
|
||||
<span class="tw-mr-2" aria-hidden="true">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-file" *ngIf="s.type == sendType.File"></i>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-file-text" *ngIf="s.type == sendType.Text"></i>
|
||||
</span>
|
||||
<button type="button" bitLink>
|
||||
{{ s.name }}
|
||||
</button>
|
||||
<ng-container *ngIf="s.disabled">
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'disabled' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "disabled" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.password">
|
||||
<i
|
||||
class="bwi bwi-key"
|
||||
appStopProp
|
||||
title="{{ 'password' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "password" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.maxAccessCountReached">
|
||||
<i
|
||||
class="bwi bwi-ban"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "maxAccessCountReached" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.expired">
|
||||
<i
|
||||
class="bwi bwi-clock"
|
||||
appStopProp
|
||||
title="{{ 'expired' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "expired" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.pendingDelete">
|
||||
<i
|
||||
class="bwi bwi-trash"
|
||||
appStopProp
|
||||
title="{{ 'pendingDeletion' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "pendingDeletion" | i18n }}</span>
|
||||
</ng-container>
|
||||
<div class="tw-flex tw-gap-2 tw-items-center">
|
||||
<span aria-hidden="true">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-file" *ngIf="s.type == sendType.File"></i>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-file-text" *ngIf="s.type == sendType.Text"></i>
|
||||
</span>
|
||||
<button type="button" bitLink>
|
||||
{{ s.name }}
|
||||
</button>
|
||||
<ng-container *ngIf="s.disabled">
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'disabled' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "disabled" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.password">
|
||||
<i
|
||||
class="bwi bwi-key"
|
||||
appStopProp
|
||||
title="{{ 'password' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "password" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.maxAccessCountReached">
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "maxAccessCountReached" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.expired">
|
||||
<i
|
||||
class="bwi bwi-clock"
|
||||
appStopProp
|
||||
title="{{ 'expired' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "expired" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.pendingDelete">
|
||||
<i
|
||||
class="bwi bwi-trash"
|
||||
appStopProp
|
||||
title="{{ 'pendingDeletion' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "pendingDeletion" | i18n }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-text-muted" (click)="editSend(s)" class="tw-cursor-pointer">
|
||||
<small bitTypography="body2" appStopProp>{{ s.deletionDate | date: "medium" }}</small>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -138,6 +138,7 @@ export class VaultItemsComponent {
|
||||
|
||||
return canRestore$;
|
||||
}),
|
||||
map((canRestore) => canRestore && this.showBulkTrashOptions),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AfterContentInit, Directive, HostListener, Input } from "@angular/core";
|
||||
|
||||
import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
@Directive({
|
||||
selector: "[app-link-sso]",
|
||||
})
|
||||
export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
|
||||
@Input() organization: Organization;
|
||||
returnUri = "/settings/organizations";
|
||||
redirectUri = window.location.origin + "/sso-connector.html";
|
||||
clientId = "web";
|
||||
|
||||
@HostListener("click", ["$event"])
|
||||
async onClick($event: MouseEvent) {
|
||||
$event.preventDefault();
|
||||
await this.submit(this.returnUri, true);
|
||||
}
|
||||
|
||||
async ngAfterContentInit() {
|
||||
this.identifier = this.organization.identifier;
|
||||
}
|
||||
}
|
||||
@@ -46,14 +46,12 @@
|
||||
bitMenuItem
|
||||
(click)="unlinkSso(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||
{{ "unlinkSso" | i18n }}
|
||||
</button>
|
||||
<ng-template #linkSso>
|
||||
<a href="#" bitMenuItem app-link-sso [organization]="organization">
|
||||
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
|
||||
<button type="button" bitMenuItem (click)="handleLinkSso(organization)">
|
||||
{{ "linkSso" | i18n }}
|
||||
</a>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<button *ngIf="showLeaveOrgOption" type="button" bitMenuItem (click)="leave(organization)">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
combineLatest,
|
||||
@@ -37,6 +35,7 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { EnrollMasterPasswordReset } from "../../../../admin-console/organizations/users/enroll-master-password-reset.component";
|
||||
import { LinkSsoService } from "../../../../auth/core/services";
|
||||
import { OptionsInput } from "../shared/components/vault-filter-section.component";
|
||||
import { OrganizationFilter } from "../shared/models/vault-filter.type";
|
||||
|
||||
@@ -45,12 +44,12 @@ import { OrganizationFilter } from "../shared/models/vault-filter.type";
|
||||
templateUrl: "organization-options.component.html",
|
||||
})
|
||||
export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
protected actionPromise: Promise<void | boolean>;
|
||||
protected actionPromise?: Promise<void | boolean>;
|
||||
protected resetPasswordPolicy?: Policy | undefined;
|
||||
protected loaded = false;
|
||||
protected hideMenu = false;
|
||||
protected showLeaveOrgOption = false;
|
||||
protected organization: OrganizationFilter;
|
||||
protected organization!: OrganizationFilter;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -72,11 +71,14 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
private configService: ConfigService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private linkSsoService: LinkSsoService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const resetPasswordPolicies$ = this.policyService.policies$.pipe(
|
||||
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
|
||||
const resetPasswordPolicies$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.policies$(userId)),
|
||||
map((policies) => policies.filter((p) => p.type == PolicyType.ResetPassword)),
|
||||
);
|
||||
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
@@ -145,6 +147,23 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
return org?.useSso && org?.identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links SSO to an organization.
|
||||
* @param organization The organization to link SSO to.
|
||||
*/
|
||||
async handleLinkSso(organization: Organization) {
|
||||
try {
|
||||
await this.linkSsoService.linkSso(organization.identifier);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async unlinkSso(org: Organization) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: org.name,
|
||||
@@ -163,7 +182,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("unlinkedSso"),
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -187,7 +206,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("leftOrganization"),
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -213,7 +232,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
// Remove reset password
|
||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
request.masterPasswordHash = "ignored";
|
||||
request.resetPasswordKey = null;
|
||||
request.resetPasswordKey = "";
|
||||
this.actionPromise =
|
||||
this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.organization.id,
|
||||
@@ -224,7 +243,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("withdrawPasswordResetSuccess"),
|
||||
});
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
@@ -6,6 +6,9 @@ import { firstValueFrom, merge, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -91,6 +94,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private trialFlowService = inject(TrialFlowService);
|
||||
protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
constructor(
|
||||
protected vaultFilterService: VaultFilterService,
|
||||
@@ -101,6 +105,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -110,10 +115,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
this.isLoaded = true;
|
||||
|
||||
// Without refactoring the entire component, we need to manually update the organization filter whenever the policies update
|
||||
merge(
|
||||
this.policyService.get$(PolicyType.SingleOrg),
|
||||
this.policyService.get$(PolicyType.PersonalOwnership),
|
||||
)
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
merge(
|
||||
this.policyService.policiesByType$(PolicyType.SingleOrg, userId).pipe(getFirstPolicy),
|
||||
this.policyService
|
||||
.policiesByType$(PolicyType.PersonalOwnership, userId)
|
||||
.pipe(getFirstPolicy),
|
||||
),
|
||||
),
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => this.addOrganizationFilter()),
|
||||
takeUntil(this.destroy$),
|
||||
@@ -150,7 +163,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
filter.selectedOrganizationNode = orgNode;
|
||||
}
|
||||
this.vaultFilterService.setOrganizationFilter(orgNode.node);
|
||||
await this.vaultFilterService.expandOrgFilter();
|
||||
const userId = await firstValueFrom(this.activeUserId$);
|
||||
await this.vaultFilterService.expandOrgFilter(userId);
|
||||
};
|
||||
|
||||
applyTypeFilter = async (filterNode: TreeNode<CipherTypeFilter>): Promise<void> => {
|
||||
@@ -190,9 +204,22 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected async addOrganizationFilter(): Promise<VaultFilterSection> {
|
||||
const singleOrgPolicy = await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
|
||||
const personalVaultPolicy = await this.policyService.policyAppliesToUser(
|
||||
PolicyType.PersonalOwnership,
|
||||
const singleOrgPolicy = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const personalVaultPolicy = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const addAction = !singleOrgPolicy
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
@@ -22,16 +23,19 @@ export abstract class VaultFilterService {
|
||||
folderTree$: Observable<TreeNode<FolderFilter>>;
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>>;
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
|
||||
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
|
||||
expandOrgFilter: () => Promise<void>;
|
||||
getOrganizationFilter: () => Observable<Organization>;
|
||||
setOrganizationFilter: (organization: Organization) => void;
|
||||
buildTypeTree: (
|
||||
abstract getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
|
||||
abstract setCollapsedFilterNodes: (
|
||||
collapsedFilterNodes: Set<string>,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
abstract expandOrgFilter: (userId: UserId) => Promise<void>;
|
||||
abstract getOrganizationFilter: () => Observable<Organization>;
|
||||
abstract setOrganizationFilter: (organization: Organization) => void;
|
||||
abstract buildTypeTree: (
|
||||
head: CipherTypeFilter,
|
||||
array: CipherTypeFilter[],
|
||||
) => Observable<TreeNode<CipherTypeFilter>>;
|
||||
// TODO: Remove this from org vault when collection admin service adopts state management
|
||||
reloadCollections?: (collections: CollectionAdminView[]) => void;
|
||||
clearOrganizationFilter: () => void;
|
||||
abstract reloadCollections?: (collections: CollectionAdminView[]) => void;
|
||||
abstract clearOrganizationFilter: () => void;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/../spec/fake-account-service";
|
||||
import { FakeActiveUserState } from "@bitwarden/common/../spec/fake-state";
|
||||
import { FakeSingleUserState } from "@bitwarden/common/../spec/fake-state";
|
||||
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, ReplaySubject } from "rxjs";
|
||||
@@ -42,7 +42,7 @@ describe("vault filter service", () => {
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
let collapsedGroupingsState: FakeActiveUserState<string[]>;
|
||||
let collapsedGroupingsState: FakeSingleUserState<string[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
organizationService = mock<OrganizationService>();
|
||||
@@ -65,11 +65,11 @@ describe("vault filter service", () => {
|
||||
organizationService.memberOrganizations$.mockReturnValue(organizations);
|
||||
folderService.folderViews$.mockReturnValue(folderViews);
|
||||
collectionService.decryptedCollections$ = collectionViews;
|
||||
policyService.policyAppliesToActiveUser$
|
||||
.calledWith(PolicyType.PersonalOwnership)
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.PersonalOwnership, mockUserId)
|
||||
.mockReturnValue(personalOwnershipPolicy);
|
||||
policyService.policyAppliesToActiveUser$
|
||||
.calledWith(PolicyType.SingleOrg)
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.SingleOrg, mockUserId)
|
||||
.mockReturnValue(singleOrgPolicy);
|
||||
cipherService.cipherViews$.mockReturnValue(cipherViews);
|
||||
|
||||
@@ -83,21 +83,21 @@ describe("vault filter service", () => {
|
||||
collectionService,
|
||||
accountService,
|
||||
);
|
||||
collapsedGroupingsState = stateProvider.activeUser.getFake(COLLAPSED_GROUPINGS);
|
||||
collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
|
||||
});
|
||||
|
||||
describe("collapsed filter nodes", () => {
|
||||
const nodes = new Set(["1", "2"]);
|
||||
|
||||
it("should update the collapsedFilterNodes$", async () => {
|
||||
await vaultFilterService.setCollapsedFilterNodes(nodes);
|
||||
await vaultFilterService.setCollapsedFilterNodes(nodes, mockUserId);
|
||||
|
||||
const collapsedGroupingsState = stateProvider.activeUser.getFake(COLLAPSED_GROUPINGS);
|
||||
expect(await firstValueFrom(collapsedGroupingsState.state$)).toEqual(Array.from(nodes));
|
||||
expect(collapsedGroupingsState.nextMock).toHaveBeenCalledWith([
|
||||
const collapsedGroupingsState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
Array.from(nodes),
|
||||
]);
|
||||
COLLAPSED_GROUPINGS,
|
||||
);
|
||||
expect(await firstValueFrom(collapsedGroupingsState.state$)).toEqual(Array.from(nodes));
|
||||
expect(collapsedGroupingsState.nextMock).toHaveBeenCalledWith(Array.from(nodes));
|
||||
});
|
||||
|
||||
it("loads from state on initialization", async () => {
|
||||
|
||||
@@ -23,8 +23,10 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -47,16 +49,27 @@ const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
memberOrganizations$ = this.activeUserId$.pipe(
|
||||
switchMap((id) => this.organizationService.memberOrganizations$(id)),
|
||||
);
|
||||
|
||||
collapsedFilterNodes$ = this.activeUserId$.pipe(
|
||||
switchMap((id) => this.collapsedGroupingsState(id).state$),
|
||||
map((state) => new Set(state)),
|
||||
);
|
||||
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>> = combineLatest([
|
||||
this.memberOrganizations$,
|
||||
this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg),
|
||||
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
|
||||
this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
),
|
||||
this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId),
|
||||
),
|
||||
),
|
||||
]).pipe(
|
||||
switchMap(([orgs, singleOrgPolicy, personalOwnershipPolicy]) =>
|
||||
this.buildOrganizationTree(orgs, singleOrgPolicy, personalOwnershipPolicy),
|
||||
@@ -97,11 +110,9 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree();
|
||||
|
||||
private collapsedGroupingsState: ActiveUserState<string[]> =
|
||||
this.stateProvider.getActive(COLLAPSED_GROUPINGS);
|
||||
|
||||
readonly collapsedFilterNodes$: Observable<Set<string>> =
|
||||
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
|
||||
private collapsedGroupingsState(userId: UserId): SingleUserState<string[]> {
|
||||
return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
@@ -119,8 +130,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>;
|
||||
}
|
||||
|
||||
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
|
||||
await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes));
|
||||
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>, userId: UserId): Promise<void> {
|
||||
await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes));
|
||||
}
|
||||
|
||||
protected async getCollapsedFilterNodes(): Promise<Set<string>> {
|
||||
@@ -143,13 +154,13 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async expandOrgFilter() {
|
||||
async expandOrgFilter(userId: UserId) {
|
||||
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
|
||||
if (!collapsedFilterNodes.has("AllVaults")) {
|
||||
return;
|
||||
}
|
||||
collapsedFilterNodes.delete("AllVaults");
|
||||
await this.setCollapsedFilterNodes(collapsedFilterNodes);
|
||||
await this.setCollapsedFilterNodes(collapsedFilterNodes, userId);
|
||||
}
|
||||
|
||||
protected async buildOrganizationTree(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Observable, Subject, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { VaultFilterService } from "../../services/abstractions/vault-filter.service";
|
||||
@@ -17,6 +19,7 @@ import { VaultFilter } from "../models/vault-filter.model";
|
||||
})
|
||||
export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private activeUserId$ = getUserId(this.accountService.activeAccount$);
|
||||
|
||||
@Input() activeFilter: VaultFilter;
|
||||
@Input() section: VaultFilterSection;
|
||||
@@ -29,6 +32,7 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private injector: Injector,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.vaultFilterService.collapsedFilterNodes$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -126,7 +130,8 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.collapsedFilterNodes.add(node.id);
|
||||
}
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes);
|
||||
const userId = await firstValueFrom(this.activeUserId$);
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes, userId);
|
||||
}
|
||||
|
||||
// an injector is necessary to pass data into a dynamic component
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
import { LinkSsoDirective } from "./components/link-sso.directive";
|
||||
import { OrganizationOptionsComponent } from "./components/organization-options.component";
|
||||
import { VaultFilterComponent } from "./components/vault-filter.component";
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service";
|
||||
@@ -12,7 +11,7 @@ import { VaultFilterService } from "./services/vault-filter.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule, SearchModule],
|
||||
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoDirective],
|
||||
declarations: [VaultFilterComponent, OrganizationOptionsComponent],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -142,13 +142,13 @@ describe("VaultOnboardingComponent", () => {
|
||||
});
|
||||
|
||||
describe("individualVaultPolicyCheck", () => {
|
||||
it("should set isIndividualPolicyVault to true", async () => {
|
||||
it("should set isIndividualPolicyVault to true", () => {
|
||||
individualVaultPolicyCheckSpy.mockRestore();
|
||||
const spy = jest
|
||||
.spyOn((component as any).policyService, "policyAppliesToActiveUser$")
|
||||
.spyOn((component as any).policyService, "policyAppliesToUser$")
|
||||
.mockReturnValue(of(true));
|
||||
|
||||
await component.individualVaultPolicyCheck();
|
||||
component.individualVaultPolicyCheck();
|
||||
fixture.detectChanges();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SimpleChanges,
|
||||
OnChanges,
|
||||
} from "@angular/core";
|
||||
import { Subject, takeUntil, Observable, firstValueFrom, fromEvent } from "rxjs";
|
||||
import { Subject, takeUntil, Observable, firstValueFrom, fromEvent, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -20,7 +20,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
@@ -67,7 +66,6 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
protected policyService: PolicyService,
|
||||
private apiService: ApiService,
|
||||
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
@@ -165,9 +163,14 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
individualVaultPolicyCheck() {
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((data) => {
|
||||
this.isIndividualPolicyVault = data;
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
|
||||
status: OrganizationUserStatusType.Confirmed,
|
||||
userId: "UserId",
|
||||
};
|
||||
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
|
||||
const policyAppliesToUser$ = new BehaviorSubject<boolean>(true);
|
||||
const collection = {
|
||||
id: "12345-5555",
|
||||
organizationId: "234534-34334",
|
||||
@@ -75,7 +75,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
|
||||
},
|
||||
{
|
||||
provide: PolicyService,
|
||||
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
|
||||
useValue: { policyAppliesToUser$: () => policyAppliesToUser$ },
|
||||
},
|
||||
{
|
||||
provide: RoutedVaultFilterService,
|
||||
@@ -129,13 +129,13 @@ describe("AdminConsoleCipherFormConfigService", () => {
|
||||
});
|
||||
|
||||
it("sets `allowPersonalOwnership`", async () => {
|
||||
policyAppliesToActiveUser$.next(true);
|
||||
policyAppliesToUser$.next(true);
|
||||
|
||||
let result = await adminConsoleConfigService.buildConfig("clone", cipherId);
|
||||
|
||||
expect(result.allowPersonalOwnership).toBe(false);
|
||||
|
||||
policyAppliesToActiveUser$.next(false);
|
||||
policyAppliesToUser$.next(false);
|
||||
|
||||
result = await adminConsoleConfigService.buildConfig("clone", cipherId);
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
|
||||
});
|
||||
|
||||
it("disables personal ownership when not cloning", async () => {
|
||||
policyAppliesToActiveUser$.next(false);
|
||||
policyAppliesToUser$.next(false);
|
||||
|
||||
let result = await adminConsoleConfigService.buildConfig("add", cipherId);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -30,9 +31,13 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
|
||||
private apiService: ApiService = inject(ApiService);
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
|
||||
private allowPersonalOwnership$ = this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||
.pipe(map((p) => !p));
|
||||
private allowPersonalOwnership$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId),
|
||||
),
|
||||
map((p) => !p),
|
||||
);
|
||||
|
||||
private organizationId$ = this.routedVaultFilterService.filter$.pipe(
|
||||
map((filter) => filter.organizationId),
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user