From e092d42b725ef8f9690f03631391d900cba6ef2d Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 30 May 2023 16:30:15 -0700 Subject: [PATCH] [AC-1104] Fix access import/export with custom permission (#5014) * [AC-1104] Allow importBlockedByPolicy to be overridden Adjust the import component so that the importBlockedByPolicy flag can be overridden by the org import component to always return false. * [AC-1104] Allow disabledByPolicy to be overridden in export component Adjust the export component so that the disabledByPolicy flag can be overridden by the org export component to always return false. * [AC-1104] Cleanup logic that disables export form * [AC-1104] Use observable subscription for assigning importBlockedByPolicy flag * [AC-1264] Add optional success callback for import component Use the optional callback in org-import.component.ts to clear the file and file contents when the user does not have access to the vault page * [AC-1264] Re-order properties * [AC-1104] Refactor import component to only use onSuccess callback that can be overridden --- .../import-export/org-export.component.ts | 8 ++-- .../import-export/org-import.component.ts | 44 +++++++++++++----- .../tools/import-export/import.component.ts | 45 ++++++++++++++----- .../export/components/export.component.ts | 21 +++++---- 4 files changed, 79 insertions(+), 39 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/tools/import-export/org-export.component.ts b/apps/web/src/app/admin-console/organizations/tools/import-export/org-export.component.ts index 6260511811..d8c657cdab 100644 --- a/apps/web/src/app/admin-console/organizations/tools/import-export/org-export.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/import-export/org-export.component.ts @@ -54,6 +54,10 @@ export class OrganizationExportComponent extends ExportComponent { ); } + protected get disabledByPolicy(): boolean { + return false; + } + async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { @@ -62,10 +66,6 @@ export class OrganizationExportComponent extends ExportComponent { await super.ngOnInit(); } - async checkExportDisabled() { - return; - } - getExportData() { if (this.isFileEncryptedExport) { return this.exportService.getPasswordProtectedExport(this.filePassword, this.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/import-export/org-import.component.ts b/apps/web/src/app/admin-console/organizations/tools/import-export/org-import.component.ts index e475be97ae..355426b06c 100644 --- a/apps/web/src/app/admin-console/organizations/tools/import-export/org-import.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/import-export/org-import.component.ts @@ -1,13 +1,18 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { switchMap, takeUntil } from "rxjs/operators"; import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + canAccessVaultTab, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ImportServiceAbstraction } from "@bitwarden/importer"; @@ -19,7 +24,11 @@ import { ImportComponent } from "../../../../tools/import-export/import.componen }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class OrganizationImportComponent extends ImportComponent { - organizationName: string; + organization: Organization; + + protected get importBlockedByPolicy(): boolean { + return false; + } constructor( i18nService: I18nService, @@ -47,21 +56,32 @@ export class OrganizationImportComponent extends ImportComponent { ); } - 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; - this.successNavigate = ["organizations", this.organizationId, "vault"]; - await super.ngOnInit(); - }); - const organization = await this.organizationService.get(this.organizationId); - this.organizationName = organization.name; + ngOnInit() { + this.route.params + .pipe( + switchMap((params) => this.organizationService.get$(params.organizationId)), + takeUntil(this.destroy$) + ) + .subscribe((organization) => { + this.organizationId = organization.id; + this.organization = organization; + }); + super.ngOnInit(); + } + + protected async onSuccessfulImport(): Promise { + if (canAccessVaultTab(this.organization)) { + await this.router.navigate(["organizations", this.organizationId, "vault"]); + } else { + this.fileSelected = null; + this.fileContents = ""; + } } async submit() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, - content: { key: "importWarning", placeholders: [this.organizationName] }, + content: { key: "importWarning", placeholders: [this.organization.name] }, type: SimpleDialogType.WARNING, }); diff --git a/apps/web/src/app/tools/import-export/import.component.ts b/apps/web/src/app/tools/import-export/import.component.ts index 89104e2e33..f0f530ba0e 100644 --- a/apps/web/src/app/tools/import-export/import.component.ts +++ b/apps/web/src/app/tools/import-export/import.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import * as JSZip from "jszip"; -import { firstValueFrom } from "rxjs"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; import Swal, { SweetAlertIcon } from "sweetalert2"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; @@ -14,28 +15,29 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ImportOption, - ImportType, ImportResult, ImportServiceAbstraction, + ImportType, } from "@bitwarden/importer"; -import { ImportSuccessDialogComponent, FilePasswordPromptComponent } from "./dialog"; +import { FilePasswordPromptComponent, ImportSuccessDialogComponent } from "./dialog"; @Component({ selector: "app-import", templateUrl: "import.component.html", }) -export class ImportComponent implements OnInit { +export class ImportComponent implements OnInit, OnDestroy { featuredImportOptions: ImportOption[]; importOptions: ImportOption[]; format: ImportType = null; fileContents: string; fileSelected: File; loading = false; - importBlockedByPolicy = false; protected organizationId: string = null; - protected successNavigate: any[] = ["vault"]; + protected destroy$ = new Subject(); + + private _importBlockedByPolicy = false; constructor( protected i18nService: I18nService, @@ -49,12 +51,26 @@ export class ImportComponent implements OnInit { protected dialogService: DialogServiceAbstraction ) {} - async ngOnInit() { + protected get importBlockedByPolicy(): boolean { + return this._importBlockedByPolicy; + } + + /** + * Callback that is called after a successful import. + */ + protected async onSuccessfulImport(): Promise { + await this.router.navigate(["vault"]); + } + + ngOnInit() { this.setImportOptions(); - this.importBlockedByPolicy = await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership) - ); + this.policyService + .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) + .pipe(takeUntil(this.destroy$)) + .subscribe((policyAppliesToActiveUser) => { + this._importBlockedByPolicy = policyAppliesToActiveUser; + }); } async submit() { @@ -134,7 +150,7 @@ export class ImportComponent implements OnInit { }); this.syncService.fullSync(true); - this.router.navigate(this.successNavigate); + await this.onSuccessfulImport(); } catch (e) { this.error(e); this.logService.error(e); @@ -264,4 +280,9 @@ export class ImportComponent implements OnInit { return await ref.onClosedPromise(); } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/libs/angular/src/tools/export/components/export.component.ts b/libs/angular/src/tools/export/components/export.component.ts index a1a8b583b2..a9f06922a3 100644 --- a/libs/angular/src/tools/export/components/export.component.ts +++ b/libs/angular/src/tools/export/components/export.component.ts @@ -1,6 +1,6 @@ import { Directive, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { merge, takeUntil, Subject, startWith } from "rxjs"; +import { merge, startWith, Subject, takeUntil } from "rxjs"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -21,7 +21,11 @@ export class ExportComponent implements OnInit, OnDestroy { @Output() onSaved = new EventEmitter(); formPromise: Promise; - disabledByPolicy = false; + private _disabledByPolicy = false; + + protected get disabledByPolicy(): boolean { + return this._disabledByPolicy; + } exportForm = this.formBuilder.group({ format: ["json"], @@ -59,11 +63,12 @@ export class ExportComponent implements OnInit, OnDestroy { .policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport) .pipe(takeUntil(this.destroy$)) .subscribe((policyAppliesToActiveUser) => { - this.disabledByPolicy = policyAppliesToActiveUser; + this._disabledByPolicy = policyAppliesToActiveUser; + if (this.disabledByPolicy) { + this.exportForm.disable(); + } }); - await this.checkExportDisabled(); - merge( this.exportForm.get("format").valueChanges, this.exportForm.get("fileEncryptionType").valueChanges @@ -77,12 +82,6 @@ export class ExportComponent implements OnInit, OnDestroy { this.destroy$.next(); } - async checkExportDisabled() { - if (this.disabledByPolicy) { - this.exportForm.disable(); - } - } - get encryptedFormat() { return this.format === "encrypted_json"; }