1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-04-14 20:07:04 +02:00
bitwarden-browser/apps/cli/src/commands/list.command.ts
SmithThe4th 10c8a2101a
[PM-12049] Remove usage of ActiveUserState from folder service ()
* Migrated folder service from using active user state to single user state

Added extra test cases for encrypted folder and decrypted folders

Updated derived state to use decrypt with key

* Update callers in the web

* Update callers in the browser

* Update callers in libs

* Update callers in cli

* Fixed test

* Fixed folder state test

* Fixed test

* removed duplicate activeUserId

* Added takewhile operator to only make calls when userId is present

* Simplified to accept a single user id instead of an observable

* Required userid to be passed from notification service

* [PM-15635] Folders not working on desktop ()

* Added folders memory state definition

* added decrypted folders state

* Refactored service to remove derived state

* removed combinedstate and added clear decrypted folders to methods

* Fixed test

* Fixed issue with editing folder on the desktop app

* Fixed test

* Changed state name

* fixed ts strict issue

* fixed ts strict issue

* fixed ts strict issue

* removed unnecessasry null encrypteed folder check

* Handle null folderdata

* [PM-16197] "Items with No Folder" shows as a folder to edit name and delete ()

* Force redcryption anytime encryption state changes

* Fixed text file

* revert changes

* create new object with nofolder instead of modifying exisiting object

* Fixed failing test

* switched to use memory-large-object

* Fixed ts sctrict issue

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
2025-01-02 17:16:33 -05:00

267 lines
10 KiB
TypeScript

import { firstValueFrom, map } from "rxjs";
import {
OrganizationUserApiService,
CollectionService,
CollectionData,
Collection,
CollectionDetailsResponse as ApiCollectionDetailsResponse,
CollectionResponse as ApiCollectionResponse,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { OrganizationUserResponse } from "../admin-console/models/response/organization-user.response";
import { OrganizationResponse } from "../admin-console/models/response/organization.response";
import { Response } from "../models/response";
import { ListResponse } from "../models/response/list.response";
import { CliUtils } from "../utils";
import { CipherResponse } from "../vault/models/cipher.response";
import { CollectionResponse } from "../vault/models/collection.response";
import { FolderResponse } from "../vault/models/folder.response";
export class ListCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
private searchService: SearchService,
private organizationUserApiService: OrganizationUserApiService,
private apiService: ApiService,
private eventCollectionService: EventCollectionService,
private accountService: AccountService,
) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "items":
return await this.listCiphers(normalizedOptions);
case "folders":
return await this.listFolders(normalizedOptions);
case "collections":
return await this.listCollections(normalizedOptions);
case "org-collections":
return await this.listOrganizationCollections(normalizedOptions);
case "org-members":
return await this.listOrganizationMembers(normalizedOptions);
case "organizations":
return await this.listOrganizations(normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async listCiphers(options: Options) {
let ciphers: CipherView[];
options.trash = options.trash || false;
if (options.url != null && options.url.trim() !== "") {
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url);
} else {
ciphers = await this.cipherService.getAllDecrypted();
}
if (
options.folderId != null ||
options.collectionId != null ||
options.organizationId != null
) {
ciphers = ciphers.filter((c) => {
if (options.trash !== c.isDeleted) {
return false;
}
if (options.folderId != null) {
if (options.folderId === "notnull" && c.folderId != null) {
return true;
}
const folderId = options.folderId === "null" ? null : options.folderId;
if (folderId === c.folderId) {
return true;
}
}
if (options.organizationId != null) {
if (options.organizationId === "notnull" && c.organizationId != null) {
return true;
}
const organizationId = options.organizationId === "null" ? null : options.organizationId;
if (organizationId === c.organizationId) {
return true;
}
}
if (options.collectionId != null) {
if (
options.collectionId === "notnull" &&
c.collectionIds != null &&
c.collectionIds.length > 0
) {
return true;
}
const collectionId = options.collectionId === "null" ? null : options.collectionId;
if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) {
return true;
}
if (
collectionId != null &&
c.collectionIds != null &&
c.collectionIds.indexOf(collectionId) > -1
) {
return true;
}
}
return false;
});
} else if (options.search == null || options.search.trim() === "") {
ciphers = ciphers.filter((c) => options.trash === c.isDeleted);
}
if (options.search != null && options.search.trim() !== "") {
ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash);
}
await this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, ciphers, true);
const res = new ListResponse(ciphers.map((o) => new CipherResponse(o)));
return Response.success(res);
}
private async listFolders(options: Options) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
if (options.search != null && options.search.trim() !== "") {
folders = CliUtils.searchFolders(folders, options.search);
}
const res = new ListResponse(folders.map((o) => new FolderResponse(o)));
return Response.success(res);
}
private async listCollections(options: Options) {
let collections = await this.collectionService.getAllDecrypted();
if (options.organizationId != null) {
collections = collections.filter((c) => {
if (options.organizationId === c.organizationId) {
return true;
}
return false;
});
}
if (options.search != null && options.search.trim() !== "") {
collections = CliUtils.searchCollections(collections, options.search);
}
const res = new ListResponse(collections.map((o) => new CollectionResponse(o)));
return Response.success(res);
}
private async listOrganizationCollections(options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
const organization = await this.organizationService.getFromState(options.organizationId);
if (organization == null) {
return Response.error("Organization not found.");
}
try {
let response: ApiListResponse<ApiCollectionResponse>;
if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(options.organizationId);
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data
.filter((c) => c.organizationId === options.organizationId)
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
let decCollections = await this.collectionService.decryptMany(collections);
if (options.search != null && options.search.trim() !== "") {
decCollections = CliUtils.searchCollections(decCollections, options.search);
}
const res = new ListResponse(decCollections.map((o) => new CollectionResponse(o)));
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async listOrganizationMembers(options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
const organization = await this.organizationService.getFromState(options.organizationId);
if (organization == null) {
return Response.error("Organization not found.");
}
try {
const response = await this.organizationUserApiService.getAllUsers(options.organizationId);
const res = new ListResponse(
response.data.map((r) => {
const u = new OrganizationUserResponse();
u.email = r.email;
u.name = r.name;
u.id = r.id;
u.status = r.status;
u.type = r.type;
u.twoFactorEnabled = r.twoFactorEnabled;
return u;
}),
);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async listOrganizations(options: Options) {
let organizations = await firstValueFrom(this.organizationService.memberOrganizations$);
if (options.search != null && options.search.trim() !== "") {
organizations = CliUtils.searchOrganizations(organizations, options.search);
}
const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o)));
return Response.success(res);
}
}
class Options {
organizationId: string;
collectionId: string;
folderId: string;
search: string;
url: string;
trash: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.collectionId = passedOptions?.collectionid || passedOptions?.collectionId;
this.folderId = passedOptions?.folderid || passedOptions?.folderId;
this.search = passedOptions?.search;
this.url = passedOptions?.url;
this.trash = CliUtils.convertBooleanOption(passedOptions?.trash);
}
}