mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 10:54:00 +00:00
Desktop/pm 18769/migrate vault filters (#17919)
Migrated vault filters to new v3 vault's navigation * Decoupled existing vault filtering from vault component by using routed params with routed-vault-filter-bridge * Converted vault filters to standalone components * Removed extending filter Base Components from deprecated /libs/angular library and handled logic directly * Moved shared 'models' and 'services' directories from web-vault into /libs/vault
This commit is contained in:
committed by
jaasen-livefront
parent
7088447046
commit
acad754540
@@ -1,12 +1,11 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CreateCollectionRequest, UpdateCollectionRequest } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
CreateCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
|
||||
interface SelectionResponseLike {
|
||||
id: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
manage: boolean;
|
||||
}
|
||||
|
||||
export class CollectionAccessSelectionView extends View {
|
||||
readonly id: string;
|
||||
readonly readOnly: boolean;
|
||||
readonly hidePasswords: boolean;
|
||||
readonly manage: boolean;
|
||||
|
||||
constructor(response?: SelectionResponseLike) {
|
||||
super();
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = response.id;
|
||||
this.readOnly = response.readOnly;
|
||||
this.hidePasswords = response.hidePasswords;
|
||||
this.manage = response.manage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { CollectionAccessDetailsResponse, CollectionResponse } from "./collection.response";
|
||||
import { CollectionView } from "./collection.view";
|
||||
|
||||
// TODO: this is used to represent the pseudo "Unassigned" collection as well as
|
||||
// the user's personal vault (as a pseudo organization). This should be separated out into different values.
|
||||
export const Unassigned = "unassigned";
|
||||
export type Unassigned = typeof Unassigned;
|
||||
|
||||
export class CollectionAdminView extends CollectionView {
|
||||
groups: CollectionAccessSelectionView[] = [];
|
||||
users: CollectionAccessSelectionView[] = [];
|
||||
|
||||
/**
|
||||
* Flag indicating the collection has no active user or group assigned to it with CanManage permissions
|
||||
* In this case, the collection can be managed by admins/owners or custom users with appropriate permissions
|
||||
*/
|
||||
unmanaged: boolean = false;
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean = false;
|
||||
|
||||
/**
|
||||
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
|
||||
*/
|
||||
override canEdit(org: Organization): boolean {
|
||||
if (this.isDefaultCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
org?.canEditAnyCollection ||
|
||||
(this.unmanaged && org?.canEditUnmanagedCollections) ||
|
||||
super.canEdit(org)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can delete a collection from the Admin Console.
|
||||
*/
|
||||
override canDelete(org: Organization): boolean {
|
||||
if (this.isDefaultCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return org?.canDeleteAnyCollection || super.canDelete(org);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify user access to this collection
|
||||
*/
|
||||
canEditUserAccess(org: Organization): boolean {
|
||||
if (this.isDefaultCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(org.permissions.manageUsers && org.allowAdminAccessToAllCollectionItems) || this.canEdit(org)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify group access to this collection
|
||||
*/
|
||||
canEditGroupAccess(org: Organization): boolean {
|
||||
if (this.isDefaultCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(org.permissions.manageGroups && org.allowAdminAccessToAllCollectionItems) ||
|
||||
this.canEdit(org)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
|
||||
*/
|
||||
override canViewCollectionInfo(org: Organization | undefined): boolean {
|
||||
if (this.isUnassignedCollection || this.isDefaultCollection) {
|
||||
return false;
|
||||
}
|
||||
const isAdmin = org?.isAdmin ?? false;
|
||||
const permissions = org?.permissions.editAnyCollection ?? false;
|
||||
|
||||
return this.manage || isAdmin || permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this collection represents the pseudo "Unassigned" collection
|
||||
* This is different from the "unmanaged" flag, which indicates that no users or groups have access to the collection
|
||||
*/
|
||||
get isUnassignedCollection() {
|
||||
return this.id === Unassigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the collection name can be edited. Editing the collection name is restricted for collections
|
||||
* that were DefaultUserCollections but where the relevant user has been offboarded.
|
||||
* When this occurs, the offboarded user's email is treated as the collection name, and cannot be edited.
|
||||
* This is important for security so that the server cannot ask the client to encrypt arbitrary data.
|
||||
* WARNING! This is an IMPORTANT restriction that MUST be maintained for security purposes.
|
||||
* Do not edit or remove this unless you understand why.
|
||||
*/
|
||||
override canEditName(org: Organization): boolean {
|
||||
return (this.canEdit(org) && !this.defaultUserCollectionEmail) || super.canEditName(org);
|
||||
}
|
||||
static async fromCollectionAccessDetails(
|
||||
collection: CollectionAccessDetailsResponse,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<CollectionAdminView> {
|
||||
const view = new CollectionAdminView({ ...collection });
|
||||
try {
|
||||
view.name = await encryptService.decryptString(new EncString(view.name), orgKey);
|
||||
} catch (e) {
|
||||
// Note: This should be replaced by the owning team with appropriate, domain-specific behavior.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"[CollectionAdminView/fromCollectionAccessDetails] Error decrypting collection name",
|
||||
e,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
view.assigned = collection.assigned;
|
||||
view.readOnly = collection.readOnly;
|
||||
view.hidePasswords = collection.hidePasswords;
|
||||
view.manage = collection.manage;
|
||||
view.unmanaged = collection.unmanaged;
|
||||
view.type = collection.type;
|
||||
view.externalId = collection.externalId;
|
||||
view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail;
|
||||
|
||||
view.groups = collection.groups
|
||||
? collection.groups.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
view.users = collection.users
|
||||
? collection.users.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
static async fromCollectionResponse(
|
||||
collection: CollectionResponse,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<CollectionAdminView> {
|
||||
let collectionName: string;
|
||||
try {
|
||||
collectionName = await encryptService.decryptString(new EncString(collection.name), orgKey);
|
||||
} catch (e) {
|
||||
// Note: This should be updated by the owning team with appropriate, domain specific behavior
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"[CollectionAdminView/fromCollectionResponse] Failed to decrypt the collection name",
|
||||
e,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const collectionAdminView = new CollectionAdminView({
|
||||
id: collection.id,
|
||||
name: collectionName,
|
||||
organizationId: collection.organizationId,
|
||||
});
|
||||
|
||||
collectionAdminView.externalId = collection.externalId;
|
||||
|
||||
return collectionAdminView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export class CollectionData {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: string;
|
||||
defaultUserCollectionEmail: string | undefined;
|
||||
externalId: string | undefined;
|
||||
readOnly: boolean = false;
|
||||
manage: boolean = false;
|
||||
hidePasswords: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(response: CollectionDetailsResponse) {
|
||||
this.id = response.id;
|
||||
this.organizationId = response.organizationId;
|
||||
this.name = response.name;
|
||||
this.externalId = response.externalId;
|
||||
this.readOnly = response.readOnly;
|
||||
this.manage = response.manage;
|
||||
this.hidePasswords = response.hidePasswords;
|
||||
this.type = response.type;
|
||||
this.defaultUserCollectionEmail = response.defaultUserCollectionEmail;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CollectionData | null>): CollectionData | null {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return Object.assign(new CollectionData(new CollectionDetailsResponse({})), obj);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { SelectionReadOnlyResponse } from "@bitwarden/common/admin-console/models/response/selection-read-only.response";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export class CollectionResponse extends BaseResponse {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: string;
|
||||
defaultUserCollectionEmail: string | undefined;
|
||||
externalId: string | undefined;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.externalId = this.getResponseProperty("ExternalId");
|
||||
this.defaultUserCollectionEmail = this.getResponseProperty("DefaultUserCollectionEmail");
|
||||
this.type = this.getResponseProperty("Type") ?? CollectionTypes.SharedCollection;
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionDetailsResponse extends CollectionResponse {
|
||||
readOnly: boolean;
|
||||
manage: boolean;
|
||||
hidePasswords: boolean;
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.readOnly = this.getResponseProperty("ReadOnly") || false;
|
||||
this.manage = this.getResponseProperty("Manage") || false;
|
||||
this.hidePasswords = this.getResponseProperty("HidePasswords") || false;
|
||||
|
||||
// Temporary until the API is updated to return this property in AC-2084
|
||||
// For now, we can assume that if the object is 'collectionDetails' then the user is assigned
|
||||
this.assigned = this.getResponseProperty("object") == "collectionDetails";
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionAccessDetailsResponse extends CollectionDetailsResponse {
|
||||
groups: SelectionReadOnlyResponse[] = [];
|
||||
users: SelectionReadOnlyResponse[] = [];
|
||||
unmanaged: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.assigned = this.getResponseProperty("Assigned") || false;
|
||||
this.unmanaged = this.getResponseProperty("Unmanaged") || false;
|
||||
|
||||
const groups = this.getResponseProperty("Groups");
|
||||
if (groups != null) {
|
||||
this.groups = groups.map((g: any) => new SelectionReadOnlyResponse(g));
|
||||
}
|
||||
|
||||
const users = this.getResponseProperty("Users");
|
||||
if (users != null) {
|
||||
this.users = users.map((g: any) => new SelectionReadOnlyResponse(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import Domain from "@bitwarden/common/platform/models/domain/domain-base";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { CollectionData } from "./collection.data";
|
||||
|
||||
export const CollectionTypes = {
|
||||
SharedCollection: 0,
|
||||
DefaultUserCollection: 1,
|
||||
} as const;
|
||||
|
||||
export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes];
|
||||
|
||||
export class Collection extends Domain {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: EncString;
|
||||
externalId: string | undefined;
|
||||
readOnly: boolean = false;
|
||||
hidePasswords: boolean = false;
|
||||
manage: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
defaultUserCollectionEmail: string | undefined;
|
||||
|
||||
constructor(c: { id: CollectionId; name: EncString; organizationId: OrganizationId }) {
|
||||
super();
|
||||
this.id = c.id;
|
||||
this.name = c.name;
|
||||
this.organizationId = c.organizationId;
|
||||
}
|
||||
|
||||
static fromCollectionData(obj: CollectionData): Collection {
|
||||
if (obj == null || obj.name == null || obj.organizationId == null) {
|
||||
throw new Error("CollectionData must contain name and organizationId.");
|
||||
}
|
||||
|
||||
const collection = new Collection({
|
||||
...obj,
|
||||
name: new EncString(obj.name),
|
||||
});
|
||||
|
||||
collection.externalId = obj.externalId;
|
||||
collection.readOnly = obj.readOnly;
|
||||
collection.hidePasswords = obj.hidePasswords;
|
||||
collection.manage = obj.manage;
|
||||
collection.type = obj.type;
|
||||
collection.defaultUserCollectionEmail = obj.defaultUserCollectionEmail;
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
static async fromCollectionView(
|
||||
view: CollectionView,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<Collection> {
|
||||
const collection = new Collection({
|
||||
name: await encryptService.encryptString(view.name, orgKey),
|
||||
id: view.id,
|
||||
organizationId: view.organizationId,
|
||||
});
|
||||
|
||||
collection.externalId = view.externalId;
|
||||
collection.readOnly = view.readOnly;
|
||||
collection.hidePasswords = view.hidePasswords;
|
||||
collection.manage = view.manage;
|
||||
collection.type = view.type;
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
decrypt(orgKey: OrgKey, encryptService: EncryptService): Promise<CollectionView> {
|
||||
return CollectionView.fromCollection(this, encryptService, orgKey);
|
||||
}
|
||||
|
||||
// @TODO: This would be better off in Collection.Utils. Move this there when
|
||||
// refactoring to a shared lib.
|
||||
static isCollectionId(id: any): id is CollectionId {
|
||||
return typeof id === "string" && id != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { Collection, CollectionType, CollectionTypes } from "./collection";
|
||||
import { CollectionAccessDetailsResponse } from "./collection.response";
|
||||
|
||||
export const NestingDelimiter = "/";
|
||||
|
||||
export class CollectionView implements View, ITreeNodeObject {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
externalId: string | undefined;
|
||||
// readOnly applies to the items within a collection
|
||||
readOnly: boolean = false;
|
||||
hidePasswords: boolean = false;
|
||||
manage: boolean = false;
|
||||
assigned: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
defaultUserCollectionEmail: string | undefined;
|
||||
|
||||
private _name: string;
|
||||
|
||||
constructor(c: { id: CollectionId; organizationId: OrganizationId; name: string }) {
|
||||
this.id = c.id;
|
||||
this.organizationId = c.organizationId;
|
||||
this._name = c.name;
|
||||
}
|
||||
|
||||
set name(name: string) {
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.defaultUserCollectionEmail ?? this._name;
|
||||
}
|
||||
|
||||
canEditItems(org: Organization): boolean {
|
||||
if (org != null && org.id !== this.organizationId) {
|
||||
throw new Error(
|
||||
"Id of the organization provided does not match the org id of the collection.",
|
||||
);
|
||||
}
|
||||
|
||||
return org?.canEditAllCiphers || this.manage || (this.assigned && !this.readOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can edit a collection (including user and group access) from the individual vault.
|
||||
* Does not include admin permissions - see {@link CollectionAdminView.canEdit}.
|
||||
*/
|
||||
canEdit(org: Organization | undefined): boolean {
|
||||
if (this.isDefaultCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (org != null && org.id !== this.organizationId) {
|
||||
throw new Error(
|
||||
"Id of the organization provided does not match the org id of the collection.",
|
||||
);
|
||||
}
|
||||
|
||||
return this.manage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can delete a collection from the individual vault.
|
||||
* Does not include admin permissions - see {@link CollectionAdminView.canDelete}.
|
||||
*/
|
||||
canDelete(org: Organization | undefined): boolean {
|
||||
if (org != null && org.id !== this.organizationId) {
|
||||
throw new Error(
|
||||
"Id of the organization provided does not match the org id of the collection.",
|
||||
);
|
||||
}
|
||||
|
||||
const canDeleteManagedCollections = !org?.limitCollectionDeletion || org.isAdmin;
|
||||
|
||||
// Only use individual permissions, not admin permissions
|
||||
return canDeleteManagedCollections && this.manage && !this.isDefaultCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can view collection info and access in a read-only state from the individual vault
|
||||
*/
|
||||
canViewCollectionInfo(org: Organization | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the collection name can be edited. Editing the collection name is restricted for collections
|
||||
* that were DefaultUserCollections but where the relevant user has been offboarded.
|
||||
* When this occurs, the offboarded user's email is treated as the collection name, and cannot be edited.
|
||||
* This is important for security so that the server cannot ask the client to encrypt arbitrary data.
|
||||
* WARNING! This is an IMPORTANT restriction that MUST be maintained for security purposes.
|
||||
* Do not edit or remove this unless you understand why.
|
||||
*/
|
||||
canEditName(org: Organization): boolean {
|
||||
return this.canEdit(org) && !this.defaultUserCollectionEmail;
|
||||
}
|
||||
|
||||
get isDefaultCollection() {
|
||||
return this.type == CollectionTypes.DefaultUserCollection;
|
||||
}
|
||||
|
||||
// FIXME: we should not use a CollectionView object for the vault filter header because it is not a real
|
||||
// CollectionView and this violates ts-strict rules.
|
||||
static vaultFilterHead(): CollectionView {
|
||||
return new CollectionView({
|
||||
id: "" as CollectionId,
|
||||
organizationId: "" as OrganizationId,
|
||||
name: "",
|
||||
});
|
||||
}
|
||||
|
||||
static async fromCollection(
|
||||
collection: Collection,
|
||||
encryptService: EncryptService,
|
||||
key: OrgKey,
|
||||
): Promise<CollectionView> {
|
||||
const view = new CollectionView({ ...collection, name: "" });
|
||||
|
||||
view.name = await encryptService.decryptString(collection.name, key);
|
||||
view.assigned = true;
|
||||
view.externalId = collection.externalId;
|
||||
view.readOnly = collection.readOnly;
|
||||
view.hidePasswords = collection.hidePasswords;
|
||||
view.manage = collection.manage;
|
||||
view.type = collection.type;
|
||||
view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail;
|
||||
return view;
|
||||
}
|
||||
|
||||
static async fromCollectionAccessDetails(
|
||||
collection: CollectionAccessDetailsResponse,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<CollectionView> {
|
||||
const view = new CollectionView({ ...collection });
|
||||
|
||||
try {
|
||||
view.name = await encryptService.decryptString(new EncString(collection.name), orgKey);
|
||||
} catch (e) {
|
||||
// Note: This should be replaced by the owning team with appropriate, domain-specific behavior.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[CollectionView] Error decrypting collection name", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
view.externalId = collection.externalId;
|
||||
view.type = collection.type;
|
||||
view.assigned = collection.assigned;
|
||||
view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail;
|
||||
return view;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CollectionView>) {
|
||||
return Object.assign(new CollectionView({ ...obj }), obj);
|
||||
}
|
||||
|
||||
encrypt(orgKey: OrgKey, encryptService: EncryptService): Promise<Collection> {
|
||||
return Collection.fromCollectionView(this, encryptService, orgKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./collection-admin.view";
|
||||
export * from "./collection.view";
|
||||
export * from "./collection.response";
|
||||
export * from "./collection";
|
||||
export * from "./collection.data";
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionResponse } from "@bitwarden/admin-console/common";
|
||||
import { CollectionResponse } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
||||
|
||||
120
libs/common/src/admin-console/utils/collection-utils.spec.ts
Normal file
120
libs/common/src/admin-console/utils/collection-utils.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
|
||||
|
||||
describe("CollectionUtils Service", () => {
|
||||
describe("getNestedCollectionTree", () => {
|
||||
it("should return collections properly sorted if provided out of order", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
const childCollection = new CollectionView({
|
||||
name: "Parent/Child",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
collections.push(childCollection);
|
||||
collections.push(parentCollection);
|
||||
|
||||
// Act
|
||||
const result = getNestedCollectionTree(collections);
|
||||
|
||||
// Assert
|
||||
expect(result[0].node.name).toBe("Parent");
|
||||
expect(result[0].children[0].node.name).toBe("Child");
|
||||
});
|
||||
|
||||
it("should return an empty array if no collections are provided", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
// Act
|
||||
const result = getNestedCollectionTree(collections);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlatCollectionTree", () => {
|
||||
it("should flatten a tree node with no children", () => {
|
||||
// Arrange
|
||||
const collection = new CollectionView({
|
||||
name: "Test Collection",
|
||||
id: "test-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [
|
||||
new TreeNode<CollectionView>(collection, {} as TreeNode<CollectionView>),
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe(collection);
|
||||
});
|
||||
|
||||
it("should flatten a tree node with children", () => {
|
||||
// Arrange
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
id: "parent-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child1Collection = new CollectionView({
|
||||
name: "Child 1",
|
||||
id: "child1-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child2Collection = new CollectionView({
|
||||
name: "Child 2",
|
||||
id: "child2-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const grandchildCollection = new CollectionView({
|
||||
name: "Grandchild",
|
||||
id: "grandchild-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const parentNode = new TreeNode<CollectionView>(
|
||||
parentCollection,
|
||||
{} as TreeNode<CollectionView>,
|
||||
);
|
||||
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
|
||||
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
|
||||
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);
|
||||
|
||||
parentNode.children = [child1Node, child2Node];
|
||||
child1Node.children = [grandchildNode];
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [parentNode];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(4);
|
||||
expect(result[0]).toBe(parentCollection);
|
||||
expect(result).toContain(child1Collection);
|
||||
expect(result).toContain(child2Collection);
|
||||
expect(result).toContain(grandchildCollection);
|
||||
});
|
||||
});
|
||||
});
|
||||
87
libs/common/src/admin-console/utils/collection-utils.ts
Normal file
87
libs/common/src/admin-console/utils/collection-utils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
CollectionView,
|
||||
NestingDelimiter,
|
||||
CollectionAdminView,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
|
||||
export function getNestedCollectionTree(
|
||||
collections: CollectionAdminView[],
|
||||
): TreeNode<CollectionAdminView>[];
|
||||
export function getNestedCollectionTree(collections: CollectionView[]): TreeNode<CollectionView>[];
|
||||
export function getNestedCollectionTree(
|
||||
collections: (CollectionView | CollectionAdminView)[],
|
||||
): TreeNode<CollectionView | CollectionAdminView>[] {
|
||||
if (!collections) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections: CollectionView[] | CollectionAdminView[] = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
const all: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
const groupedByOrg = new Map<OrganizationId, (CollectionView | CollectionAdminView)[]>();
|
||||
clonedCollections.map((c) => {
|
||||
const key = c.organizationId;
|
||||
(groupedByOrg.get(key) ?? groupedByOrg.set(key, []).get(key)!).push(c);
|
||||
});
|
||||
for (const group of groupedByOrg.values()) {
|
||||
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
for (const c of group) {
|
||||
const parts = c.name ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, c, undefined, NestingDelimiter);
|
||||
}
|
||||
all.push(...nodes);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
export function cloneCollection(collection: CollectionView): CollectionView;
|
||||
export function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
export function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = Object.assign(
|
||||
new CollectionAdminView({ ...collection, name: collection.name }),
|
||||
collection,
|
||||
);
|
||||
} else {
|
||||
cloned = Object.assign(
|
||||
new CollectionView({ ...collection, name: collection.name }),
|
||||
collection,
|
||||
);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
export function getFlatCollectionTree(nodes: TreeNode<CollectionView>[]): CollectionView[];
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionView | CollectionAdminView>[],
|
||||
): (CollectionView | CollectionAdminView)[] {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodes.flatMap((node) => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return [node.node];
|
||||
}
|
||||
|
||||
const children = getFlatCollectionTree(node.children);
|
||||
return [node.node, ...children];
|
||||
});
|
||||
}
|
||||
1
libs/common/src/admin-console/utils/index.ts
Normal file
1
libs/common/src/admin-console/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./collection-utils";
|
||||
@@ -1,8 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionView,
|
||||
Collection as CollectionDomain,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionExport } from "./collection.export";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionView,
|
||||
Collection as CollectionDomain,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { CollectionId, emptyGuid, OrganizationId } from "../../types/guid";
|
||||
|
||||
@@ -4,11 +4,11 @@ import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionData,
|
||||
CollectionDetailsResponse,
|
||||
CollectionService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { PolicyResponse } from "../../admin-console/models/response/policy.response";
|
||||
import { UserDecryptionResponse } from "../../key-management/models/response/user-decryption.response";
|
||||
|
||||
@@ -4,16 +4,15 @@ import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CreateCollectionRequest, UpdateCollectionRequest } from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
CreateCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Observable, firstValueFrom, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
Reference in New Issue
Block a user