[AC-1782] Flexible collections import behavior in Password Manager and Admin Console (#6888)
* Added logic to only return organisations where user has import permissions and collections that he manages on Import * Changed the UnassignedCollections validation logic * Added validation to check if the user is coming from AdminConsole on the import component * Added import collection service abstraction to allow get admin collections * Corrected feature flag reads on import component * Refactor import component methods ngOnInit and performImport to improve codescene Using FeatureFlag Observable * Modified validation to allow import if user has organizations to import into * Using the new organization flexiblecollections property on import * Created collection-admin-import.service to return all the org collections to the import on Admin Console * Small changes on import flexible collections * Fix linting issues * changed canAccessImport rules and deprecated canAccessImportExport * Validating if user canAccessImportExport instead of admin before calling the handleOrganizationImportInit. * AC-2095 - Corrected getAllAdminCollections from ImportCollectionAdminService to properly get all the collections on AdminConsole * Reverting AC-2095 --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
parent
289a5cd002
commit
305fd39871
|
@ -49,8 +49,8 @@ const routes: Routes = [
|
|||
{
|
||||
path: "import",
|
||||
loadComponent: () =>
|
||||
import("../../../tools/import/import-web.component").then(
|
||||
(mod) => mod.ImportWebComponent,
|
||||
import("../../../tools/import/admin-import.component").then(
|
||||
(mod) => mod.AdminImportComponent,
|
||||
),
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { Component } from "@angular/core";
|
||||
|
||||
import { ImportCollectionServiceAbstraction } from "@bitwarden/importer/core";
|
||||
import { ImportComponent } from "@bitwarden/importer/ui";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
import { CollectionAdminService } from "../../vault/core/collection-admin.service";
|
||||
|
||||
import { ImportCollectionAdminService } from "./import-collection-admin.service";
|
||||
import { ImportWebComponent } from "./import-web.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "import-web.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, ImportComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ImportCollectionServiceAbstraction,
|
||||
useClass: ImportCollectionAdminService,
|
||||
deps: [CollectionAdminService],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AdminImportComponent extends ImportWebComponent {}
|
|
@ -0,0 +1,14 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction";
|
||||
import { CollectionAdminService } from "../../vault/core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../vault/core/views/collection-admin.view";
|
||||
|
||||
@Injectable()
|
||||
export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction {
|
||||
constructor(private collectionAdminService: CollectionAdminService) {}
|
||||
|
||||
async getAllAdminCollections(organizationId: string): Promise<CollectionAdminView[]> {
|
||||
return await this.collectionAdminService.getAll(organizationId);
|
||||
}
|
||||
}
|
|
@ -57,6 +57,10 @@ export function canAccessAdmin(i18nService: I18nService) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* To be removed after Flexible Collections.
|
||||
**/
|
||||
export function canAccessImportExport(i18nService: I18nService) {
|
||||
return map<Organization[], Organization[]>((orgs) =>
|
||||
orgs
|
||||
|
@ -65,6 +69,17 @@ export function canAccessImportExport(i18nService: I18nService) {
|
|||
);
|
||||
}
|
||||
|
||||
export function canAccessImport(i18nService: I18nService) {
|
||||
return map<Organization[], Organization[]>((orgs) =>
|
||||
orgs
|
||||
.filter(
|
||||
(org) =>
|
||||
org.canAccessImportExport || (org.canCreateNewCollections && org.flexibleCollections),
|
||||
)
|
||||
.sort(Utils.getSortFunction(i18nService, "name")),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if a user is a member of an organization (rather than only being a ProviderUser)
|
||||
* @deprecated Use organizationService.memberOrganizations$ instead
|
||||
|
|
|
@ -2,9 +2,11 @@ import { CommonModule } from "@angular/common";
|
|||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Optional,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
|
@ -16,7 +18,7 @@ import { filter, map, takeUntil } from "rxjs/operators";
|
|||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
canAccessImportExport,
|
||||
canAccessImport,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
|
@ -50,6 +52,7 @@ import { ImportOption, ImportResult, ImportType } from "../models";
|
|||
import {
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
ImportCollectionServiceAbstraction,
|
||||
ImportService,
|
||||
ImportServiceAbstraction,
|
||||
} from "../services";
|
||||
|
@ -129,6 +132,7 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
protected destroy$ = new Subject<void>();
|
||||
|
||||
private _importBlockedByPolicy = false;
|
||||
private _isFromAC = false;
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
vaultSelector: [
|
||||
|
@ -176,9 +180,12 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
protected syncService: SyncService,
|
||||
protected dialogService: DialogService,
|
||||
protected folderService: FolderService,
|
||||
protected collectionService: CollectionService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected collectionService: CollectionService,
|
||||
protected formBuilder: FormBuilder,
|
||||
@Inject(ImportCollectionServiceAbstraction)
|
||||
@Optional()
|
||||
protected importCollectionService: ImportCollectionServiceAbstraction,
|
||||
) {}
|
||||
|
||||
protected get importBlockedByPolicy(): boolean {
|
||||
|
@ -200,41 +207,12 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
this.setImportOptions();
|
||||
|
||||
await this.initializeOrganizations();
|
||||
|
||||
if (this.organizationId) {
|
||||
this.formGroup.controls.vaultSelector.patchValue(this.organizationId);
|
||||
this.formGroup.controls.vaultSelector.disable();
|
||||
|
||||
this.collections$ = Utils.asyncToObservable(() =>
|
||||
this.collectionService
|
||||
.getAllDecrypted()
|
||||
.then((c) => c.filter((c2) => c2.organizationId === this.organizationId)),
|
||||
);
|
||||
if (this.organizationId && this.canAccessImportExport(this.organizationId)) {
|
||||
this.handleOrganizationImportInit();
|
||||
} else {
|
||||
// Filter out the `no folder`-item from folderViews$
|
||||
this.folders$ = this.folderService.folderViews$.pipe(
|
||||
map((folders) => folders.filter((f) => f.id != null)),
|
||||
);
|
||||
this.formGroup.controls.targetSelector.disable();
|
||||
this.handleImportInit();
|
||||
}
|
||||
|
||||
this.formGroup.controls.vaultSelector.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value) => {
|
||||
this.organizationId = value != "myVault" ? value : undefined;
|
||||
if (!this._importBlockedByPolicy) {
|
||||
this.formGroup.controls.targetSelector.enable();
|
||||
}
|
||||
if (value) {
|
||||
this.collections$ = Utils.asyncToObservable(() =>
|
||||
this.collectionService
|
||||
.getAllDecrypted()
|
||||
.then((c) => c.filter((c2) => c2.organizationId === value)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.controls.vaultSelector.setValue("myVault");
|
||||
}
|
||||
this.formGroup.controls.format.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value) => {
|
||||
|
@ -244,10 +222,58 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
await this.handlePolicies();
|
||||
}
|
||||
|
||||
private handleOrganizationImportInit() {
|
||||
this.formGroup.controls.vaultSelector.patchValue(this.organizationId);
|
||||
this.formGroup.controls.vaultSelector.disable();
|
||||
|
||||
this.collections$ = Utils.asyncToObservable(() =>
|
||||
this.importCollectionService
|
||||
.getAllAdminCollections(this.organizationId)
|
||||
.then((collections) => collections.sort(Utils.getSortFunction(this.i18nService, "name"))),
|
||||
);
|
||||
|
||||
this._isFromAC = true;
|
||||
}
|
||||
|
||||
private handleImportInit() {
|
||||
// Filter out the no folder-item from folderViews$
|
||||
this.folders$ = this.folderService.folderViews$.pipe(
|
||||
map((folders) => folders.filter((f) => f.id != null)),
|
||||
);
|
||||
|
||||
this.formGroup.controls.targetSelector.disable();
|
||||
|
||||
combineLatest([this.formGroup.controls.vaultSelector.valueChanges, this.organizations$])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([value, organizations]) => {
|
||||
this.organizationId = value !== "myVault" ? value : undefined;
|
||||
|
||||
if (!this._importBlockedByPolicy) {
|
||||
this.formGroup.controls.targetSelector.enable();
|
||||
}
|
||||
const flexCollectionEnabled =
|
||||
organizations.find((x) => x.id == this.organizationId)?.flexibleCollections ?? false;
|
||||
if (value) {
|
||||
this.collections$ = Utils.asyncToObservable(() =>
|
||||
this.collectionService
|
||||
.getAllDecrypted()
|
||||
.then((decryptedCollections) =>
|
||||
decryptedCollections
|
||||
.filter(
|
||||
(c2) => c2.organizationId === value && (!flexCollectionEnabled || c2.manage),
|
||||
)
|
||||
.sort(Utils.getSortFunction(this.i18nService, "name")),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
this.formGroup.controls.vaultSelector.setValue("myVault");
|
||||
}
|
||||
|
||||
private async initializeOrganizations() {
|
||||
this.organizations$ = concat(
|
||||
this.organizationService.memberOrganizations$.pipe(
|
||||
canAccessImportExport(this.i18nService),
|
||||
canAccessImport(this.i18nService),
|
||||
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))),
|
||||
),
|
||||
);
|
||||
|
@ -293,24 +319,7 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
protected async performImport() {
|
||||
if (this.organization) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "warning" },
|
||||
content: { key: "importWarning", placeholders: [this.organization.name] },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.importBlockedByPolicy && this.organizationId == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("personalOwnershipPolicyInEffectImports"),
|
||||
);
|
||||
if (!(await this.validateImport())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -333,10 +342,9 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const fileEl = document.getElementById("import_input_file") as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
let fileContents = this.formGroup.controls.fileContents.value;
|
||||
if ((files == null || files.length === 0) && (fileContents == null || fileContents === "")) {
|
||||
const importContents = await this.setImportContents();
|
||||
|
||||
if (importContents == null || importContents === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
|
@ -345,37 +353,13 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
try {
|
||||
const content = await this.getFileContents(files[0]);
|
||||
if (content != null) {
|
||||
fileContents = content;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileContents == null || fileContents === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("selectFile"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.organizationId) {
|
||||
await this.organizationService.get(this.organizationId)?.isAdmin;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.importService.import(
|
||||
importer,
|
||||
fileContents,
|
||||
importContents,
|
||||
this.organizationId,
|
||||
this.formGroup.controls.targetSelector.value,
|
||||
this.canAccessImportExport(this.organizationId),
|
||||
this.canAccessImportExport(this.organizationId) && this._isFromAC,
|
||||
);
|
||||
|
||||
//No errors, display success message
|
||||
|
@ -393,13 +377,6 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private isUserAdmin(organizationId?: string): boolean {
|
||||
if (!organizationId) {
|
||||
return false;
|
||||
}
|
||||
return this.organizationService.get(this.organizationId)?.isAdmin;
|
||||
}
|
||||
|
||||
private canAccessImportExport(organizationId?: string): boolean {
|
||||
if (!organizationId) {
|
||||
return false;
|
||||
|
@ -507,6 +484,58 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||
return await lastValueFrom(dialog.closed);
|
||||
}
|
||||
|
||||
private async validateImport(): Promise<boolean> {
|
||||
if (this.organization) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "warning" },
|
||||
content: { key: "importWarning", placeholders: [this.organization.name] },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.importBlockedByPolicy && this.organizationId == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("personalOwnershipPolicyInEffectImports"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async setImportContents(): Promise<string> {
|
||||
const fileEl = document.getElementById("import_input_file") as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
let fileContents = this.formGroup.controls.fileContents.value;
|
||||
if ((files == null || files.length === 0) && (fileContents == null || fileContents === "")) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("selectFile"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
try {
|
||||
const content = await this.getFileContents(files[0]);
|
||||
if (content != null) {
|
||||
fileContents = content;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return fileContents;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
export abstract class ImportCollectionServiceAbstraction {
|
||||
getAllAdminCollections: (organizationId: string) => Promise<CollectionView[]>;
|
||||
}
|
|
@ -152,9 +152,8 @@ export class ImportService implements ImportServiceAbstraction {
|
|||
Utils.isNullOrWhitespace(selectedImportTarget) &&
|
||||
!canAccessImportExport
|
||||
) {
|
||||
const hasUnassignedCollections = importResult.ciphers.some(
|
||||
(c) => !Array.isArray(c.collectionIds) || c.collectionIds.length == 0,
|
||||
);
|
||||
const hasUnassignedCollections =
|
||||
importResult.collectionRelationships.length < importResult.ciphers.length;
|
||||
if (hasUnassignedCollections) {
|
||||
throw new Error(this.i18nService.t("importUnassignedItemsError"));
|
||||
}
|
||||
|
|
|
@ -3,3 +3,5 @@ export { ImportApiService } from "./import-api.service";
|
|||
|
||||
export { ImportServiceAbstraction } from "./import.service.abstraction";
|
||||
export { ImportService } from "./import.service";
|
||||
|
||||
export { ImportCollectionServiceAbstraction } from "./import-collection.service.abstraction";
|
||||
|
|
Loading…
Reference in New Issue