mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Merge remote-tracking branch 'origin' into auth/pm-16783/tech-debt-fixes-toast-service
This commit is contained in:
@@ -7,9 +7,11 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm
|
||||
import { OrganizationService } 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 { 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";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -45,11 +47,9 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.collectionIds = this.loadCipherCollections();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
@@ -95,7 +95,8 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
this.cipherDomain.collectionIds = selectedCollectionIds;
|
||||
try {
|
||||
this.formPromise = this.saveCollections();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.formPromise = this.saveCollections(activeUserId);
|
||||
await this.formPromise;
|
||||
this.onSavedCollections.emit();
|
||||
this.toastService.showToast({
|
||||
@@ -114,8 +115,8 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected loadCipherCollections() {
|
||||
@@ -129,7 +130,7 @@ export class CollectionsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
protected saveCollections() {
|
||||
return this.cipherService.saveCollectionsWithServer(this.cipherDomain);
|
||||
protected saveCollections(userId: UserId) {
|
||||
return this.cipherService.saveCollectionsWithServer(this.cipherDomain, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy
|
||||
|
||||
async loadNewUserData() {
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
selectedRegion: selectedRegion$ | async,
|
||||
} as data"
|
||||
>
|
||||
<div class="environment-selector-btn">
|
||||
<div class="tw-text-sm tw-text-muted tw-leading-7 tw-font-normal tw-pl-4">
|
||||
{{ "accessing" | i18n }}:
|
||||
<button
|
||||
type="button"
|
||||
@@ -13,7 +13,7 @@
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="cdk-overlay-container"
|
||||
>
|
||||
<span class="text-primary">
|
||||
<span class="tw-text-primary-600 tw-text-sm tw-font-semibold">
|
||||
<ng-container *ngIf="data.selectedRegion; else fallback">
|
||||
{{ data.selectedRegion.domain }}
|
||||
</ng-container>
|
||||
@@ -35,9 +35,9 @@
|
||||
(backdropClick)="isOpen = false"
|
||||
(detach)="close()"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="tw-box-content">
|
||||
<div
|
||||
class="environment-selector-dialog"
|
||||
class="tw-bg-background tw-w-full tw-shadow-md tw-p-2 tw-rounded-md"
|
||||
data-testid="environment-selector-dialog"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
@@ -48,7 +48,7 @@
|
||||
<ng-container *ngFor="let region of availableRegions; let i = index">
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
class="tw-text-main tw-w-full tw-text-left tw-py-0 tw-border tw-border-transparent tw-transition-all tw-duration-200 tw-ease-in-out tw-pr-2 tw-rounded-md"
|
||||
(click)="toggle(region.key)"
|
||||
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
|
||||
[attr.data-testid]="'environment-selector-dialog-item-' + i"
|
||||
@@ -65,7 +65,7 @@
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
class="tw-text-main tw-w-full tw-text-left tw-py-0 tw-pr-2 tw-border tw-border-transparent tw-transition-all tw-duration-200 tw-ease-in-out tw-rounded-md"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
|
||||
data-testid="environment-selector-dialog-item-self-hosted"
|
||||
|
||||
@@ -64,11 +64,12 @@ export class LoginViaAuthRequestComponentV1
|
||||
|
||||
protected StateEnum = State;
|
||||
protected state = State.StandardAuthRequest;
|
||||
|
||||
protected webVaultUrl: string;
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
private resendTimeout = 12000;
|
||||
protected deviceManagementUrl: string;
|
||||
|
||||
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
|
||||
|
||||
@@ -95,6 +96,12 @@ export class LoginViaAuthRequestComponentV1
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
// Get the web vault URL from the environment service
|
||||
environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => {
|
||||
this.webVaultUrl = env.getWebVaultUrl();
|
||||
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
|
||||
});
|
||||
|
||||
// Gets signalR push notification
|
||||
// Only fires on approval to prevent enumeration
|
||||
this.authRequestService.authRequestPushNotification$
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { AbstractControl, UntypedFormBuilder, ValidatorFn, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { RegisterResponse } from "@bitwarden/common/auth/models/response/register.response";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "../../platform/abstractions/form-validation-errors.service";
|
||||
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
|
||||
import { InputsFieldMatch } from "../validators/inputs-field-match.validator";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@Directive()
|
||||
export class RegisterComponent extends CaptchaProtectedComponent implements OnInit {
|
||||
@Input() isInTrialFlow = false;
|
||||
@Output() createdAccount = new EventEmitter<string>();
|
||||
|
||||
showPassword = false;
|
||||
formPromise: Promise<RegisterResponse>;
|
||||
referenceData: ReferenceEventRequest;
|
||||
showTerms = true;
|
||||
showErrorSummary = false;
|
||||
passwordStrengthResult: any;
|
||||
characterMinimumMessage: string;
|
||||
minimumLength = Utils.minimumPasswordLength;
|
||||
color: string;
|
||||
text: string;
|
||||
|
||||
formGroup = this.formBuilder.group(
|
||||
{
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
name: [""],
|
||||
masterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
|
||||
confirmMasterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
|
||||
hint: [
|
||||
null,
|
||||
[
|
||||
InputsFieldMatch.validateInputsDoesntMatch(
|
||||
"masterPassword",
|
||||
this.i18nService.t("hintEqualsPassword"),
|
||||
),
|
||||
],
|
||||
],
|
||||
checkForBreaches: [true],
|
||||
acceptPolicies: [false, [this.acceptPoliciesValidation()]],
|
||||
},
|
||||
{
|
||||
validator: InputsFieldMatch.validateFormInputsMatch(
|
||||
"masterPassword",
|
||||
"confirmMasterPassword",
|
||||
this.i18nService.t("masterPassDoesntMatch"),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
protected successRoute = "login";
|
||||
|
||||
protected accountCreated = false;
|
||||
|
||||
protected captchaBypassToken: string = null;
|
||||
|
||||
// allows for extending classes to modify the register request before sending
|
||||
// currently used by web to add organization invitation details
|
||||
protected modifyRegisterRequest: (request: RegisterRequest) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
protected formValidationErrorService: FormValidationErrorsService,
|
||||
protected formBuilder: UntypedFormBuilder,
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected router: Router,
|
||||
i18nService: I18nService,
|
||||
protected keyService: KeyService,
|
||||
protected apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
protected logService: LogService,
|
||||
protected auditService: AuditService,
|
||||
protected dialogService: DialogService,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// 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.setupCaptcha();
|
||||
}
|
||||
|
||||
async submit(showToast = true) {
|
||||
let email = this.formGroup.value.email;
|
||||
email = email.trim().toLowerCase();
|
||||
let name = this.formGroup.value.name;
|
||||
name = name === "" ? null : name; // Why do we do this?
|
||||
const masterPassword = this.formGroup.value.masterPassword;
|
||||
try {
|
||||
if (!this.accountCreated) {
|
||||
const registerResponse = await this.registerAccount(
|
||||
await this.buildRegisterRequest(email, masterPassword, name),
|
||||
showToast,
|
||||
);
|
||||
if (!registerResponse.successful) {
|
||||
return;
|
||||
}
|
||||
this.captchaBypassToken = registerResponse.captchaBypassToken;
|
||||
this.accountCreated = true;
|
||||
}
|
||||
if (this.isInTrialFlow) {
|
||||
if (!this.accountCreated) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("trialAccountCreated"),
|
||||
});
|
||||
}
|
||||
const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken);
|
||||
if (loginResponse.captchaRequired) {
|
||||
return;
|
||||
}
|
||||
this.createdAccount.emit(this.formGroup.value.email);
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("newAccountCreated"),
|
||||
});
|
||||
// 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: { email: email } });
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
}
|
||||
|
||||
getStrengthResult(result: any) {
|
||||
this.passwordStrengthResult = result;
|
||||
}
|
||||
|
||||
getPasswordScoreText(event: PasswordColorText) {
|
||||
this.color = event.color;
|
||||
this.text = event.text;
|
||||
}
|
||||
|
||||
private getErrorToastMessage() {
|
||||
const error: AllValidationErrors = this.formValidationErrorService
|
||||
.getFormValidationErrors(this.formGroup.controls)
|
||||
.shift();
|
||||
|
||||
if (error) {
|
||||
switch (error.errorName) {
|
||||
case "email":
|
||||
return this.i18nService.t("invalidEmail");
|
||||
case "inputsDoesntMatchError":
|
||||
return this.i18nService.t("masterPassDoesntMatch");
|
||||
case "inputsMatchError":
|
||||
return this.i18nService.t("hintEqualsPassword");
|
||||
case "minlength":
|
||||
return this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength);
|
||||
default:
|
||||
return this.i18nService.t(this.errorTag(error));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private errorTag(error: AllValidationErrors): string {
|
||||
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
|
||||
return `${error.controlName}${name}`;
|
||||
}
|
||||
|
||||
//validation would be ignored on selfhosted
|
||||
private acceptPoliciesValidation(): ValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
const ctrlValue = control.value;
|
||||
|
||||
return !ctrlValue && this.showTerms ? { required: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private async validateRegistration(showToast: boolean): Promise<{ isValid: boolean }> {
|
||||
this.formGroup.markAllAsTouched();
|
||||
this.showErrorSummary = true;
|
||||
|
||||
if (this.formGroup.get("acceptPolicies").hasError("required")) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("acceptPoliciesRequired"),
|
||||
});
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
//web
|
||||
if (this.formGroup.invalid && !showToast) {
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
//desktop, browser
|
||||
if (this.formGroup.invalid && showToast) {
|
||||
const errorText = this.getErrorToastMessage();
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: errorText,
|
||||
});
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
const passwordWeak =
|
||||
this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3;
|
||||
const passwordLeak =
|
||||
this.formGroup.controls.checkForBreaches.value &&
|
||||
(await this.auditService.passwordLeaked(this.formGroup.controls.masterPassword.value)) > 0;
|
||||
|
||||
if (passwordWeak && passwordLeak) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return { isValid: false };
|
||||
}
|
||||
} else if (passwordWeak) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return { isValid: false };
|
||||
}
|
||||
} else if (passwordLeak) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "exposedMasterPassword" },
|
||||
content: { key: "exposedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return { isValid: false };
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
private async buildRegisterRequest(
|
||||
email: string,
|
||||
masterPassword: string,
|
||||
name: string,
|
||||
): Promise<RegisterRequest> {
|
||||
const hint = this.formGroup.value.hint;
|
||||
const kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
const key = await this.keyService.makeMasterKey(masterPassword, email, kdfConfig);
|
||||
const newUserKey = await this.keyService.makeUserKey(key);
|
||||
const masterKeyHash = await this.keyService.hashMasterKey(masterPassword, key);
|
||||
const keys = await this.keyService.makeKeyPair(newUserKey[0]);
|
||||
const request = new RegisterRequest(
|
||||
email,
|
||||
name,
|
||||
masterKeyHash,
|
||||
hint,
|
||||
newUserKey[1].encryptedString,
|
||||
this.referenceData,
|
||||
this.captchaToken,
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
||||
if (this.modifyRegisterRequest) {
|
||||
await this.modifyRegisterRequest(request);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private async registerAccount(
|
||||
request: RegisterRequest,
|
||||
showToast: boolean,
|
||||
): Promise<{ successful: boolean; captchaBypassToken?: string }> {
|
||||
if (!(await this.validateRegistration(showToast)).isValid) {
|
||||
return { successful: false };
|
||||
}
|
||||
this.formPromise = this.apiService.postRegister(request);
|
||||
try {
|
||||
const response = await this.formPromise;
|
||||
return { successful: true, captchaBypassToken: response.captchaBypassToken };
|
||||
} catch (e) {
|
||||
if (this.handleCaptchaRequired(e)) {
|
||||
return { successful: false };
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async logIn(
|
||||
email: string,
|
||||
masterPassword: string,
|
||||
captchaBypassToken: string,
|
||||
): Promise<{ captchaRequired: boolean }> {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
captchaBypassToken,
|
||||
null,
|
||||
);
|
||||
const loginResponse = await this.loginStrategyService.logIn(credentials);
|
||||
if (this.handleCaptchaRequired(loginResponse)) {
|
||||
return { captchaRequired: true };
|
||||
}
|
||||
return { captchaRequired: false };
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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";
|
||||
@@ -47,7 +47,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
resetPasswordAutoEnroll = false;
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
successRoute = "vault";
|
||||
userId: UserId;
|
||||
activeUserId: UserId;
|
||||
|
||||
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
ForceSetPasswordReason = ForceSetPasswordReason;
|
||||
@@ -96,10 +96,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.userId),
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
@@ -111,7 +111,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
} else {
|
||||
// Try to get orgSsoId from state as fallback
|
||||
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier();
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
||||
}
|
||||
}),
|
||||
filter((orgSsoId) => orgSsoId != null),
|
||||
@@ -167,10 +167,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
|
||||
const existingUserPrivateKey = (await firstValueFrom(
|
||||
this.keyService.userPrivateKey$(this.userId),
|
||||
this.keyService.userPrivateKey$(this.activeUserId),
|
||||
)) as Uint8Array;
|
||||
const existingUserPublicKey = await firstValueFrom(
|
||||
this.keyService.userPublicKey$(this.userId),
|
||||
this.keyService.userPublicKey$(this.activeUserId),
|
||||
);
|
||||
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||
@@ -217,7 +217,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
this.userId,
|
||||
this.activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
});
|
||||
@@ -260,7 +260,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
this.userId,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
@@ -269,9 +269,9 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.userId);
|
||||
await this.keyService.setUserKey(userKey[0], this.userId);
|
||||
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
||||
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
||||
|
||||
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||
// Existing TDE users will have private key set on sync or on login
|
||||
@@ -280,7 +280,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.userId);
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
||||
}
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
@@ -288,6 +288,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,8 @@ export class SsoComponent implements OnInit {
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
|
||||
|
||||
// Users enrolled in admin acct recovery can be forced to set a new password after
|
||||
// having the admin set a temp password for them (affects TDE & standard users)
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwoFactorMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
@@ -214,7 +214,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
}
|
||||
}
|
||||
|
||||
async selectOtherTwofactorMethod() {
|
||||
async selectOtherTwoFactorMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||
const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed);
|
||||
if (response.result === TwoFactorOptionsDialogResult.Provider) {
|
||||
@@ -262,7 +262,8 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
|
||||
@@ -102,6 +102,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
|
||||
// Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired
|
||||
@@ -287,7 +288,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
|
||||
@@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { OrganizationUserStatusType } 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -73,10 +74,8 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
this.cipher = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
@@ -104,10 +103,8 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const cipherView = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
|
||||
@@ -147,13 +147,15 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap
|
||||
import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
@@ -194,8 +196,6 @@ import { AppIdService } from "@bitwarden/common/platform/services/app-id.service
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
|
||||
@@ -282,12 +282,6 @@ import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
ImportService,
|
||||
ImportServiceAbstraction,
|
||||
} from "@bitwarden/importer/core";
|
||||
import {
|
||||
KeyService,
|
||||
DefaultKeyService,
|
||||
@@ -302,7 +296,12 @@ import {
|
||||
DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
DefaultTaskService,
|
||||
NewDeviceVerificationNoticeService,
|
||||
PasswordRepromptService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
import {
|
||||
VaultExportService,
|
||||
VaultExportServiceAbstraction,
|
||||
@@ -312,9 +311,6 @@ import {
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@@ -805,7 +801,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SsoLoginServiceAbstraction,
|
||||
useClass: SsoLoginService,
|
||||
deps: [StateProvider],
|
||||
deps: [StateProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
@@ -826,26 +822,6 @@ const safeProviders: SafeProvider[] = [
|
||||
MigrationRunner,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ImportApiServiceAbstraction,
|
||||
useClass: ImportApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ImportServiceAbstraction,
|
||||
useClass: ImportService,
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
FolderServiceAbstraction,
|
||||
ImportApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CollectionService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
PinServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IndividualVaultExportServiceAbstraction,
|
||||
useClass: IndividualVaultExportService,
|
||||
@@ -1492,6 +1468,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: PasswordLoginStrategyData,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaskService,
|
||||
useClass: DefaultTaskService,
|
||||
deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
|
||||
import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/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";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
GeneratorType,
|
||||
DefaultPasswordBoundaries as DefaultBoundaries,
|
||||
} from "@bitwarden/generator-core";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
UsernameGeneratorOptions,
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
export class EmailForwarderOptions {
|
||||
name: string;
|
||||
value: string;
|
||||
validForSelfHosted: boolean;
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class GeneratorComponent implements OnInit, OnDestroy {
|
||||
@Input() comingFromAddEdit = false;
|
||||
@Input() type: GeneratorType | "";
|
||||
@Output() onSelected = new EventEmitter<string>();
|
||||
|
||||
usernameGeneratingPromise: Promise<string>;
|
||||
typeOptions: any[];
|
||||
usernameTypeOptions: any[];
|
||||
subaddressOptions: any[];
|
||||
catchallOptions: any[];
|
||||
forwardOptions: EmailForwarderOptions[];
|
||||
usernameOptions: UsernameGeneratorOptions = { website: null };
|
||||
passwordOptions: PasswordGeneratorOptions = {};
|
||||
username = "-";
|
||||
password = "-";
|
||||
showOptions = false;
|
||||
avoidAmbiguous = false;
|
||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||
usernameWebsite: string = null;
|
||||
|
||||
get passTypeOptions() {
|
||||
return this._passTypeOptions.filter((o) => !o.disabled);
|
||||
}
|
||||
private _passTypeOptions: { name: string; value: GeneratorType; disabled: boolean }[];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private isInitialized$ = new BehaviorSubject(false);
|
||||
|
||||
// update screen reader minimum password length with 500ms debounce
|
||||
// so that the user isn't flooded with status updates
|
||||
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
||||
DefaultBoundaries.length.min,
|
||||
);
|
||||
protected passwordOptionsMinLengthForReader$ = this._passwordOptionsMinLengthForReader.pipe(
|
||||
map((val) => val || DefaultBoundaries.length.min),
|
||||
debounceTime(500),
|
||||
);
|
||||
|
||||
private _password = new BehaviorSubject<string>("-");
|
||||
|
||||
constructor(
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected accountService: AccountService,
|
||||
protected i18nService: I18nService,
|
||||
protected logService: LogService,
|
||||
protected route: ActivatedRoute,
|
||||
protected ngZone: NgZone,
|
||||
private win: Window,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("password"), value: "password" },
|
||||
{ name: i18nService.t("username"), value: "username" },
|
||||
];
|
||||
this._passTypeOptions = [
|
||||
{ name: i18nService.t("password"), value: "password", disabled: false },
|
||||
{ name: i18nService.t("passphrase"), value: "passphrase", disabled: false },
|
||||
];
|
||||
this.usernameTypeOptions = [
|
||||
{
|
||||
name: i18nService.t("plusAddressedEmail"),
|
||||
value: "subaddress",
|
||||
desc: i18nService.t("plusAddressedEmailDesc"),
|
||||
},
|
||||
{
|
||||
name: i18nService.t("catchallEmail"),
|
||||
value: "catchall",
|
||||
desc: i18nService.t("catchallEmailDesc"),
|
||||
},
|
||||
{
|
||||
name: i18nService.t("forwardedEmail"),
|
||||
value: "forwarded",
|
||||
desc: i18nService.t("forwardedEmailDesc"),
|
||||
},
|
||||
{ name: i18nService.t("randomWord"), value: "word" },
|
||||
];
|
||||
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||
|
||||
this.forwardOptions = [
|
||||
{ name: "", value: "", validForSelfHosted: false },
|
||||
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
this._password.pipe(debounceTime(250)).subscribe((password) => {
|
||||
ngZone.run(() => {
|
||||
this.password = password;
|
||||
});
|
||||
this.passwordGenerationService.addHistory(this.password).catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
|
||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||
|
||||
if (!this.type) {
|
||||
if (navigationType) {
|
||||
this.type = navigationType;
|
||||
} else {
|
||||
this.type = this.passwordOptions.type === "username" ? "username" : "password";
|
||||
}
|
||||
}
|
||||
|
||||
this.passwordOptions.type =
|
||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||
|
||||
const overrideType = this.enforcedPasswordPolicyOptions.overridePasswordType ?? "";
|
||||
const isDisabled = overrideType.length
|
||||
? (value: string, policyValue: string) => value !== policyValue
|
||||
: (_value: string, _policyValue: string) => false;
|
||||
for (const option of this._passTypeOptions) {
|
||||
option.disabled = isDisabled(option.value, overrideType);
|
||||
}
|
||||
|
||||
if (this.usernameOptions.type == null) {
|
||||
this.usernameOptions.type = "word";
|
||||
}
|
||||
if (
|
||||
this.usernameOptions.subaddressEmail == null ||
|
||||
this.usernameOptions.subaddressEmail === ""
|
||||
) {
|
||||
this.usernameOptions.subaddressEmail = accountEmail;
|
||||
}
|
||||
if (this.usernameWebsite == null) {
|
||||
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||
} else {
|
||||
this.usernameOptions.website = this.usernameWebsite;
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
combineLatest([
|
||||
this.route.queryParams.pipe(first()),
|
||||
this.accountService.activeAccount$.pipe(first()),
|
||||
this.passwordGenerationService.getOptions$(),
|
||||
this.usernameGenerationService.getOptions$(),
|
||||
])
|
||||
.pipe(
|
||||
map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({
|
||||
navigationType: qParams.type as GeneratorType,
|
||||
accountEmail: account.email,
|
||||
passwordOptions,
|
||||
passwordPolicy,
|
||||
usernameOptions,
|
||||
})),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((options) => {
|
||||
this.passwordOptions = options.passwordOptions;
|
||||
this.enforcedPasswordPolicyOptions = options.passwordPolicy;
|
||||
this.usernameOptions = options.usernameOptions;
|
||||
|
||||
this.cascadeOptions(options.navigationType, options.accountEmail);
|
||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
this.regenerate().catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.isInitialized$.next(true);
|
||||
});
|
||||
|
||||
// once initialization is complete, `ngOnInit` should return.
|
||||
//
|
||||
// FIXME(#6944): if a sync is in progress, wait to complete until after
|
||||
// the sync completes.
|
||||
await firstValueFrom(
|
||||
this.isInitialized$.pipe(
|
||||
skipWhile((initialized) => !initialized),
|
||||
takeUntil(this.destroy$),
|
||||
),
|
||||
);
|
||||
|
||||
if (this.usernameWebsite !== null) {
|
||||
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
|
||||
this.subaddressOptions.push(websiteOption);
|
||||
this.catchallOptions.push(websiteOption);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.isInitialized$.complete();
|
||||
this._passwordOptionsMinLengthForReader.complete();
|
||||
}
|
||||
|
||||
async typeChanged() {
|
||||
await this.savePasswordOptions();
|
||||
}
|
||||
|
||||
async regenerate() {
|
||||
if (this.type === "password") {
|
||||
await this.regeneratePassword();
|
||||
} else if (this.type === "username") {
|
||||
await this.regenerateUsername();
|
||||
}
|
||||
}
|
||||
|
||||
async sliderChanged() {
|
||||
// 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.savePasswordOptions();
|
||||
await this.passwordGenerationService.addHistory(this.password);
|
||||
}
|
||||
|
||||
async onPasswordOptionsMinNumberInput($event: Event) {
|
||||
// `savePasswordOptions()` replaces the null
|
||||
this.passwordOptions.number = null;
|
||||
|
||||
await this.savePasswordOptions();
|
||||
|
||||
// fixes UI desync that occurs when minNumber has a fixed value
|
||||
// that is reset through normalization
|
||||
($event.target as HTMLInputElement).value = `${this.passwordOptions.minNumber}`;
|
||||
}
|
||||
|
||||
async setPasswordOptionsNumber($event: boolean) {
|
||||
this.passwordOptions.number = $event;
|
||||
// `savePasswordOptions()` replaces the null
|
||||
this.passwordOptions.minNumber = null;
|
||||
|
||||
await this.savePasswordOptions();
|
||||
}
|
||||
|
||||
async onPasswordOptionsMinSpecialInput($event: Event) {
|
||||
// `savePasswordOptions()` replaces the null
|
||||
this.passwordOptions.special = null;
|
||||
|
||||
await this.savePasswordOptions();
|
||||
|
||||
// fixes UI desync that occurs when minSpecial has a fixed value
|
||||
// that is reset through normalization
|
||||
($event.target as HTMLInputElement).value = `${this.passwordOptions.minSpecial}`;
|
||||
}
|
||||
|
||||
async setPasswordOptionsSpecial($event: boolean) {
|
||||
this.passwordOptions.special = $event;
|
||||
// `savePasswordOptions()` replaces the null
|
||||
this.passwordOptions.minSpecial = null;
|
||||
|
||||
await this.savePasswordOptions();
|
||||
}
|
||||
|
||||
async sliderInput() {
|
||||
await this.normalizePasswordOptions();
|
||||
}
|
||||
|
||||
async savePasswordOptions() {
|
||||
// map navigation state into generator type
|
||||
const restoreType = this.passwordOptions.type;
|
||||
if (this.type === "username") {
|
||||
this.passwordOptions.type = this.type;
|
||||
}
|
||||
|
||||
// save options
|
||||
await this.normalizePasswordOptions();
|
||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||
|
||||
// restore the original format
|
||||
this.passwordOptions.type = restoreType;
|
||||
}
|
||||
|
||||
async saveUsernameOptions() {
|
||||
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||
if (this.usernameOptions.type === "forwarded") {
|
||||
this.username = "-";
|
||||
}
|
||||
}
|
||||
|
||||
async regeneratePassword() {
|
||||
this._password.next(
|
||||
await this.passwordGenerationService.generatePassword(this.passwordOptions),
|
||||
);
|
||||
}
|
||||
|
||||
regenerateUsername() {
|
||||
return this.generateUsername();
|
||||
}
|
||||
|
||||
async generateUsername() {
|
||||
try {
|
||||
this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername(
|
||||
this.usernameOptions,
|
||||
);
|
||||
this.username = await this.usernameGeneratingPromise;
|
||||
if (this.username === "" || this.username === null) {
|
||||
this.username = "-";
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
copy() {
|
||||
const password = this.type === "password";
|
||||
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||
this.platformUtilsService.copyToClipboard(
|
||||
password ? this.password : this.username,
|
||||
copyOptions,
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
"valueCopied",
|
||||
this.i18nService.t(password ? "password" : "username"),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
select() {
|
||||
this.onSelected.emit(this.type === "password" ? this.password : this.username);
|
||||
}
|
||||
|
||||
toggleOptions() {
|
||||
this.showOptions = !this.showOptions;
|
||||
}
|
||||
|
||||
regenerateWithoutButtonPress() {
|
||||
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
|
||||
}
|
||||
|
||||
private async normalizePasswordOptions() {
|
||||
// Application level normalize options dependent on class variables
|
||||
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
|
||||
|
||||
if (
|
||||
!this.passwordOptions.uppercase &&
|
||||
!this.passwordOptions.lowercase &&
|
||||
!this.passwordOptions.number &&
|
||||
!this.passwordOptions.special
|
||||
) {
|
||||
this.passwordOptions.lowercase = true;
|
||||
if (this.win != null) {
|
||||
const lowercase = this.win.document.querySelector("#lowercase") as HTMLInputElement;
|
||||
if (lowercase) {
|
||||
lowercase.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
||||
this.passwordOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { GeneratedPasswordHistory } from "@bitwarden/generator-history";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Directive()
|
||||
export class PasswordGeneratorHistoryComponent implements OnInit {
|
||||
history: GeneratedPasswordHistory[] = [];
|
||||
|
||||
constructor(
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
private win: Window,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.history = await this.passwordGenerationService.getHistory();
|
||||
}
|
||||
|
||||
clear = async () => {
|
||||
this.history = await this.passwordGenerationService.clear();
|
||||
};
|
||||
|
||||
copy(password: string) {
|
||||
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||
this.platformUtilsService.copyToClipboard(password, copyOptions);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Type, inject } from "@angular/core";
|
||||
import { Route, Routes } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { componentRouteSwap } from "../../utils/component-route-swap";
|
||||
|
||||
/**
|
||||
* Helper function to swap between two components based on the GeneratorToolsModernization feature flag.
|
||||
* @param defaultComponent - The current non-refreshed component to render.
|
||||
* @param refreshedComponent - The new refreshed component to render.
|
||||
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
|
||||
* @param altOptions - The alt route options to apply to the alt component.
|
||||
*/
|
||||
export function generatorSwap(
|
||||
defaultComponent: Type<any>,
|
||||
refreshedComponent: Type<any>,
|
||||
options: Route,
|
||||
altOptions?: Route,
|
||||
): Routes {
|
||||
return componentRouteSwap(
|
||||
defaultComponent,
|
||||
refreshedComponent,
|
||||
async () => {
|
||||
const configService = inject(ConfigService);
|
||||
return configService.getFeatureFlag(FeatureFlag.GeneratorToolsModernization);
|
||||
},
|
||||
options,
|
||||
altOptions,
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Navigation, Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { extensionRefreshRedirect } from "./extension-refresh-redirect";
|
||||
|
||||
describe("extensionRefreshRedirect", () => {
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = mock<ConfigService>();
|
||||
router = mock<Router>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true when ExtensionRefresh flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const result = await TestBed.runInInjectionContext(() =>
|
||||
extensionRefreshRedirect("/redirect")(),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh);
|
||||
expect(router.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns UrlTree when ExtensionRefresh flag is enabled and preserves query params", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
const urlTree = new UrlTree();
|
||||
urlTree.queryParams = { test: "test" };
|
||||
|
||||
const navigation: Navigation = {
|
||||
extras: {},
|
||||
id: 0,
|
||||
initialUrl: new UrlTree(),
|
||||
extractedUrl: urlTree,
|
||||
trigger: "imperative",
|
||||
previousNavigation: undefined,
|
||||
};
|
||||
|
||||
router.getCurrentNavigation.mockReturnValue(navigation);
|
||||
|
||||
await TestBed.runInInjectionContext(() => extensionRefreshRedirect("/redirect")());
|
||||
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], {
|
||||
queryParams: urlTree.queryParams,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { UrlTree, Router } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
/**
|
||||
* Helper function to redirect to a new URL based on the ExtensionRefresh feature flag.
|
||||
* @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled.
|
||||
*/
|
||||
export function extensionRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
|
||||
return async () => {
|
||||
const configService = inject(ConfigService);
|
||||
const router = inject(Router);
|
||||
|
||||
const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
|
||||
if (shouldRedirect) {
|
||||
const currentNavigation = router.getCurrentNavigation();
|
||||
const queryParams = currentNavigation?.extractedUrl?.queryParams || {};
|
||||
|
||||
// Preserve query params when redirecting as it is likely that the refreshed component
|
||||
// will be consuming the same query params.
|
||||
return router.createUrlTree([redirectUrl], { queryParams });
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Type, inject } from "@angular/core";
|
||||
import { Route, Routes } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { componentRouteSwap } from "./component-route-swap";
|
||||
|
||||
/**
|
||||
* Helper function to swap between two components based on the ExtensionRefresh feature flag.
|
||||
* @param defaultComponent - The current non-refreshed component to render.
|
||||
* @param refreshedComponent - The new refreshed component to render.
|
||||
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
|
||||
* @param altOptions - The alt route options to apply to the alt component.
|
||||
*/
|
||||
export function extensionRefreshSwap(
|
||||
defaultComponent: Type<any>,
|
||||
refreshedComponent: Type<any>,
|
||||
options: Route,
|
||||
altOptions?: Route,
|
||||
): Routes {
|
||||
return componentRouteSwap(
|
||||
defaultComponent,
|
||||
refreshedComponent,
|
||||
async () => {
|
||||
const configService = inject(ConfigService);
|
||||
return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
|
||||
},
|
||||
options,
|
||||
altOptions,
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,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 { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -101,8 +102,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
private personalOwnershipPolicyAppliesToActiveUser: boolean;
|
||||
private previousCipherId: string;
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
const creationDate = this.datePipe.transform(
|
||||
@@ -125,7 +124,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected policyService: PolicyService,
|
||||
protected logService: LogService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
protected organizationService: OrganizationService,
|
||||
private organizationService: OrganizationService,
|
||||
protected dialogService: DialogService,
|
||||
protected win: Window,
|
||||
protected datePipe: DatePipe,
|
||||
@@ -263,12 +262,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.title = this.i18nService.t("addItem");
|
||||
}
|
||||
|
||||
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(activeUserId);
|
||||
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
if (this.cipher == null) {
|
||||
if (this.editMode) {
|
||||
const cipher = await this.loadCipher();
|
||||
const cipher = await this.loadCipher(activeUserId);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
@@ -420,9 +420,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.cipher.id = null;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.encryptCipher(activeUserId);
|
||||
try {
|
||||
this.formPromise = this.saveCipher(cipher);
|
||||
@@ -516,7 +514,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.deleteCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.deletePromise = this.deleteCipher(activeUserId);
|
||||
await this.deletePromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -542,7 +541,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
this.restorePromise = this.restoreCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.restorePromise = this.restoreCipher(activeUserId);
|
||||
await this.restorePromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -725,8 +725,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return allCollections.filter((c) => !c.readOnly);
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected encryptCipher(userId: UserId) {
|
||||
@@ -746,14 +746,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
: this.cipherService.updateWithServer(cipher, orgAdmin);
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, this.asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, this.asAdmin);
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId, this.asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId, this.asAdmin);
|
||||
}
|
||||
|
||||
protected restoreCipher() {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, this.asAdmin);
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId, this.asAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -773,8 +773,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return this.ownershipOptions[0].value;
|
||||
}
|
||||
|
||||
async loadAddEditCipherInfo(): Promise<boolean> {
|
||||
const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$);
|
||||
async loadAddEditCipherInfo(userId: UserId): Promise<boolean> {
|
||||
const addEditCipherInfo: any = await firstValueFrom(
|
||||
this.cipherService.addEditCipherInfo$(userId),
|
||||
);
|
||||
const loadedSavedInfo = addEditCipherInfo != null;
|
||||
|
||||
if (loadedSavedInfo) {
|
||||
@@ -787,7 +789,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
await this.cipherService.setAddEditCipherInfo(null);
|
||||
await this.cipherService.setAddEditCipherInfo(null, userId);
|
||||
|
||||
return loadedSavedInfo;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -84,9 +85,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
||||
this.cipherDomain = await this.formPromise;
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
@@ -125,12 +124,11 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id, activeUserId);
|
||||
const updatedCipher = await this.deletePromises[attachment.id];
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const cipher = new Cipher(updatedCipher);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
@@ -228,10 +226,8 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
@@ -287,7 +283,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
@@ -301,7 +297,10 @@ export class AttachmentsComponent implements OnInit {
|
||||
);
|
||||
|
||||
// 3. Delete old
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(
|
||||
attachment.id,
|
||||
activeUserId,
|
||||
);
|
||||
await this.deletePromises[attachment.id];
|
||||
const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id);
|
||||
if (foundAttachment.length > 0) {
|
||||
@@ -335,16 +334,16 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected saveCipherAttachment(file: File, userId: UserId) {
|
||||
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId);
|
||||
}
|
||||
|
||||
protected deleteCipherAttachment(attachmentId: string) {
|
||||
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId);
|
||||
protected deleteCipherAttachment(attachmentId: string, userId: UserId) {
|
||||
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId, userId);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -39,10 +40,8 @@ export class PasswordHistoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const decCipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@@ -41,11 +44,20 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
protected searchService: SearchService,
|
||||
protected cipherService: CipherService,
|
||||
protected accountService: AccountService,
|
||||
) {
|
||||
this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => {
|
||||
void this.doSearch(ciphers);
|
||||
this.loaded = true;
|
||||
});
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.cipherService.cipherViews$(userId).pipe(map((ciphers) => ({ userId, ciphers }))),
|
||||
),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(({ userId, ciphers }) => {
|
||||
void this.doSearch(ciphers, userId);
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -122,10 +134,16 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||
|
||||
protected async doSearch(indexedCiphers?: CipherView[]) {
|
||||
indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$));
|
||||
protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) {
|
||||
// Get userId from activeAccount if not provided from parent stream
|
||||
if (!userId) {
|
||||
userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
}
|
||||
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
|
||||
indexedCiphers =
|
||||
indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$(userId)));
|
||||
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$(userId));
|
||||
if (failedCiphers != null && failedCiphers.length > 0) {
|
||||
indexedCiphers = [...failedCiphers, ...indexedCiphers];
|
||||
}
|
||||
|
||||
@@ -11,29 +11,30 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { filter, firstValueFrom, map, Observable } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.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 { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionId, 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 { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherType, FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
@@ -65,7 +66,6 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
showPrivateKey: boolean;
|
||||
canAccessPremium: boolean;
|
||||
showPremiumRequiredTotp: boolean;
|
||||
showUpgradeRequiredTotp: boolean;
|
||||
totpCode: string;
|
||||
totpCodeFormatted: string;
|
||||
totpDash: number;
|
||||
@@ -80,7 +80,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
private previousCipherId: string;
|
||||
private passwordReprompted = false;
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
private destroyed$ = new Subject<void>();
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
@@ -144,38 +144,39 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
async load() {
|
||||
this.cleanUp();
|
||||
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
// Grab individual cipher from `cipherViews$` for the most up-to-date information
|
||||
this.cipher = await firstValueFrom(
|
||||
this.cipherService.cipherViews$.pipe(
|
||||
map((ciphers) => ciphers.find((c) => c.id === this.cipherId)),
|
||||
this.cipherService
|
||||
.cipherViews$(activeUserId)
|
||||
.pipe(
|
||||
map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
|
||||
filter((cipher) => !!cipher),
|
||||
),
|
||||
);
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe((cipher) => {
|
||||
this.cipher = cipher;
|
||||
});
|
||||
|
||||
this.canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
||||
);
|
||||
this.showPremiumRequiredTotp =
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId;
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||
this.collectionId as CollectionId,
|
||||
]);
|
||||
|
||||
this.showUpgradeRequiredTotp =
|
||||
this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp;
|
||||
|
||||
if (this.cipher.folderId) {
|
||||
this.folder = await (
|
||||
await firstValueFrom(this.folderService.folderViews$(activeUserId))
|
||||
).find((f) => f.id == this.cipher.folderId);
|
||||
}
|
||||
|
||||
const canGenerateTotp = this.cipher.organizationId
|
||||
? this.cipher.organizationUseTotp
|
||||
: this.canAccessPremium;
|
||||
|
||||
if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) {
|
||||
if (
|
||||
this.cipher.type === CipherType.Login &&
|
||||
this.cipher.login.totp &&
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium)
|
||||
) {
|
||||
await this.totpUpdateCode();
|
||||
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||
await this.totpTick(interval);
|
||||
@@ -250,7 +251,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deleteCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.deleteCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -272,7 +274,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.restoreCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.restoreCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -380,7 +383,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
if (cipherId) {
|
||||
await this.cipherService.updateLastLaunchedDate(cipherId);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.updateLastLaunchedDate(cipherId, activeUserId);
|
||||
}
|
||||
|
||||
this.platformUtilsService.launchUri(uri.launchUri);
|
||||
@@ -498,14 +502,14 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
a.downloading = false;
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id);
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected restoreCipher() {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id);
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected async promptPassword() {
|
||||
@@ -524,6 +528,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
this.showCardNumber = false;
|
||||
this.showCardCode = false;
|
||||
this.passwordReprompted = false;
|
||||
this.destroyed$.next();
|
||||
if (this.totpInterval) {
|
||||
clearInterval(this.totpInterval);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,13 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard";
|
||||
@@ -36,19 +33,23 @@ describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
const isSelfHost = jest.fn().mockResolvedValue(false);
|
||||
const isSelfHost = jest.fn().mockReturnValue(false);
|
||||
const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false);
|
||||
const policyAppliesToActiveUser$ = jest.fn().mockReturnValue(new BehaviorSubject<boolean>(false));
|
||||
const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
|
||||
const getProfileCreationDate = jest.fn().mockResolvedValue(eightDaysAgo);
|
||||
const hasMasterPasswordAndMasterKeyHash = jest.fn().mockResolvedValue(true);
|
||||
const getUserSSOBound = jest.fn().mockResolvedValue(false);
|
||||
const getUserSSOBoundAdminOwner = jest.fn().mockResolvedValue(false);
|
||||
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockClear();
|
||||
isSelfHost.mockClear();
|
||||
getProfileCreationDate.mockClear();
|
||||
getProfileTwoFactorEnabled.mockClear();
|
||||
policyAppliesToActiveUser$.mockClear();
|
||||
createUrlTree.mockClear();
|
||||
hasMasterPasswordAndMasterKeyHash.mockClear();
|
||||
getUserSSOBound.mockClear();
|
||||
getUserSSOBoundAdminOwner.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -57,10 +58,15 @@ describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
{ provide: NewDeviceVerificationNoticeService, useValue: { noticeState$ } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{ provide: PlatformUtilsService, useValue: { isSelfHost } },
|
||||
{ provide: PolicyService, useValue: { policyAppliesToActiveUser$ } },
|
||||
{ provide: UserVerificationService, useValue: { hasMasterPasswordAndMasterKeyHash } },
|
||||
{
|
||||
provide: VaultProfileService,
|
||||
useValue: { getProfileCreationDate, getProfileTwoFactorEnabled },
|
||||
useValue: {
|
||||
getProfileCreationDate,
|
||||
getProfileTwoFactorEnabled,
|
||||
getUserSSOBound,
|
||||
getUserSSOBoundAdminOwner,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -92,7 +98,7 @@ describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
expect(isSelfHost).not.toHaveBeenCalled();
|
||||
expect(getProfileTwoFactorEnabled).not.toHaveBeenCalled();
|
||||
expect(getProfileCreationDate).not.toHaveBeenCalled();
|
||||
expect(policyAppliesToActiveUser$).not.toHaveBeenCalled();
|
||||
expect(hasMasterPasswordAndMasterKeyHash).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,13 +129,6 @@ describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` SSO is required", async () => {
|
||||
policyAppliesToActiveUser$.mockReturnValueOnce(new BehaviorSubject(true));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
expect(policyAppliesToActiveUser$).toHaveBeenCalledWith(PolicyType.RequireSso);
|
||||
});
|
||||
|
||||
it("returns `true` when the profile was created less than a week ago", async () => {
|
||||
const sixDaysAgo = new Date();
|
||||
sixDaysAgo.setDate(sixDaysAgo.getDate() - 6);
|
||||
@@ -139,6 +138,63 @@ describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` when the profile service throws an error", async () => {
|
||||
getProfileCreationDate.mockRejectedValueOnce(new Error("test"));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
describe("SSO bound", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFeatureFlag.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns "true" when the user is SSO bound and not an admin or owner', async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(true);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(false);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns "true" when the user is an admin or owner of an SSO bound organization and has not logged in with their master password', async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(true);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(true);
|
||||
hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(false);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("shows notice when the user is an admin or owner of an SSO bound organization and logged in with their master password", async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(true);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(true);
|
||||
hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
|
||||
it("shows notice when the user that is not in an SSO bound organization", async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(false);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(false);
|
||||
hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("temp flag", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
|
||||
@@ -22,8 +19,8 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
|
||||
const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService);
|
||||
const accountService = inject(AccountService);
|
||||
const platformUtilsService = inject(PlatformUtilsService);
|
||||
const policyService = inject(PolicyService);
|
||||
const vaultProfileService = inject(VaultProfileService);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
|
||||
if (route.queryParams["fromNewDeviceVerification"]) {
|
||||
return true;
|
||||
@@ -47,17 +44,28 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
|
||||
return router.createUrlTree(["/login"]);
|
||||
}
|
||||
|
||||
const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id);
|
||||
const isSelfHosted = await platformUtilsService.isSelfHost();
|
||||
const requiresSSO = await isSSORequired(policyService);
|
||||
const isProfileLessThanWeekOld = await profileIsLessThanWeekOld(
|
||||
vaultProfileService,
|
||||
currentAcct.id,
|
||||
);
|
||||
try {
|
||||
const isSelfHosted = platformUtilsService.isSelfHost();
|
||||
const userIsSSOUser = await ssoAppliesToUser(
|
||||
userVerificationService,
|
||||
vaultProfileService,
|
||||
currentAcct.id,
|
||||
);
|
||||
const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id);
|
||||
const isProfileLessThanWeekOld = await profileIsLessThanWeekOld(
|
||||
vaultProfileService,
|
||||
currentAcct.id,
|
||||
);
|
||||
|
||||
// When any of the following are true, the device verification notice is
|
||||
// not applicable for the user.
|
||||
if (has2FAEnabled || isSelfHosted || requiresSSO || isProfileLessThanWeekOld) {
|
||||
// When any of the following are true, the device verification notice is
|
||||
// not applicable for the user. When the user has *not* logged in with their
|
||||
// master password, assume they logged in with SSO.
|
||||
if (has2FAEnabled || isSelfHosted || userIsSSOUser || isProfileLessThanWeekOld) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Skip showing the notice if there was a problem determining applicability
|
||||
// The most likely problem to occur is the user not having a network connection
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -101,9 +109,39 @@ async function profileIsLessThanWeekOld(
|
||||
return !isMoreThan7DaysAgo(creationDate);
|
||||
}
|
||||
|
||||
/** Returns true when the user is required to login via SSO */
|
||||
async function isSSORequired(policyService: PolicyService) {
|
||||
return firstValueFrom(policyService.policyAppliesToActiveUser$(PolicyType.RequireSso));
|
||||
/**
|
||||
* Returns true when either:
|
||||
* - The user is SSO bound to an organization and is not an Admin or Owner
|
||||
* - The user is an Admin or Owner of an organization with SSO bound and has not logged in with their master password
|
||||
*
|
||||
* NOTE: There are edge cases where this does not satisfy the original requirement of showing the notice to
|
||||
* users who are subject to the SSO required policy. When Owners and Admins log in with their MP they will see the notice
|
||||
* when they log in with SSO they will not. This is a concession made because the original logic references policies would not work for TDE users.
|
||||
* When this guard is run for those users a sync hasn't occurred and thus the policies are not available.
|
||||
*/
|
||||
async function ssoAppliesToUser(
|
||||
userVerificationService: UserVerificationService,
|
||||
vaultProfileService: VaultProfileService,
|
||||
userId: string,
|
||||
) {
|
||||
const userSSOBound = await vaultProfileService.getUserSSOBound(userId);
|
||||
const userSSOBoundAdminOwner = await vaultProfileService.getUserSSOBoundAdminOwner(userId);
|
||||
const userLoggedInWithMP = await userLoggedInWithMasterPassword(userVerificationService, userId);
|
||||
|
||||
const nonOwnerAdminSsoUser = userSSOBound && !userSSOBoundAdminOwner;
|
||||
const ssoAdminOwnerLoggedInWithMP = userSSOBoundAdminOwner && !userLoggedInWithMP;
|
||||
|
||||
return nonOwnerAdminSsoUser || ssoAdminOwnerLoggedInWithMP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the user logged in with their master password.
|
||||
*/
|
||||
async function userLoggedInWithMasterPassword(
|
||||
userVerificationService: UserVerificationService,
|
||||
userId: string,
|
||||
) {
|
||||
return userVerificationService.hasMasterPasswordAndMasterKeyHash(userId);
|
||||
}
|
||||
|
||||
/** Returns the true when the date given is older than 7 days */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
|
||||
import { VaultProfileService } from "./vault-profile.service";
|
||||
|
||||
@@ -13,6 +14,12 @@ describe("VaultProfileService", () => {
|
||||
creationDate: hardcodedDateString,
|
||||
twoFactorEnabled: true,
|
||||
id: "new-user-id",
|
||||
organizations: [
|
||||
{
|
||||
ssoBound: true,
|
||||
type: OrganizationUserType.Admin,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -91,4 +98,64 @@ describe("VaultProfileService", () => {
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserSSOBound", () => {
|
||||
it("calls `getProfile` when stored ssoBound property is not stored", async () => {
|
||||
expect(service["userIsSsoBound"]).toBeNull();
|
||||
|
||||
const userIsSsoBound = await service.getUserSSOBound(userId);
|
||||
|
||||
expect(userIsSsoBound).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["userIsSsoBound"] = false;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const userIsSsoBound = await service.getUserSSOBound(userId);
|
||||
|
||||
expect(userIsSsoBound).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when ssoBound property is already stored", async () => {
|
||||
service["userIsSsoBound"] = false;
|
||||
|
||||
const userIsSsoBound = await service.getUserSSOBound(userId);
|
||||
|
||||
expect(userIsSsoBound).toBe(false);
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserSSOBoundAdminOwner", () => {
|
||||
it("calls `getProfile` when stored userIsSsoBoundAdminOwner property is not stored", async () => {
|
||||
expect(service["userIsSsoBoundAdminOwner"]).toBeNull();
|
||||
|
||||
const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
|
||||
|
||||
expect(userIsSsoBoundAdminOwner).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["userIsSsoBoundAdminOwner"] = false;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
|
||||
|
||||
expect(userIsSsoBoundAdminOwner).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when userIsSsoBoundAdminOwner property is already stored", async () => {
|
||||
service["userIsSsoBoundAdminOwner"] = false;
|
||||
|
||||
const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
|
||||
|
||||
expect(userIsSsoBoundAdminOwner).toBe(false);
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
|
||||
@Injectable({
|
||||
@@ -24,6 +25,12 @@ export class VaultProfileService {
|
||||
/** True when 2FA is enabled on the profile. */
|
||||
private profile2FAEnabled: boolean | null = null;
|
||||
|
||||
/** True when ssoBound is true for any of the users organizations */
|
||||
private userIsSsoBound: boolean | null = null;
|
||||
|
||||
/** True when the user is an admin or owner of the ssoBound organization */
|
||||
private userIsSsoBoundAdminOwner: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Returns the creation date of the profile.
|
||||
* Note: `Date`s are mutable in JS, creating a new
|
||||
@@ -52,12 +59,43 @@ export class VaultProfileService {
|
||||
return profile.twoFactorEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user logs in with SSO for any organization.
|
||||
*/
|
||||
async getUserSSOBound(userId: string): Promise<boolean> {
|
||||
if (this.userIsSsoBound !== null && userId === this.userId) {
|
||||
return Promise.resolve(this.userIsSsoBound);
|
||||
}
|
||||
|
||||
await this.fetchAndCacheProfile();
|
||||
|
||||
return !!this.userIsSsoBound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the user is an Admin or Owner of an organization with `ssoBound` true.
|
||||
*/
|
||||
async getUserSSOBoundAdminOwner(userId: string): Promise<boolean> {
|
||||
if (this.userIsSsoBoundAdminOwner !== null && userId === this.userId) {
|
||||
return Promise.resolve(this.userIsSsoBoundAdminOwner);
|
||||
}
|
||||
|
||||
await this.fetchAndCacheProfile();
|
||||
|
||||
return !!this.userIsSsoBoundAdminOwner;
|
||||
}
|
||||
|
||||
private async fetchAndCacheProfile(): Promise<ProfileResponse> {
|
||||
const profile = await this.apiService.getProfile();
|
||||
|
||||
this.userId = profile.id;
|
||||
this.profileCreatedDate = profile.creationDate;
|
||||
this.profile2FAEnabled = profile.twoFactorEnabled;
|
||||
const ssoBoundOrg = profile.organizations.find((org) => org.ssoBound);
|
||||
this.userIsSsoBound = !!ssoBoundOrg;
|
||||
this.userIsSsoBoundAdminOwner =
|
||||
ssoBoundOrg?.type === OrganizationUserType.Admin ||
|
||||
ssoBoundOrg?.type === OrganizationUserType.Owner;
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -11,19 +11,17 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ActiveUserState, 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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service";
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
@@ -33,8 +31,6 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
private readonly collapsedGroupings$: Observable<Set<string>> =
|
||||
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
protected folderService: FolderService,
|
||||
@@ -66,7 +62,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> {
|
||||
const transformation = async (storedFolders: FolderView[]) => {
|
||||
const transformation = async (storedFolders: FolderView[], userId: UserId) => {
|
||||
let folders: FolderView[];
|
||||
|
||||
// If no org or "My Vault" is selected, show all folders
|
||||
@@ -74,7 +70,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
folders = storedFolders;
|
||||
} else {
|
||||
// Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const ciphers = await this.cipherService.getAllDecrypted(userId);
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
|
||||
folders = storedFolders.filter(
|
||||
(f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null,
|
||||
@@ -88,9 +84,13 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
});
|
||||
};
|
||||
|
||||
return this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.folderService.folderViews$(userId)),
|
||||
mergeMap((folders) => from(transformation(folders))),
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.folderService
|
||||
.folderViews$(userId)
|
||||
.pipe(mergeMap((folders) => from(transformation(folders, userId)))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
async getFolderNested(id: string): Promise<TreeNode<FolderView>> {
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const folders = await this.getAllFoldersNested(
|
||||
await firstValueFrom(this.folderService.folderViews$(activeUserId)),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user