1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-24534] Archive via CLI (#16502)

* refactor `canInteract` into a component level usage.

- The default service is going to be used in the CLI which won't make use of the UI-related aspects

* all nested entities to be imported from the vault

* initial add of archive command to the cli

* add archive to oss serve

* check for deleted cipher when attempting to archive

* add searchability/list functionality for archived ciphers

* restore an archived cipher

* unarchive a cipher when a user is editing it and has lost their premium status

* add missing feature flags

* re-export only needed services from the vault

* add needed await

* add prompt when applicable for editing an archived cipher

* move cipher archive service into `common/vault`

* fix testing code
This commit is contained in:
Nick Krantz
2025-09-30 09:45:04 -05:00
committed by GitHub
parent 7848b7d480
commit 727689d827
27 changed files with 401 additions and 131 deletions

View File

@@ -119,10 +119,12 @@ import { SystemNotificationsService } from "@bitwarden/common/platform/system-no
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service"; import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { import {
@@ -145,8 +147,6 @@ import {
DefaultSshImportPromptService, DefaultSshImportPromptService,
PasswordRepromptService, PasswordRepromptService,
SshImportPromptService, SshImportPromptService,
CipherArchiveService,
DefaultCipherArchiveService,
} from "@bitwarden/vault"; } from "@bitwarden/vault";
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service"; import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
@@ -708,14 +708,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: CipherArchiveService, provide: CipherArchiveService,
useClass: DefaultCipherArchiveService, useClass: DefaultCipherArchiveService,
deps: [ deps: [CipherService, ApiService, BillingAccountProfileStateService, ConfigService],
CipherService,
ApiService,
DialogService,
PasswordRepromptService,
BillingAccountProfileStateService,
ConfigService,
],
}), }),
]; ];

View File

@@ -13,6 +13,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
@@ -28,7 +29,7 @@ import {
MenuModule, MenuModule,
ToastService, ToastService,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { CipherArchiveService, PasswordRepromptService } from "@bitwarden/vault"; import { PasswordRepromptService } from "@bitwarden/vault";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";

View File

@@ -14,6 +14,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec"; import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec";
import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
@@ -26,7 +27,6 @@ import {
RestrictedItemTypesService, RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service"; } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { CipherArchiveService } from "@bitwarden/vault";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";

View File

@@ -26,6 +26,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
@@ -35,7 +36,6 @@ import {
CipherViewLike, CipherViewLike,
CipherViewLikeUtils, CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils"; } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { CipherArchiveService } from "@bitwarden/vault";
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator"; import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service"; import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";

View File

@@ -9,6 +9,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import {
@@ -22,7 +23,11 @@ import {
ToastService, ToastService,
TypographyModule, TypographyModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { CanDeleteCipherDirective, CipherArchiveService } from "@bitwarden/vault"; import {
CanDeleteCipherDirective,
DecryptionFailureDialogComponent,
PasswordRepromptService,
} from "@bitwarden/vault";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
@@ -56,6 +61,7 @@ export class ArchiveComponent {
private toastService = inject(ToastService); private toastService = inject(ToastService);
private i18nService = inject(I18nService); private i18nService = inject(I18nService);
private cipherArchiveService = inject(CipherArchiveService); private cipherArchiveService = inject(CipherArchiveService);
private passwordRepromptService = inject(PasswordRepromptService);
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId); private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
@@ -69,7 +75,7 @@ export class ArchiveComponent {
); );
async view(cipher: CipherView) { async view(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) { if (!(await this.canInteract(cipher))) {
return; return;
} }
@@ -79,7 +85,7 @@ export class ArchiveComponent {
} }
async edit(cipher: CipherView) { async edit(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) { if (!(await this.canInteract(cipher))) {
return; return;
} }
@@ -89,7 +95,7 @@ export class ArchiveComponent {
} }
async delete(cipher: CipherView) { async delete(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) { if (!(await this.canInteract(cipher))) {
return; return;
} }
const confirmed = await this.dialogService.openSimpleDialog({ const confirmed = await this.dialogService.openSimpleDialog({
@@ -118,7 +124,7 @@ export class ArchiveComponent {
} }
async unarchive(cipher: CipherView) { async unarchive(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) { if (!(await this.canInteract(cipher))) {
return; return;
} }
const activeUserId = await firstValueFrom(this.userId$); const activeUserId = await firstValueFrom(this.userId$);
@@ -132,7 +138,7 @@ export class ArchiveComponent {
} }
async clone(cipher: CipherView) { async clone(cipher: CipherView) {
if (!(await this.cipherArchiveService.canInteract(cipher))) { if (!(await this.canInteract(cipher))) {
return; return;
} }
@@ -156,4 +162,21 @@ export class ArchiveComponent {
}, },
}); });
} }
/**
* Check if the user is able to interact with the cipher
* (password re-prompt / decryption failure checks).
* @param cipher
* @private
*/
private canInteract(cipher: CipherView) {
if (cipher.decryptionFailure) {
DecryptionFailureDialogComponent.open(this.dialogService, {
cipherIds: [cipher.id as CipherId],
});
return false;
}
return this.passwordRepromptService.passwordRepromptCheck(cipher);
}
} }

View File

@@ -9,9 +9,9 @@ import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
import { CipherArchiveService } from "@bitwarden/vault";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";

View File

@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import * as inquirer from "inquirer";
import { firstValueFrom, map, switchMap } from "rxjs"; import { firstValueFrom, map, switchMap } from "rxjs";
import { UpdateCollectionRequest } from "@bitwarden/admin-console/common"; import { UpdateCollectionRequest } from "@bitwarden/admin-console/common";
@@ -9,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
@@ -40,6 +42,7 @@ export class EditCommand {
private accountService: AccountService, private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
private policyService: PolicyService, private policyService: PolicyService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async run( async run(
@@ -92,6 +95,10 @@ export class EditCommand {
private async editCipher(id: string, req: CipherExport) { private async editCipher(id: string, req: CipherExport) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(id, activeUserId); const cipher = await this.cipherService.get(id, activeUserId);
const hasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
@@ -102,6 +109,17 @@ export class EditCommand {
} }
cipherView = CipherExport.toView(req, cipherView); cipherView = CipherExport.toView(req, cipherView);
// When a user is editing an archived cipher and does not have premium, automatically unarchive it
if (cipherView.isArchived && !hasPremium) {
const acceptedPrompt = await this.promptForArchiveEdit();
if (!acceptedPrompt) {
return Response.error("Edit cancelled.");
}
cipherView.archivedDate = null;
}
const isCipherRestricted = const isCipherRestricted =
await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView); await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView);
if (isCipherRestricted) { if (isCipherRestricted) {
@@ -240,6 +258,38 @@ export class EditCommand {
return Response.error(e); return Response.error(e);
} }
} }
/** Prompt the user to accept movement of their cipher back to the their vault. */
private async promptForArchiveEdit(): Promise<boolean> {
// When running in serve or no interaction mode, automatically accept the prompt
if (process.env.BW_SERVE === "true" || process.env.BW_NOINTERACTION === "true") {
CliUtils.writeLn(
"Archive is only available with a Premium subscription, which has ended. Your edit was saved and the item was moved back to your vault.",
);
return true;
}
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "list",
name: "confirm",
message:
"When you edit and save details for an archived item without a Premium subscription, it'll be moved from your archive back to your vault.",
choices: [
{
name: "Move now",
value: "confirmed",
},
{
name: "Cancel",
value: "cancel",
},
],
});
return answer.confirm === "confirmed";
}
} }
class Options { class Options {

View File

@@ -16,6 +16,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response"; import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
@@ -45,6 +46,7 @@ export class ListCommand {
private accountService: AccountService, private accountService: AccountService,
private keyService: KeyService, private keyService: KeyService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
private cipherArchiveService: CipherArchiveService,
) {} ) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> { async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -71,8 +73,13 @@ export class ListCommand {
let ciphers: CipherView[]; let ciphers: CipherView[];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const userCanArchive = await firstValueFrom(
this.cipherArchiveService.userCanArchive$(activeUserId),
);
options.trash = options.trash || false; options.trash = options.trash || false;
options.archived = userCanArchive && options.archived;
if (options.url != null && options.url.trim() !== "") { if (options.url != null && options.url.trim() !== "") {
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId); ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId);
} else { } else {
@@ -85,9 +92,12 @@ export class ListCommand {
options.organizationId != null options.organizationId != null
) { ) {
ciphers = ciphers.filter((c) => { ciphers = ciphers.filter((c) => {
if (options.trash !== c.isDeleted) { const matchesStateOptions = this.matchesStateOptions(c, options);
if (!matchesStateOptions) {
return false; return false;
} }
if (options.folderId != null) { if (options.folderId != null) {
if (options.folderId === "notnull" && c.folderId != null) { if (options.folderId === "notnull" && c.folderId != null) {
return true; return true;
@@ -131,11 +141,16 @@ export class ListCommand {
return false; return false;
}); });
} else if (options.search == null || options.search.trim() === "") { } else if (options.search == null || options.search.trim() === "") {
ciphers = ciphers.filter((c) => options.trash === c.isDeleted); ciphers = ciphers.filter((c) => this.matchesStateOptions(c, options));
} }
if (options.search != null && options.search.trim() !== "") { if (options.search != null && options.search.trim() !== "") {
ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); ciphers = this.searchService.searchCiphersBasic(
ciphers,
options.search,
options.trash,
options.archived,
);
} }
ciphers = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(ciphers); ciphers = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(ciphers);
@@ -287,6 +302,17 @@ export class ListCommand {
const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o))); const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o)));
return Response.success(res); return Response.success(res);
} }
/**
* Checks if the cipher passes either the trash or the archive options.
* @returns true if the cipher passes *any* of the filters
*/
private matchesStateOptions(c: CipherView, options: Options): boolean {
const passesTrashFilter = options.trash && c.isDeleted;
const passesArchivedFilter = options.archived && c.isArchived;
return passesTrashFilter || passesArchivedFilter;
}
} }
class Options { class Options {
@@ -296,6 +322,7 @@ class Options {
search: string; search: string;
url: string; url: string;
trash: boolean; trash: boolean;
archived: boolean;
constructor(passedOptions: Record<string, any>) { constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId; this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
@@ -304,5 +331,6 @@ class Options {
this.search = passedOptions?.search; this.search = passedOptions?.search;
this.url = passedOptions?.url; this.url = passedOptions?.url;
this.trash = CliUtils.convertBooleanOption(passedOptions?.trash); this.trash = CliUtils.convertBooleanOption(passedOptions?.trash);
this.archived = CliUtils.convertBooleanOption(passedOptions?.archived);
} }
} }

View File

@@ -2,8 +2,14 @@ import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { UserId } from "@bitwarden/user-core";
import { Response } from "../models/response"; import { Response } from "../models/response";
@@ -12,6 +18,8 @@ export class RestoreCommand {
private cipherService: CipherService, private cipherService: CipherService,
private accountService: AccountService, private accountService: AccountService,
private cipherAuthorizationService: CipherAuthorizationService, private cipherAuthorizationService: CipherAuthorizationService,
private cipherArchiveService: CipherArchiveService,
private configService: ConfigService,
) {} ) {}
async run(object: string, id: string): Promise<Response> { async run(object: string, id: string): Promise<Response> {
@@ -30,10 +38,23 @@ export class RestoreCommand {
private async restoreCipher(id: string) { private async restoreCipher(id: string) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(id, activeUserId); const cipher = await this.cipherService.get(id, activeUserId);
const isArchivedVaultEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive),
);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
if (cipher.archivedDate && isArchivedVaultEnabled) {
return this.restoreArchivedCipher(cipher, activeUserId);
} else {
return this.restoreDeletedCipher(cipher, activeUserId);
}
}
/** Restores a cipher from the trash. */
private async restoreDeletedCipher(cipher: Cipher, userId: UserId) {
if (cipher.deletedDate == null) { if (cipher.deletedDate == null) {
return Response.badRequest("Cipher is not in trash."); return Response.badRequest("Cipher is not in trash.");
} }
@@ -47,7 +68,17 @@ export class RestoreCommand {
} }
try { try {
await this.cipherService.restoreWithServer(id, activeUserId); await this.cipherService.restoreWithServer(cipher.id, userId);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
/** Restore a cipher from the archive vault */
private async restoreArchivedCipher(cipher: Cipher, userId: UserId) {
try {
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, userId);
return Response.success(); return Response.success();
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);

View File

@@ -51,7 +51,7 @@ export class ServeCommand {
.use(koaBodyParser()) .use(koaBodyParser())
.use(koaJson({ pretty: false, param: "pretty" })); .use(koaJson({ pretty: false, param: "pretty" }));
this.serveConfigurator.configureRouter(router); await this.serveConfigurator.configureRouter(router);
server.use(router.routes()).use(router.allowedMethods()); server.use(router.routes()).use(router.allowedMethods());

View File

@@ -5,6 +5,8 @@ import * as koaRouter from "@koa/router";
import * as koa from "koa"; import * as koa from "koa";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom, map } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ConfirmCommand } from "./admin-console/commands/confirm.command";
import { ShareCommand } from "./admin-console/commands/share.command"; import { ShareCommand } from "./admin-console/commands/share.command";
import { LockCommand } from "./auth/commands/lock.command"; import { LockCommand } from "./auth/commands/lock.command";
@@ -26,6 +28,7 @@ import {
SendListCommand, SendListCommand,
SendRemovePasswordCommand, SendRemovePasswordCommand,
} from "./tools/send"; } from "./tools/send";
import { ArchiveCommand } from "./vault/archive.command";
import { CreateCommand } from "./vault/create.command"; import { CreateCommand } from "./vault/create.command";
import { DeleteCommand } from "./vault/delete.command"; import { DeleteCommand } from "./vault/delete.command";
import { SyncCommand } from "./vault/sync.command"; import { SyncCommand } from "./vault/sync.command";
@@ -40,6 +43,7 @@ export class OssServeConfigurator {
private statusCommand: StatusCommand; private statusCommand: StatusCommand;
private syncCommand: SyncCommand; private syncCommand: SyncCommand;
private deleteCommand: DeleteCommand; private deleteCommand: DeleteCommand;
private archiveCommand: ArchiveCommand;
private confirmCommand: ConfirmCommand; private confirmCommand: ConfirmCommand;
private restoreCommand: RestoreCommand; private restoreCommand: RestoreCommand;
private lockCommand: LockCommand; private lockCommand: LockCommand;
@@ -81,6 +85,7 @@ export class OssServeConfigurator {
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.keyService, this.serviceContainer.keyService,
this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.cliRestrictedItemTypesService,
this.serviceContainer.cipherArchiveService,
); );
this.createCommand = new CreateCommand( this.createCommand = new CreateCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
@@ -104,6 +109,7 @@ export class OssServeConfigurator {
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.cliRestrictedItemTypesService,
this.serviceContainer.policyService, this.serviceContainer.policyService,
this.serviceContainer.billingAccountProfileStateService,
); );
this.generateCommand = new GenerateCommand( this.generateCommand = new GenerateCommand(
this.serviceContainer.passwordGenerationService, this.serviceContainer.passwordGenerationService,
@@ -127,6 +133,13 @@ export class OssServeConfigurator {
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.cliRestrictedItemTypesService,
); );
this.archiveCommand = new ArchiveCommand(
this.serviceContainer.cipherService,
this.serviceContainer.accountService,
this.serviceContainer.configService,
this.serviceContainer.cipherArchiveService,
this.serviceContainer.billingAccountProfileStateService,
);
this.confirmCommand = new ConfirmCommand( this.confirmCommand = new ConfirmCommand(
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.keyService, this.serviceContainer.keyService,
@@ -140,6 +153,8 @@ export class OssServeConfigurator {
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cipherAuthorizationService, this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.cipherArchiveService,
this.serviceContainer.configService,
); );
this.shareCommand = new ShareCommand( this.shareCommand = new ShareCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
@@ -199,7 +214,7 @@ export class OssServeConfigurator {
); );
} }
configureRouter(router: koaRouter) { async configureRouter(router: koaRouter) {
router.get("/generate", async (ctx, next) => { router.get("/generate", async (ctx, next) => {
const response = await this.generateCommand.run(ctx.request.query); const response = await this.generateCommand.run(ctx.request.query);
this.processResponse(ctx.response, response); this.processResponse(ctx.response, response);
@@ -401,6 +416,23 @@ export class OssServeConfigurator {
this.processResponse(ctx.response, response); this.processResponse(ctx.response, response);
await next(); await next();
}); });
const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag(
FeatureFlag.PM19148_InnovationArchive,
);
if (isArchivedEnabled) {
router.post("/archive/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
response = await this.archiveCommand.run(ctx.params.object, ctx.params.id);
this.processResponse(ctx.response, response);
await next();
});
}
} }
protected processResponse(res: koa.Response, commandResponse: Response) { protected processResponse(res: koa.Response, commandResponse: Response) {

View File

@@ -5,6 +5,7 @@ import { program, Command, OptionValues } from "commander";
import { firstValueFrom, of, switchMap } from "rxjs"; import { firstValueFrom, of, switchMap } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockCommand } from "./auth/commands/lock.command"; import { LockCommand } from "./auth/commands/lock.command";
import { LoginCommand } from "./auth/commands/login.command"; import { LoginCommand } from "./auth/commands/login.command";
@@ -26,6 +27,10 @@ const writeLn = CliUtils.writeLn;
export class Program extends BaseProgram { export class Program extends BaseProgram {
async register() { async register() {
const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag(
FeatureFlag.PM19148_InnovationArchive,
);
program program
.option("--pretty", "Format output. JSON is tabbed with two spaces.") .option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option("--raw", "Return raw output instead of a descriptive message.") .option("--raw", "Return raw output instead of a descriptive message.")
@@ -94,6 +99,9 @@ export class Program extends BaseProgram {
" bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==", " bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==",
); );
writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412"); writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412");
if (isArchivedEnabled) {
writeLn(" bw archive item 99ee88d2-6046-4ea7-92c2-acac464b1412");
}
writeLn(" bw generate -lusn --length 18"); writeLn(" bw generate -lusn --length 18");
writeLn(" bw config server https://bitwarden.example.com"); writeLn(" bw config server https://bitwarden.example.com");
writeLn(" bw send -f ./file.ext"); writeLn(" bw send -f ./file.ext");

View File

@@ -15,7 +15,7 @@ export async function registerOssPrograms(serviceContainer: ServiceContainer) {
await program.register(); await program.register();
const vaultProgram = new VaultProgram(serviceContainer); const vaultProgram = new VaultProgram(serviceContainer);
vaultProgram.register(); await vaultProgram.register();
const sendProgram = new SendProgram(serviceContainer); const sendProgram = new SendProgram(serviceContainer);
sendProgram.register(); sendProgram.register();

View File

@@ -125,6 +125,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { import {
@@ -132,6 +133,7 @@ import {
DefaultCipherAuthorizationService, DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service"; } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
@@ -303,6 +305,7 @@ export class ServiceContainer {
cipherEncryptionService: CipherEncryptionService; cipherEncryptionService: CipherEncryptionService;
restrictedItemTypesService: RestrictedItemTypesService; restrictedItemTypesService: RestrictedItemTypesService;
cliRestrictedItemTypesService: CliRestrictedItemTypesService; cliRestrictedItemTypesService: CliRestrictedItemTypesService;
cipherArchiveService: CipherArchiveService;
constructor() { constructor() {
let p = null; let p = null;
@@ -730,6 +733,13 @@ export class ServiceContainer {
this.messagingService, this.messagingService,
); );
this.cipherArchiveService = new DefaultCipherArchiveService(
this.cipherService,
this.apiService,
this.billingAccountProfileStateService,
this.configService,
);
this.folderService = new FolderService( this.folderService = new FolderService(
this.keyService, this.keyService,
this.encryptService, this.encryptService,

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore // @ts-strict-ignore
import { program, Command } from "commander"; import { program, Command } from "commander";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ConfirmCommand } from "./admin-console/commands/confirm.command";
import { ShareCommand } from "./admin-console/commands/share.command"; import { ShareCommand } from "./admin-console/commands/share.command";
import { BaseProgram } from "./base-program"; import { BaseProgram } from "./base-program";
@@ -13,25 +15,34 @@ import { Response } from "./models/response";
import { ExportCommand } from "./tools/export.command"; import { ExportCommand } from "./tools/export.command";
import { ImportCommand } from "./tools/import.command"; import { ImportCommand } from "./tools/import.command";
import { CliUtils } from "./utils"; import { CliUtils } from "./utils";
import { ArchiveCommand } from "./vault/archive.command";
import { CreateCommand } from "./vault/create.command"; import { CreateCommand } from "./vault/create.command";
import { DeleteCommand } from "./vault/delete.command"; import { DeleteCommand } from "./vault/delete.command";
const writeLn = CliUtils.writeLn; const writeLn = CliUtils.writeLn;
export class VaultProgram extends BaseProgram { export class VaultProgram extends BaseProgram {
register() { async register() {
const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag(
FeatureFlag.PM19148_InnovationArchive,
);
program program
.addCommand(this.listCommand()) .addCommand(this.listCommand(isArchivedEnabled))
.addCommand(this.getCommand()) .addCommand(this.getCommand())
.addCommand(this.createCommand()) .addCommand(this.createCommand())
.addCommand(this.editCommand()) .addCommand(this.editCommand())
.addCommand(this.deleteCommand()) .addCommand(this.deleteCommand())
.addCommand(this.restoreCommand()) .addCommand(this.restoreCommand(isArchivedEnabled))
.addCommand(this.shareCommand("move", false)) .addCommand(this.shareCommand("move", false))
.addCommand(this.confirmCommand()) .addCommand(this.confirmCommand())
.addCommand(this.importCommand()) .addCommand(this.importCommand())
.addCommand(this.exportCommand()) .addCommand(this.exportCommand())
.addCommand(this.shareCommand("share", true)); .addCommand(this.shareCommand("share", true));
if (isArchivedEnabled) {
program.addCommand(this.archiveCommand());
}
} }
private validateObject(requestedObject: string, validObjects: string[]): boolean { private validateObject(requestedObject: string, validObjects: string[]): boolean {
@@ -42,7 +53,7 @@ export class VaultProgram extends BaseProgram {
Response.badRequest( Response.badRequest(
'Unknown object "' + 'Unknown object "' +
requestedObject + requestedObject +
'". Allowed objects are ' + '". Allowed objects are: ' +
validObjects.join(", ") + validObjects.join(", ") +
".", ".",
), ),
@@ -51,7 +62,7 @@ export class VaultProgram extends BaseProgram {
return success; return success;
} }
private listCommand(): Command { private listCommand(isArchivedEnabled: boolean): Command {
const listObjects = [ const listObjects = [
"items", "items",
"folders", "folders",
@@ -61,7 +72,7 @@ export class VaultProgram extends BaseProgram {
"organizations", "organizations",
]; ];
return new Command("list") const command = new Command("list")
.argument("<object>", "Valid objects are: " + listObjects.join(", ")) .argument("<object>", "Valid objects are: " + listObjects.join(", "))
.description("List an array of objects from the vault.") .description("List an array of objects from the vault.")
.option("--search <search>", "Perform a search on the listed objects.") .option("--search <search>", "Perform a search on the listed objects.")
@@ -94,6 +105,9 @@ export class VaultProgram extends BaseProgram {
" bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull", " bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull",
); );
writeLn(" bw list items --trash"); writeLn(" bw list items --trash");
if (isArchivedEnabled) {
writeLn(" bw list items --archived");
}
writeLn(" bw list folders --search email"); writeLn(" bw list folders --search email");
writeLn(" bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2"); writeLn(" bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2");
writeLn("", true); writeLn("", true);
@@ -116,11 +130,18 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.keyService, this.serviceContainer.keyService,
this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.cliRestrictedItemTypesService,
this.serviceContainer.cipherArchiveService,
); );
const response = await command.run(object, cmd); const response = await command.run(object, cmd);
this.processResponse(response); this.processResponse(response);
}); });
if (isArchivedEnabled) {
command.option("--archived", "Filter items that are archived.");
}
return command;
} }
private getCommand(): Command { private getCommand(): Command {
@@ -286,6 +307,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.cliRestrictedItemTypesService,
this.serviceContainer.policyService, this.serviceContainer.policyService,
this.serviceContainer.billingAccountProfileStateService,
); );
const response = await command.run(object, id, encodedJson, cmd); const response = await command.run(object, id, encodedJson, cmd);
this.processResponse(response); this.processResponse(response);
@@ -336,12 +358,41 @@ export class VaultProgram extends BaseProgram {
}); });
} }
private restoreCommand(): Command { private archiveCommand(): Command {
const archiveObjects = ["item"];
return new Command("archive")
.argument("<object>", "Valid objects are: " + archiveObjects.join(", "))
.argument("<id>", "Object's globally unique `id`.")
.description("Archive an object from the vault.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw archive item 7063feab-4b10-472e-b64c-785e2b870b92");
writeLn("", true);
})
.action(async (object, id) => {
if (!this.validateObject(object, archiveObjects)) {
return;
}
await this.exitIfLocked();
const command = new ArchiveCommand(
this.serviceContainer.cipherService,
this.serviceContainer.accountService,
this.serviceContainer.configService,
this.serviceContainer.cipherArchiveService,
this.serviceContainer.billingAccountProfileStateService,
);
const response = await command.run(object, id);
this.processResponse(response);
});
}
private restoreCommand(isArchivedEnabled: boolean): Command {
const restoreObjects = ["item"]; const restoreObjects = ["item"];
return new Command("restore") const command = new Command("restore")
.argument("<object>", "Valid objects are: " + restoreObjects.join(", ")) .argument("<object>", "Valid objects are: " + restoreObjects.join(", "))
.argument("<id>", "Object's globally unique `id`.") .argument("<id>", "Object's globally unique `id`.")
.description("Restores an object from the trash.")
.on("--help", () => { .on("--help", () => {
writeLn("\n Examples:"); writeLn("\n Examples:");
writeLn(""); writeLn("");
@@ -358,10 +409,20 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cipherAuthorizationService, this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.cipherArchiveService,
this.serviceContainer.configService,
); );
const response = await command.run(object, id); const response = await command.run(object, id);
this.processResponse(response); this.processResponse(response);
}); });
if (isArchivedEnabled) {
command.description("Restores an object from the trash or archive.");
} else {
command.description("Restores an object from the trash.");
}
return command;
} }
private shareCommand(commandName: string, deprecated: boolean): Command { private shareCommand(commandName: string, deprecated: boolean): Command {

View File

@@ -0,0 +1,109 @@
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { UserId } from "@bitwarden/user-core";
import { Response } from "../models/response";
export class ArchiveCommand {
constructor(
private cipherService: CipherService,
private accountService: AccountService,
private configService: ConfigService,
private cipherArchiveService: CipherArchiveService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
async run(object: string, id: string): Promise<Response> {
const featureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM19148_InnovationArchive,
);
if (!featureFlagEnabled) {
return Response.notFound();
}
if (id != null) {
id = id.toLowerCase();
}
const normalizedObject = object.toLowerCase();
if (normalizedObject === "item") {
return this.archiveCipher(id);
}
return Response.badRequest("Unknown object.");
}
private async archiveCipher(cipherId: string) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(cipherId, activeUserId);
if (cipher == null) {
return Response.notFound();
}
const cipherView = await this.cipherService.decrypt(cipher, activeUserId);
const { canArchive, errorMessage } = await this.userCanArchiveCipher(cipherView, activeUserId);
if (!canArchive) {
return Response.error(errorMessage);
}
try {
await this.cipherArchiveService.archiveWithServer(cipherView.id as CipherId, activeUserId);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
/**
* Determines if the user can archive the given cipher.
* When the user cannot archive the cipher, an appropriate error message is provided.
*/
private async userCanArchiveCipher(
cipher: CipherView,
userId: UserId,
): Promise<
{ canArchive: true; errorMessage?: never } | { canArchive: false; errorMessage: string }
> {
const hasPremiumFromAnySource = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
);
switch (true) {
case !hasPremiumFromAnySource: {
return {
canArchive: false,
errorMessage: "Premium status is required to use this feature.",
};
}
case CipherViewLikeUtils.isArchived(cipher): {
return { canArchive: false, errorMessage: "Item is already archived." };
}
case CipherViewLikeUtils.isDeleted(cipher): {
return {
canArchive: false,
errorMessage: "Item is in the trash, the item must be restored before archiving.",
};
}
case cipher.organizationId != null: {
return { canArchive: false, errorMessage: "Cannot archive items in an organization." };
}
default:
return { canArchive: true };
}
}
}

View File

@@ -9,11 +9,11 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { CipherArchiveService } from "@bitwarden/vault";
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";

View File

@@ -19,12 +19,12 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { CipherArchiveService } from "@bitwarden/vault";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { VaultFilterService } from "../services/abstractions/vault-filter.service"; import { VaultFilterService } from "../services/abstractions/vault-filter.service";

View File

@@ -54,6 +54,7 @@ import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.se
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@@ -77,7 +78,6 @@ import {
AttachmentDialogCloseResult, AttachmentDialogCloseResult,
AttachmentDialogResult, AttachmentDialogResult,
AttachmentsV2Component, AttachmentsV2Component,
CipherArchiveService,
CipherFormConfig, CipherFormConfig,
CollectionAssignmentResult, CollectionAssignmentResult,
DecryptionFailureDialogComponent, DecryptionFailureDialogComponent,

View File

@@ -16,9 +16,9 @@ export class BitServeConfigurator extends OssServeConfigurator {
super(serviceContainer); super(serviceContainer);
} }
override configureRouter(router: koaRouter): void { override async configureRouter(router: koaRouter): Promise<void> {
// Register OSS endpoints // Register OSS endpoints
super.configureRouter(router); await super.configureRouter(router);
// Register bit endpoints // Register bit endpoints
this.serveDeviceApprovals(router); this.serveDeviceApprovals(router);

View File

@@ -264,6 +264,7 @@ import {
InternalSendService, InternalSendService,
SendService as SendServiceAbstraction, SendService as SendServiceAbstraction,
} from "@bitwarden/common/tools/send/services/send.service.abstraction"; } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
@@ -284,6 +285,7 @@ import {
DefaultCipherAuthorizationService, DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service"; } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
@@ -296,7 +298,6 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
import { import {
AnonLayoutWrapperDataService, AnonLayoutWrapperDataService,
DefaultAnonLayoutWrapperDataService, DefaultAnonLayoutWrapperDataService,
DialogService,
ToastService, ToastService,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { import {
@@ -345,11 +346,7 @@ import {
import { SafeInjectionToken } from "@bitwarden/ui-common"; import { SafeInjectionToken } from "@bitwarden/ui-common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // 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 // eslint-disable-next-line no-restricted-imports
import { import { PasswordRepromptService } from "@bitwarden/vault";
CipherArchiveService,
DefaultCipherArchiveService,
PasswordRepromptService,
} from "@bitwarden/vault";
import { import {
IndividualVaultExportService, IndividualVaultExportService,
IndividualVaultExportServiceAbstraction, IndividualVaultExportServiceAbstraction,
@@ -1652,8 +1649,6 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
CipherServiceAbstraction, CipherServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
DialogService,
PasswordRepromptService,
BillingAccountProfileStateService, BillingAccountProfileStateService,
ConfigService, ConfigService,
], ],

View File

@@ -1,7 +1,6 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
export abstract class CipherArchiveService { export abstract class CipherArchiveService {
@@ -10,5 +9,4 @@ export abstract class CipherArchiveService {
abstract showArchiveVault$(userId: UserId): Observable<boolean>; abstract showArchiveVault$(userId: UserId): Observable<boolean>;
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>; abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>; abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
abstract canInteract(cipher: CipherView): Promise<boolean>;
} }

View File

@@ -30,6 +30,7 @@ export abstract class SearchService {
ciphers: C[], ciphers: C[],
query: string, query: string,
deleted?: boolean, deleted?: boolean,
archived?: boolean,
): C[]; ): C[];
abstract searchSends(sends: SendView[], query: string): SendView[]; abstract searchSends(sends: SendView[], query: string): SendView[];
} }

View File

@@ -11,21 +11,14 @@ import {
CipherBulkArchiveRequest, CipherBulkArchiveRequest,
CipherBulkUnarchiveRequest, CipherBulkUnarchiveRequest,
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request"; } from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherListView } from "@bitwarden/sdk-internal"; import { CipherListView } from "@bitwarden/sdk-internal";
import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component";
import { DefaultCipherArchiveService } from "./default-cipher-archive.service"; import { DefaultCipherArchiveService } from "./default-cipher-archive.service";
import { PasswordRepromptService } from "./password-reprompt.service";
describe("DefaultCipherArchiveService", () => { describe("DefaultCipherArchiveService", () => {
let service: DefaultCipherArchiveService; let service: DefaultCipherArchiveService;
let mockCipherService: jest.Mocked<CipherService>; let mockCipherService: jest.Mocked<CipherService>;
let mockApiService: jest.Mocked<ApiService>; let mockApiService: jest.Mocked<ApiService>;
let mockDialogService: jest.Mocked<DialogService>;
let mockPasswordRepromptService: jest.Mocked<PasswordRepromptService>;
let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>; let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>;
let mockConfigService: jest.Mocked<ConfigService>; let mockConfigService: jest.Mocked<ConfigService>;
@@ -35,16 +28,12 @@ describe("DefaultCipherArchiveService", () => {
beforeEach(() => { beforeEach(() => {
mockCipherService = mock<CipherService>(); mockCipherService = mock<CipherService>();
mockApiService = mock<ApiService>(); mockApiService = mock<ApiService>();
mockDialogService = mock<DialogService>();
mockPasswordRepromptService = mock<PasswordRepromptService>();
mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>(); mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
mockConfigService = mock<ConfigService>(); mockConfigService = mock<ConfigService>();
service = new DefaultCipherArchiveService( service = new DefaultCipherArchiveService(
mockCipherService, mockCipherService,
mockApiService, mockApiService,
mockDialogService,
mockPasswordRepromptService,
mockBillingAccountProfileStateService, mockBillingAccountProfileStateService,
mockConfigService, mockConfigService,
); );
@@ -244,46 +233,4 @@ describe("DefaultCipherArchiveService", () => {
); );
}); });
}); });
describe("canInteract", () => {
let mockCipherView: CipherView;
beforeEach(() => {
mockCipherView = {
id: cipherId,
decryptionFailure: false,
} as unknown as CipherView;
});
it("should return false and open dialog when cipher has decryption failure", async () => {
mockCipherView.decryptionFailure = true;
const openSpy = jest.spyOn(DecryptionFailureDialogComponent, "open").mockImplementation();
const result = await service.canInteract(mockCipherView);
expect(result).toBe(false);
expect(openSpy).toHaveBeenCalledWith(mockDialogService, {
cipherIds: [cipherId],
});
});
it("should return password reprompt result when no decryption failure", async () => {
mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(true);
const result = await service.canInteract(mockCipherView);
expect(result).toBe(true);
expect(mockPasswordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(
mockCipherView,
);
});
it("should return false when password reprompt fails", async () => {
mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(false);
const result = await service.canInteract(mockCipherView);
expect(result).toBe(false);
});
});
}); });

View File

@@ -12,27 +12,21 @@ import {
CipherBulkUnarchiveRequest, CipherBulkUnarchiveRequest,
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request"; } from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response"; import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import {
CipherViewLike, CipherViewLike,
CipherViewLikeUtils, CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils"; } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { DialogService } from "@bitwarden/components";
import { CipherArchiveService } from "../abstractions/cipher-archive.service"; import { CipherArchiveService } from "../abstractions/cipher-archive.service";
import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component";
import { PasswordRepromptService } from "./password-reprompt.service";
export class DefaultCipherArchiveService implements CipherArchiveService { export class DefaultCipherArchiveService implements CipherArchiveService {
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private apiService: ApiService, private apiService: ApiService,
private dialogService: DialogService,
private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService, private configService: ConfigService,
) {} ) {}
/** /**
* Observable that contains the list of ciphers that have been archived. * Observable that contains the list of ciphers that have been archived.
*/ */
@@ -125,21 +119,4 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
await this.cipherService.replace(currentCiphers, userId); await this.cipherService.replace(currentCiphers, userId);
} }
/**
* Check if the user is able to interact with the cipher
* (password re-prompt / decryption failure checks).
* @param cipher
* @private
*/
async canInteract(cipher: CipherView) {
if (cipher.decryptionFailure) {
DecryptionFailureDialogComponent.open(this.dialogService, {
cipherIds: [cipher.id as CipherId],
});
return false;
}
return await this.passwordRepromptService.passwordRepromptCheck(cipher);
}
} }

View File

@@ -296,12 +296,20 @@ export class SearchService implements SearchServiceAbstraction {
return results; return results;
} }
searchCiphersBasic<C extends CipherViewLike>(ciphers: C[], query: string, deleted = false) { searchCiphersBasic<C extends CipherViewLike>(
ciphers: C[],
query: string,
deleted = false,
archived = false,
) {
query = SearchService.normalizeSearchQuery(query.trim().toLowerCase()); query = SearchService.normalizeSearchQuery(query.trim().toLowerCase());
return ciphers.filter((c) => { return ciphers.filter((c) => {
if (deleted !== CipherViewLikeUtils.isDeleted(c)) { if (deleted !== CipherViewLikeUtils.isDeleted(c)) {
return false; return false;
} }
if (archived !== CipherViewLikeUtils.isArchived(c)) {
return false;
}
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
return true; return true;
} }

View File

@@ -27,5 +27,3 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service";
export * from "./abstractions/change-login-password.service"; export * from "./abstractions/change-login-password.service";
export * from "./services/default-change-login-password.service"; export * from "./services/default-change-login-password.service";
export * from "./abstractions/cipher-archive.service";
export * from "./services/default-cipher-archive.service";