mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
refactor(emergency-access-takeover): [PM-18721][PM-21271] Integrate InputPasswordComponent in EmergencyAccessTakeoverDialogComponent (#14636)
Integrates the `InputPasswordComponent` within the `EmergencyAccessTakeoverDialogComponent` Feature Flag: `PM16117_ChangeExistingPasswordRefactor`
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -87,13 +85,23 @@ export class EmergencyAccessService
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns policies that apply to the grantor.
|
||||
* Returns policies that apply to the grantor if the grantor is the owner of an org, otherwise returns null.
|
||||
* Intended for grantee.
|
||||
* @param id emergency access id
|
||||
*
|
||||
* @remarks
|
||||
* The ONLY time the API call will return an array of policies is when the Grantor is the OWNER
|
||||
* of an organization. In all other scenarios the server returns null. Even if the Grantor
|
||||
* is the member of an org that has enforced MP policies, the server will still return null
|
||||
* because in the Emergency Access Takeover process, the Grantor gets removed from the org upon
|
||||
* takeover, and therefore the MP policies are irrelevant.
|
||||
*
|
||||
* The only scenario where a Grantor does NOT get removed from the org is when that Grantor is the
|
||||
* OWNER of the org. In that case the server returns Grantor policies and we enforce them on the client.
|
||||
*/
|
||||
async getGrantorPolicies(id: string): Promise<Policy[]> {
|
||||
const response = await this.emergencyAccessApiService.getEmergencyGrantorPolicies(id);
|
||||
let policies: Policy[];
|
||||
let policies: Policy[] = [];
|
||||
if (response.data != null && response.data.length > 0) {
|
||||
policies = response.data.map((policyResponse) => new Policy(new PolicyData(policyResponse)));
|
||||
}
|
||||
@@ -299,6 +307,10 @@ export class EmergencyAccessService
|
||||
|
||||
const encKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey);
|
||||
|
||||
if (encKey == null || !encKey[1].encryptedString) {
|
||||
throw new Error("masterKeyEncryptedUserKey not found");
|
||||
}
|
||||
|
||||
const request = new EmergencyAccessPasswordRequest();
|
||||
request.newMasterPasswordHash = masterKeyHash;
|
||||
request.key = encKey[1].encryptedString;
|
||||
@@ -405,6 +417,15 @@ export class EmergencyAccessService
|
||||
}
|
||||
|
||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
||||
return (await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey)).encryptedString;
|
||||
const publicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
if (publicKeyEncryptedUserKey == null || !publicKeyEncryptedUserKey.encryptedString) {
|
||||
throw new Error("publicKeyEncryptedUserKey not found");
|
||||
}
|
||||
|
||||
return publicKeyEncryptedUserKey.encryptedString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { OrganizationManagementPreferencesService } from "@bitwarden/common/admi
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -34,6 +36,10 @@ import {
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAddEditDialogResult,
|
||||
} from "./emergency-access-add-edit.component";
|
||||
import {
|
||||
EmergencyAccessTakeoverDialogComponent,
|
||||
EmergencyAccessTakeoverDialogResultType,
|
||||
} from "./takeover/emergency-access-takeover-dialog.component";
|
||||
import {
|
||||
EmergencyAccessTakeoverComponent,
|
||||
EmergencyAccessTakeoverResultType,
|
||||
@@ -69,6 +75,7 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private apiService: ApiService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
@@ -285,6 +292,45 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
}
|
||||
|
||||
takeover = async (details: GrantorEmergencyAccess) => {
|
||||
const changePasswordRefactorFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
if (changePasswordRefactorFlag) {
|
||||
if (!details || !details.email || !details.id) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("grantorDetailsNotFound"),
|
||||
});
|
||||
this.logService.error(
|
||||
"Grantor details not found when attempting emergency access takeover",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const grantorName = this.userNamePipe.transform(details);
|
||||
|
||||
const dialogRef = EmergencyAccessTakeoverDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
grantorName,
|
||||
grantorEmail: details.email,
|
||||
emergencyAccessId: details.id,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === EmergencyAccessTakeoverDialogResultType.Done) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("passwordResetFor", grantorName),
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(details),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "takeover" | i18n }}
|
||||
<small class="tw-text-muted" *ngIf="dialogData.grantorName">{{ dialogData.grantorName }}</small>
|
||||
</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
@if (initializing) {
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- TODO: PM-22237 -->
|
||||
<!-- <bit-callout type="warning">{{
|
||||
"emergencyAccessLoggedOutWarning" | i18n: dialogData.grantorName
|
||||
}}</bit-callout> -->
|
||||
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
(isSubmitting)="handleIsSubmittingChange($event)"
|
||||
></auth-input-password>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="submitting$ | async"
|
||||
(click)="handlePrimaryButtonClick()"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,160 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
PasswordInputResult,
|
||||
} from "@bitwarden/auth/angular";
|
||||
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 {
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { EmergencyAccessService } from "../../../emergency-access";
|
||||
|
||||
type EmergencyAccessTakeoverDialogData = {
|
||||
grantorName: string;
|
||||
grantorEmail: string;
|
||||
/** Traces a unique emergency request */
|
||||
emergencyAccessId: string;
|
||||
};
|
||||
|
||||
export const EmergencyAccessTakeoverDialogResultType = {
|
||||
Done: "done",
|
||||
} as const;
|
||||
|
||||
export type EmergencyAccessTakeoverDialogResultType =
|
||||
(typeof EmergencyAccessTakeoverDialogResultType)[keyof typeof EmergencyAccessTakeoverDialogResultType];
|
||||
|
||||
/**
|
||||
* This component is used by a Grantee to take over emergency access of a Grantor's account
|
||||
* by changing the Grantor's master password. It is displayed as a dialog when the Grantee
|
||||
* clicks the "Takeover" button while on the `/settings/emergency-access` page (see `EmergencyAccessComponent`).
|
||||
*
|
||||
* @link https://bitwarden.com/help/emergency-access/
|
||||
*/
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-emergency-access-takeover-dialog",
|
||||
templateUrl: "./emergency-access-takeover-dialog.component.html",
|
||||
imports: [
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
I18nPipe,
|
||||
InputPasswordComponent,
|
||||
],
|
||||
})
|
||||
export class EmergencyAccessTakeoverDialogComponent implements OnInit {
|
||||
@ViewChild(InputPasswordComponent)
|
||||
inputPasswordComponent: InputPasswordComponent | undefined = undefined;
|
||||
|
||||
private parentSubmittingBehaviorSubject = new BehaviorSubject(false);
|
||||
parentSubmitting$ = this.parentSubmittingBehaviorSubject.asObservable();
|
||||
|
||||
private childSubmittingBehaviorSubject = new BehaviorSubject(false);
|
||||
childSubmitting$ = this.childSubmittingBehaviorSubject.asObservable();
|
||||
|
||||
submitting$ = combineLatest([this.parentSubmitting$, this.childSubmitting$]).pipe(
|
||||
map(([parentIsSubmitting, childIsSubmitting]) => parentIsSubmitting || childIsSubmitting),
|
||||
);
|
||||
|
||||
initializing = true;
|
||||
inputPasswordFlow = InputPasswordFlow.ChangePasswordDelegation;
|
||||
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected dialogData: EmergencyAccessTakeoverDialogData,
|
||||
private accountService: AccountService,
|
||||
private dialogRef: DialogRef<EmergencyAccessTakeoverDialogResultType>,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const grantorPolicies = await this.emergencyAccessService.getGrantorPolicies(
|
||||
this.dialogData.emergencyAccessId,
|
||||
);
|
||||
|
||||
this.masterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(activeUserId, grantorPolicies),
|
||||
);
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
protected handlePrimaryButtonClick = async () => {
|
||||
if (!this.inputPasswordComponent) {
|
||||
throw new Error("InputPasswordComponent is not initialized");
|
||||
}
|
||||
|
||||
await this.inputPasswordComponent.submit();
|
||||
};
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.parentSubmittingBehaviorSubject.next(true);
|
||||
|
||||
try {
|
||||
await this.emergencyAccessService.takeover(
|
||||
this.dialogData.emergencyAccessId,
|
||||
passwordInputResult.newPassword,
|
||||
this.dialogData.grantorEmail,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
} finally {
|
||||
this.parentSubmittingBehaviorSubject.next(false);
|
||||
}
|
||||
|
||||
this.dialogRef.close(EmergencyAccessTakeoverDialogResultType.Done);
|
||||
}
|
||||
|
||||
protected handleIsSubmittingChange(isSubmitting: boolean) {
|
||||
this.childSubmittingBehaviorSubject.next(isSubmitting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open an EmergencyAccessTakeoverDialogComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param dialogConfig Configuration for the dialog
|
||||
*/
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<
|
||||
EmergencyAccessTakeoverDialogData,
|
||||
DialogRef<EmergencyAccessTakeoverDialogResultType, unknown>
|
||||
>,
|
||||
) => {
|
||||
return dialogService.open<
|
||||
EmergencyAccessTakeoverDialogResultType,
|
||||
EmergencyAccessTakeoverDialogData
|
||||
>(EmergencyAccessTakeoverDialogComponent, dialogConfig);
|
||||
};
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export type InitiationPath =
|
||||
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
||||
initializing = true;
|
||||
|
||||
/** Password Manager or Secrets Manager */
|
||||
|
||||
@@ -5370,6 +5370,9 @@
|
||||
"emergencyRejected": {
|
||||
"message": "Emergency access rejected"
|
||||
},
|
||||
"grantorDetailsNotFound": {
|
||||
"message": "Grantor details not found"
|
||||
},
|
||||
"passwordResetFor": {
|
||||
"message": "Password reset for $USER$. You can now login using the new password.",
|
||||
"placeholders": {
|
||||
@@ -5773,12 +5776,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"emergencyAccessLoggedOutWarning": {
|
||||
"message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "John Smith"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thisUser": {
|
||||
"message": "this user"
|
||||
},
|
||||
"resetPasswordMasterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require the master password to meet the following requirements:"
|
||||
},
|
||||
"changePasswordDelegationMasterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require the master password to meet the following requirements:"
|
||||
},
|
||||
"resetPasswordSuccess": {
|
||||
"message": "Password reset success!"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user