From 3a298bd9899fd3d88cce8c46329f79ca13624478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 11 Oct 2022 13:08:48 +0100 Subject: [PATCH] [EC-377] Transition Policy service into providing observables (#3259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added abstractions for PolicyApiService and PolicyService * Added implementations for PolicyApiService and PolicyService * Updated all references to new PolicyApiService and PolicyService * Deleted old PolicyService abstraction and implementation * Fixed CLI import path for policy.service * Fixed main.background.ts policyApiService dependency for policyService * Ran prettier * Updated policy-api.service with the correct imports * [EC-377] Removed methods from StateService that read policies * [EC-377] Updated policy service getAll method to use observable collection * [EC-377] Added first unit tests for policy service * [EC-377] Added more unit tests for Policy Service * [EC-376] Sorted methods order in PolicyApiService * [EC-376] Removed unused clearCache method from PolicyService * [EC-376] Added upsert method to PolicyService * [EC-376] PolicyApiService putPolicy method now upserts data to PolicyService * [EC-377] Removed tests for deleted clearCache method * [EC-377] Added unit test for PolicyService.upsert * [EC-377] Updated references to state service observables * [EC-377] Removed getAll method from PolicyService and refactored components to use observable collection * [EC-377] Updated components to use concatMap instead of async subscribe * [EC-377] Removed getPolicyForOrganization from policyApiService * [EC-377] Updated policyAppliesToUser to return observable collection * [EC-377] Changed policyService.policyAppliesToUser to return observable * [EC-377] Fixed browser settings.component.ts getting vault timeout * Updated people.component.ts to get ResetPassword policy through a subscription * [EC-377] Changed passwordGenerationService.getOptions to return observable * [EC-377] Fixed CLI generate.command.ts getting enforcePasswordGeneratorPoliciesOnOptions * [EC-377] Fixed eslint errors on rxjs * [EC-377] Reverted changes on passwordGeneration.service and vaultTimeout.service * [EC-377] Removed eslint disable on web/vault/add-edit-component * [EC-377] Changed AccountData.policies to TemporaryDataEncryption * [EC-377] Updated import.component to be reactive to policyAppliesToUser$ * [EC-377] Updated importBlockedByPolicy$ * [EC-377] Fixed missing rename * [EC-377] Updated policyService.masterPasswordPolicyOptions to return observable * [EC-377] Fixed vaultTimeout imports from merge * [EC-377] Reverted call to passwordGenerationService.getOptions * [EC-377] Reverted call to enforcePasswordGeneratorPoliciesOnOptions * [EC-377] Removed unneeded ngOnDestroy * Apply suggestions from code review Co-authored-by: Oscar Hinton * [EC-377] Fixed login.component.ts and register.component.ts * [EC-377] Updated PolicyService to update vaultTimeout * [EC-377] Updated PolicyService dependencies * [EC-377] Renamed policyAppliesToUser to policyAppliesToActiveUser * [EC-377] VaultTimeoutSettings service now gets the vault timeout directly instead of using observables * [EC-377] Fixed unit tests by removing unneeded vaultTimeoutSettingsService * [EC-377] Set getDecryptedPolicies and setDecryptedPolicies as deprecated * [EC-377] Set PolicyService.getAll as deprecated and updated to use prototype.hasOwnProperty * [EC-565] Reverted unintended change to vaultTimeoutSettings that was causing a bug to not display the correct vault timeout * [EC-377] Removed unneeded destroy$ from preferences.component.ts * [EC-377] Fixed policy.service.ts import of OrganizationService Co-authored-by: Oscar Hinton Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com> --- .../src/background/commands.background.ts | 2 +- .../src/background/contextMenus.background.ts | 2 +- .../src/background/notification.background.ts | 4 +- apps/cli/src/commands/export.command.ts | 5 +- .../src/app/accounts/login/login.component.ts | 21 +- .../src/app/accounts/register.component.ts | 21 +- .../trial-initiation.component.spec.ts | 22 +- .../trial-initiation.component.ts | 23 +- apps/web/src/app/core/event.service.ts | 23 +- .../organizations/manage/people.component.ts | 106 +++--- .../manage/reset-password.component.ts | 31 +- .../import-export/org-import.component.ts | 1 - .../emergency-access-takeover.component.ts | 22 +- .../settings/organization-plans.component.ts | 33 +- .../settings/two-factor-setup.component.ts | 26 +- .../tools/import-export/import.component.html | 12 +- .../tools/import-export/import.component.ts | 20 +- apps/web/src/app/vault/add-edit.component.ts | 10 +- .../organization-options.component.ts | 25 +- .../src/components/add-edit.component.ts | 26 +- .../components/change-password.component.ts | 20 +- .../src/components/export.component.ts | 10 +- .../src/components/send/add-edit.component.ts | 31 +- .../src/components/send/send.component.ts | 18 +- .../settings/vault-timeout-input.component.ts | 52 ++- .../update-temp-password.component.ts | 1 - .../services/vault-filter.service.ts | 8 +- .../spec/services/policy.service.spec.ts | 356 ++++++++++++++++++ .../policy/policy-api.service.abstraction.ts | 2 - .../policy/policy.service.abstraction.ts | 16 +- libs/common/src/abstractions/state.service.ts | 12 + .../services/passwordGeneration.service.ts | 7 +- .../src/services/policy/policy-api.service.ts | 22 +- .../src/services/policy/policy.service.ts | 191 ++++++---- libs/node/src/cli/commands/login.command.ts | 5 +- 35 files changed, 915 insertions(+), 271 deletions(-) create mode 100644 libs/common/spec/services/policy.service.spec.ts diff --git a/apps/browser/src/background/commands.background.ts b/apps/browser/src/background/commands.background.ts index 7303d790ec..2ad3eafe49 100644 --- a/apps/browser/src/background/commands.background.ts +++ b/apps/browser/src/background/commands.background.ts @@ -64,7 +64,7 @@ export default class CommandsBackground { } private async generatePasswordToClipboard() { - const options = (await this.passwordGenerationService.getOptions())[0]; + const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; const password = await this.passwordGenerationService.generatePassword(options); this.platformUtilsService.copyToClipboard(password, { window: window }); this.passwordGenerationService.addHistory(password); diff --git a/apps/browser/src/background/contextMenus.background.ts b/apps/browser/src/background/contextMenus.background.ts index 22d9d56bbf..8af66db1f0 100644 --- a/apps/browser/src/background/contextMenus.background.ts +++ b/apps/browser/src/background/contextMenus.background.ts @@ -66,7 +66,7 @@ export default class ContextMenusBackground { } private async generatePasswordToClipboard() { - const options = (await this.passwordGenerationService.getOptions())[0]; + const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; const password = await this.passwordGenerationService.generatePassword(options); this.platformUtilsService.copyToClipboard(password, { window: window }); this.passwordGenerationService.addHistory(password); diff --git a/apps/browser/src/background/notification.background.ts b/apps/browser/src/background/notification.background.ts index e011f3d697..5eaec27325 100644 --- a/apps/browser/src/background/notification.background.ts +++ b/apps/browser/src/background/notification.background.ts @@ -446,6 +446,8 @@ export default class NotificationBackground { } private async allowPersonalOwnership(): Promise { - return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)); + return !(await firstValueFrom( + this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership) + )); } } diff --git a/apps/cli/src/commands/export.command.ts b/apps/cli/src/commands/export.command.ts index 50446d7e02..4b36746309 100644 --- a/apps/cli/src/commands/export.command.ts +++ b/apps/cli/src/commands/export.command.ts @@ -1,5 +1,6 @@ import * as program from "commander"; import * as inquirer from "inquirer"; +import { firstValueFrom } from "rxjs"; import { ExportFormat, ExportService } from "@bitwarden/common/abstractions/export.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; @@ -15,7 +16,9 @@ export class ExportCommand { async run(options: program.OptionValues): Promise { if ( options.organizationid == null && - (await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport)) + (await firstValueFrom( + this.policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport) + )) ) { return Response.badRequest( "One or more organization policies prevents you from exporting your personal vault." diff --git a/apps/web/src/app/accounts/login/login.component.ts b/apps/web/src/app/accounts/login/login.component.ts index c27ba53636..02bc781ee1 100644 --- a/apps/web/src/app/accounts/login/login.component.ts +++ b/apps/web/src/app/accounts/login/login.component.ts @@ -1,6 +1,7 @@ -import { Component, NgZone } from "@angular/core"; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; @@ -29,13 +30,14 @@ import { RouterService, StateService } from "../../core"; selector: "app-login", templateUrl: "login.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class LoginComponent extends BaseLoginComponent { +export class LoginComponent extends BaseLoginComponent implements OnInit, OnDestroy { showResetPasswordAutoEnrollWarning = false; enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; policies: ListResponse; showPasswordless = false; + private destroy$ = new Subject(); + constructor( authService: AuthService, router: Router, @@ -128,12 +130,21 @@ export class LoginComponent extends BaseLoginComponent { this.showResetPasswordAutoEnrollWarning = resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled; - this.enforcedPasswordPolicyOptions = - await this.policyService.getMasterPasswordPolicyOptions(policyList); + this.policyService + .masterPasswordPolicyOptions$(policyList) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPasswordPolicyOptions) => { + this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions; + }); } } } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async goAfterLogIn() { const masterPassword = this.formGroup.value.masterPassword; diff --git a/apps/web/src/app/accounts/register.component.ts b/apps/web/src/app/accounts/register.component.ts index 4e700ca4d4..2d1eb6d5e5 100644 --- a/apps/web/src/app/accounts/register.component.ts +++ b/apps/web/src/app/accounts/register.component.ts @@ -1,6 +1,7 @@ -import { Component } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component"; @@ -27,14 +28,14 @@ import { RouterService } from "../core"; selector: "app-register", templateUrl: "register.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class RegisterComponent extends BaseRegisterComponent { +export class RegisterComponent extends BaseRegisterComponent implements OnInit, OnDestroy { email = ""; showCreateOrgMessage = false; layout = ""; enforcedPolicyOptions: MasterPasswordPolicyOptions; private policies: Policy[]; + private destroy$ = new Subject(); constructor( formValidationErrorService: FormValidationErrorsService, @@ -130,11 +131,19 @@ export class RegisterComponent extends BaseRegisterComponent { } if (this.policies != null) { - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions( - this.policies - ); + this.policyService + .masterPasswordPolicyOptions$(this.policies) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPasswordPolicyOptions) => { + this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; + }); } await super.ngOnInit(); } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.spec.ts b/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.spec.ts index fd3552ebaf..c69a1bf1f0 100644 --- a/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.spec.ts +++ b/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; // eslint-disable-next-line no-restricted-imports import { Substitute } from "@fluffy-spoon/substitute"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/pipes/i18n.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -47,7 +47,7 @@ describe("TrialInitiationComponent", () => { }; policyServiceMock = { - getMasterPasswordPolicyOptions: jest.fn(), + masterPasswordPolicyOptions$: jest.fn(), }; TestBed.configureTestingModule({ @@ -145,14 +145,16 @@ describe("TrialInitiationComponent", () => { }, ], }); - policyServiceMock.getMasterPasswordPolicyOptions.mockReturnValueOnce({ - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - } as MasterPasswordPolicyOptions); + policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue( + of({ + minComplexity: 4, + minLength: 10, + requireLower: null, + requireNumbers: null, + requireSpecial: null, + requireUpper: null, + } as MasterPasswordPolicyOptions) + ); // Need to recreate component with new service mocks fixture = TestBed.createComponent(TrialInitiationComponent); diff --git a/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.ts index 00bbc6e236..d8b2fc0873 100644 --- a/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/accounts/trial-initiation/trial-initiation.component.ts @@ -1,9 +1,9 @@ import { StepperSelectionEvent } from "@angular/cdk/stepper"; import { TitleCasePipe } from "@angular/common"; -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { UntypedFormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs"; +import { first, Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -24,8 +24,7 @@ import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.co selector: "app-trial", templateUrl: "trial-initiation.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TrialInitiationComponent implements OnInit { +export class TrialInitiationComponent implements OnInit, OnDestroy { email = ""; org = ""; orgInfoSubLabel = ""; @@ -63,6 +62,8 @@ export class TrialInitiationComponent implements OnInit { } } + private destroy$ = new Subject(); + constructor( private route: ActivatedRoute, protected router: Router, @@ -140,12 +141,20 @@ export class TrialInitiationComponent implements OnInit { } if (this.policies != null) { - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions( - this.policies - ); + this.policyService + .masterPasswordPolicyOptions$(this.policies) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPasswordPolicyOptions) => { + this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; + }); } } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + stepSelectionChange(event: StepperSelectionEvent) { // Set org info sub label if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") { diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 1a17998747..6f77079cd9 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -1,16 +1,32 @@ -import { Injectable } from "@angular/core"; +import { Injectable, OnDestroy, OnInit } from "@angular/core"; +import { Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums/deviceType"; import { EventType } from "@bitwarden/common/enums/eventType"; import { PolicyType } from "@bitwarden/common/enums/policyType"; +import { Policy } from "@bitwarden/common/models/domain/policy"; import { EventResponse } from "@bitwarden/common/models/response/eventResponse"; @Injectable() -export class EventService { +export class EventService implements OnInit, OnDestroy { + private destroy$ = new Subject(); + private policies: Policy[]; + constructor(private i18nService: I18nService, private policyService: PolicyService) {} + ngOnInit(): void { + this.policyService.policies$.pipe(takeUntil(this.destroy$)).subscribe((policies) => { + this.policies = policies; + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + getDefaultDateFilters() { const d = new Date(); const end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59); @@ -326,8 +342,7 @@ export class EventService { case EventType.Policy_Updated: { msg = this.i18nService.t("modifiedPolicyId", this.formatPolicyId(ev)); - const policies = await this.policyService.getAll(); - const policy = policies.filter((p) => p.id === ev.policyId)[0]; + const policy = this.policies.filter((p) => p.id === ev.policyId)[0]; let p1 = this.getShortId(ev.policyId); if (policy != null) { p1 = PolicyType[policy.type]; diff --git a/apps/web/src/app/organizations/manage/people.component.ts b/apps/web/src/app/organizations/manage/people.component.ts index 94e9493431..cf426ef7aa 100644 --- a/apps/web/src/app/organizations/manage/people.component.ts +++ b/apps/web/src/app/organizations/manage/people.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { combineLatest, concatMap, Subject, takeUntil } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -12,7 +12,6 @@ import { LogService } from "@bitwarden/common/abstractions/log.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; @@ -43,10 +42,9 @@ import { UserGroupsComponent } from "./user-groups.component"; selector: "app-org-people", templateUrl: "people.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil export class PeopleComponent extends BasePeopleComponent - implements OnInit + implements OnInit, OnDestroy { @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @@ -77,6 +75,8 @@ export class PeopleComponent orgResetPasswordPolicyEnabled = false; callingUserType: OrganizationUserType = null; + private destroy$ = new Subject(); + constructor( apiService: ApiService, private route: ActivatedRoute, @@ -84,10 +84,8 @@ export class PeopleComponent modalService: ModalService, platformUtilsService: PlatformUtilsService, cryptoService: CryptoService, - private router: Router, searchService: SearchService, validationService: ValidationService, - private policyApiService: PolicyApiServiceAbstraction, private policyService: PolicyService, logService: LogService, searchPipe: SearchPipe, @@ -113,53 +111,63 @@ export class PeopleComponent } async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.parent.params.subscribe(async (params) => { - this.organizationId = params.organizationId; - const organization = await this.organizationService.get(this.organizationId); - this.accessEvents = organization.useEvents; - this.accessGroups = organization.useGroups; - this.canResetPassword = organization.canManageUsersPassword; - this.orgUseResetPassword = organization.useResetPassword; - this.callingUserType = organization.type; - this.orgHasKeys = organization.hasPublicAndPrivateKeys; + combineLatest([this.route.params, this.route.queryParams, this.policyService.policies$]) + .pipe( + concatMap(async ([params, qParams, policies]) => { + this.organizationId = params.organizationId; + const organization = await this.organizationService.get(this.organizationId); + this.accessEvents = organization.useEvents; + this.accessGroups = organization.useGroups; + this.canResetPassword = organization.canManageUsersPassword; + this.orgUseResetPassword = organization.useResetPassword; + this.callingUserType = organization.type; + this.orgHasKeys = organization.hasPublicAndPrivateKeys; - // Backfill pub/priv key if necessary - if (this.canResetPassword && !this.orgHasKeys) { - const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); - const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); - const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); - const response = await this.organizationApiService.updateKeys(this.organizationId, request); - if (response != null) { - this.orgHasKeys = response.publicKey != null && response.privateKey != null; - await this.syncService.fullSync(true); // Replace oganizations with new data - } else { - throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); - } - } - - await this.load(); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - this.searchText = qParams.search; - if (qParams.viewEvents != null) { - const user = this.users.filter((u) => u.id === qParams.viewEvents); - if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) { - this.events(user[0]); + // Backfill pub/priv key if necessary + if (this.canResetPassword && !this.orgHasKeys) { + const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); + const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); + const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); + const response = await this.organizationApiService.updateKeys( + this.organizationId, + request + ); + if (response != null) { + this.orgHasKeys = response.publicKey != null && response.privateKey != null; + await this.syncService.fullSync(true); // Replace oganizations with new data + } else { + throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); + } } - } - }); - }); + + const resetPasswordPolicy = policies + .filter((policy) => policy.type === PolicyType.ResetPassword) + .find((p) => p.organizationId === this.organizationId); + this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; + + await this.load(); + + this.searchText = qParams.search; + if (qParams.viewEvents != null) { + const user = this.users.filter((u) => u.id === qParams.viewEvents); + if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) { + this.events(user[0]); + } + } + }), + takeUntil(this.destroy$) + ) + .subscribe(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } async load() { - const resetPasswordPolicy = await this.policyApiService.getPolicyForOrganization( - PolicyType.ResetPassword, - this.organizationId - ); - this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; super.load(); + await super.load(); } getUsers(): Promise> { diff --git a/apps/web/src/app/organizations/manage/reset-password.component.ts b/apps/web/src/app/organizations/manage/reset-password.component.ts index d37148a25e..97e9466e80 100644 --- a/apps/web/src/app/organizations/manage/reset-password.component.ts +++ b/apps/web/src/app/organizations/manage/reset-password.component.ts @@ -1,4 +1,13 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from "@angular/core"; +import { Subject, takeUntil } from "rxjs"; import zxcvbn from "zxcvbn"; import { PasswordStrengthComponent } from "@bitwarden/angular/shared/components/password-strength/password-strength.component"; @@ -18,7 +27,7 @@ import { OrganizationUserResetPasswordRequest } from "@bitwarden/common/models/r selector: "app-reset-password", templateUrl: "reset-password.component.html", }) -export class ResetPasswordComponent implements OnInit { +export class ResetPasswordComponent implements OnInit, OnDestroy { @Input() name: string; @Input() email: string; @Input() id: string; @@ -32,6 +41,8 @@ export class ResetPasswordComponent implements OnInit { passwordStrengthResult: zxcvbn.ZXCVBNResult; formPromise: Promise; + private destroy$ = new Subject(); + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -43,8 +54,18 @@ export class ResetPasswordComponent implements OnInit { ) {} async ngOnInit() { - // Get Enforced Policy Options - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + this.policyService + .masterPasswordPolicyOptions$() + .pipe(takeUntil(this.destroy$)) + .subscribe( + (enforcedPasswordPolicyOptions) => + (this.enforcedPolicyOptions = enforcedPasswordPolicyOptions) + ); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } get loggedOutWarningName() { @@ -52,7 +73,7 @@ export class ResetPasswordComponent implements OnInit { } async generatePassword() { - const options = (await this.passwordGenerationService.getOptions())[0]; + const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; this.newPassword = await this.passwordGenerationService.generatePassword(options); this.passwordStrengthComponent.updatePasswordStrength(this.newPassword); } diff --git a/apps/web/src/app/organizations/tools/import-export/org-import.component.ts b/apps/web/src/app/organizations/tools/import-export/org-import.component.ts index 69506b226a..81a16a2968 100644 --- a/apps/web/src/app/organizations/tools/import-export/org-import.component.ts +++ b/apps/web/src/app/organizations/tools/import-export/org-import.component.ts @@ -47,7 +47,6 @@ export class OrganizationImportComponent extends ImportComponent { this.organizationId = params.organizationId; this.successNavigate = ["organizations", this.organizationId, "vault"]; await super.ngOnInit(); - this.importBlockedByPolicy = false; }); const organization = await this.organizationService.get(this.organizationId); this.organizationName = organization.name; diff --git a/apps/web/src/app/settings/emergency-access-takeover.component.ts b/apps/web/src/app/settings/emergency-access-takeover.component.ts index 5731dc8b89..27a9484f4d 100644 --- a/apps/web/src/app/settings/emergency-access-takeover.component.ts +++ b/apps/web/src/app/settings/emergency-access-takeover.component.ts @@ -1,4 +1,5 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/components/change-password.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -21,7 +22,11 @@ import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse selector: "emergency-access-takeover", templateUrl: "emergency-access-takeover.component.html", }) -export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent implements OnInit { +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class EmergencyAccessTakeoverComponent + extends ChangePasswordComponent + implements OnInit, OnDestroy +{ @Output() onDone = new EventEmitter(); @Input() emergencyAccessId: string; @Input() name: string; @@ -59,12 +64,19 @@ export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent im const policies = response.data.map( (policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse)) ); - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions( - policies - ); + + this.policyService + .masterPasswordPolicyOptions$(policies) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions)); } } + // eslint-disable-next-line rxjs-angular/prefer-takeuntil + ngOnDestroy(): void { + super.ngOnDestroy(); + } + async submit() { if (!(await this.strongPassword())) { return; diff --git a/apps/web/src/app/settings/organization-plans.component.ts b/apps/web/src/app/settings/organization-plans.component.ts index 485afd12b1..934803b3fb 100644 --- a/apps/web/src/app/settings/organization-plans.component.ts +++ b/apps/web/src/app/settings/organization-plans.component.ts @@ -1,6 +1,15 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from "@angular/core"; import { UntypedFormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; @@ -35,7 +44,7 @@ interface OnSuccessArgs { selector: "app-organization-plans", templateUrl: "organization-plans.component.html", }) -export class OrganizationPlansComponent implements OnInit { +export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent; @@ -73,6 +82,8 @@ export class OrganizationPlansComponent implements OnInit { plans: PlanResponse[]; + private destroy$ = new Subject(); + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -114,9 +125,21 @@ export class OrganizationPlansComponent implements OnInit { this.formGroup.controls.billingEmail.addValidators(Validators.required); } + this.policyService + .policyAppliesToActiveUser$(PolicyType.SingleOrg) + .pipe(takeUntil(this.destroy$)) + .subscribe((policyAppliesToActiveUser) => { + this.singleOrgPolicyBlock = policyAppliesToActiveUser; + }); + this.loading = false; } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + get createOrganization() { return this.organizationId == null; } @@ -288,8 +311,6 @@ export class OrganizationPlansComponent implements OnInit { } async submit() { - this.singleOrgPolicyBlock = await this.userHasBlockingSingleOrgPolicy(); - if (this.singleOrgPolicyBlock) { return; } @@ -353,10 +374,6 @@ export class OrganizationPlansComponent implements OnInit { } } - private async userHasBlockingSingleOrgPolicy() { - return this.policyService.policyAppliesToUser(PolicyType.SingleOrg); - } - private async updateOrganization(orgId: string) { const request = new OrganizationUpgradeRequest(); request.businessName = this.formGroup.controls.businessOwned.value diff --git a/apps/web/src/app/settings/two-factor-setup.component.ts b/apps/web/src/app/settings/two-factor-setup.component.ts index 13e3cbecea..994262548b 100644 --- a/apps/web/src/app/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/settings/two-factor-setup.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; +import { Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -25,8 +26,7 @@ import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component"; selector: "app-two-factor-setup", templateUrl: "two-factor-setup.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorSetupComponent implements OnInit { +export class TwoFactorSetupComponent implements OnInit, OnDestroy { @ViewChild("recoveryTemplate", { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef; @ViewChild("authenticatorTemplate", { read: ViewContainerRef, static: true }) @@ -49,6 +49,9 @@ export class TwoFactorSetupComponent implements OnInit { modal: ModalRef; formPromise: Promise; + private destroy$ = new Subject(); + private twoFactorAuthPolicyAppliesToActiveUser: boolean; + constructor( protected apiService: ApiService, protected modalService: ModalService, @@ -93,9 +96,22 @@ export class TwoFactorSetupComponent implements OnInit { } this.providers.sort((a: any, b: any) => a.sort - b.sort); + + this.policyService + .policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication) + .pipe(takeUntil(this.destroy$)) + .subscribe((policyAppliesToActiveUser) => { + this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser; + }); + await this.load(); } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async load() { this.loading = true; const providerList = await this.getTwoFactorProviders(); @@ -203,9 +219,7 @@ export class TwoFactorSetupComponent implements OnInit { private async evaluatePolicies() { if (this.organizationId == null && this.providers.filter((p) => p.enabled).length === 1) { - this.showPolicyWarning = await this.policyService.policyAppliesToUser( - PolicyType.TwoFactorAuthentication - ); + this.showPolicyWarning = this.twoFactorAuthPolicyAppliesToActiveUser; } else { this.showPolicyWarning = false; } diff --git a/apps/web/src/app/tools/import-export/import.component.html b/apps/web/src/app/tools/import-export/import.component.html index 26d820b518..ba2eb69925 100644 --- a/apps/web/src/app/tools/import-export/import.component.html +++ b/apps/web/src/app/tools/import-export/import.component.html @@ -1,7 +1,7 @@ - + {{ "personalOwnershipPolicyInEffectImports" | i18n }}
@@ -14,7 +14,7 @@ name="Format" [(ngModel)]="format" class="form-control" - [disabled]="importBlockedByPolicy" + [disabled]="importBlockedByPolicy$ | async" required > @@ -296,7 +296,7 @@ id="file" class="form-control-file" name="file" - [disabled]="importBlockedByPolicy" + [disabled]="importBlockedByPolicy$ | async" /> @@ -308,14 +308,14 @@ class="form-control" name="FileContents" [(ngModel)]="fileContents" - [disabled]="importBlockedByPolicy" + [disabled]="importBlockedByPolicy$ | async" >