mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-01 23:01:28 +01:00
[PM-12281] [PM-12301] [PM-12306] [PM-12334] Move delete item permission to Can Manage (#11289)
* Added inputs to the view and edit component to disable or remove the delete button when a user does not have manage rights * Refactored editByCipherId to receive cipherview object * Fixed issue where adding an item on the individual vault throws a null reference * Fixed issue where adding an item on the AC vault throws a null reference * Allow delete in unassigned collection * created reusable service to check if a user has delete permission on an item * Registered service * Used authorizationservice on the browser and desktop Only display the delete button when a user has delete permission * Added comments to the service * Passed active collectionId to add edit component renamed constructor parameter * restored input property used by the web * Fixed dependency issue * Fixed dependency issue * Fixed dependency issue * Modified service to cater for org vault * Updated to include new dependency * Updated components to use the observable * Added check on the cli to know if user has rights to delete an item * Renamed abstraction and renamed implementation to include Default Fixed permission issues * Fixed test to reflect changes in implementation * Modified base classes to use new naming Passed new parameters for the canDeleteCipher * Modified base classes to use new naming Made changes from base class * Desktop changes Updated reference naming * cli changes Updated reference naming Passed new parameters for the canDeleteCipher$ * Updated references * browser changes Updated reference naming Passed new parameters for the canDeleteCipher$ * Modified cipher form dialog to take in active collection id used canDeleteCipher$ on the vault item dialog to disable the delete button when user does not have the required permissions * Fix number of arguments issue * Added active collection id * Updated canDeleteCipher$ arguments * Updated to pass the cipher object * Fixed up refrences and comments * Updated dependency * updated check to canEditUnassignedCiphers * Fixed unit tests * Removed activeCollectionId from cipher form * Fixed issue where bulk delete option shows for can edit users * Fix null reference when checking if a cipher belongs to the unassigned collection * Fixed bug where allowedCollection passed is undefined * Modified cipher by adding a isAdminConsoleAction argument to tell when a reuqest comes from the admin console * Passed isAdminConsoleAction as true when request is from the admin console
This commit is contained in:
parent
470ddf79ab
commit
4a30782939
@ -177,6 +177,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.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";
|
||||
@ -369,6 +373,7 @@ export default class MainBackground {
|
||||
themeStateService: DefaultThemeStateService;
|
||||
autoSubmitLoginBackground: AutoSubmitLoginBackground;
|
||||
sdkService: SdkService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
@ -1265,6 +1270,11 @@ export default class MainBackground {
|
||||
}
|
||||
|
||||
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
||||
|
||||
this.cipherAuthorizationService = new DefaultCipherAuthorizationService(
|
||||
this.collectionService,
|
||||
this.organizationService,
|
||||
);
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
<button
|
||||
slot="end"
|
||||
*ngIf="cipher.edit"
|
||||
*ngIf="canDeleteCipher$ | async"
|
||||
[bitAction]="delete"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
|
@ -15,6 +15,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
@ -81,6 +82,12 @@ describe("ViewV2Component", () => {
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
{
|
||||
provide: CipherAuthorizationService,
|
||||
useValue: {
|
||||
canDeleteCipher$: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@ -68,6 +69,7 @@ export class ViewV2Component {
|
||||
cipher: CipherView;
|
||||
organization$: Observable<Organization>;
|
||||
folder$: Observable<FolderView>;
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
collections$: Observable<CollectionView[]>;
|
||||
loadAction: typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON;
|
||||
|
||||
@ -83,6 +85,7 @@ export class ViewV2Component {
|
||||
private accountService: AccountService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private popupRouterCacheService: PopupRouterCacheService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
@ -101,6 +104,8 @@ export class ViewV2Component {
|
||||
await this.vaultPopupAutofillService.doAutofill(this.cipher);
|
||||
}
|
||||
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(cipher);
|
||||
|
||||
await this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientViewed,
|
||||
cipher.id,
|
||||
|
@ -779,7 +779,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="editMode && !cloneMode && !(!cipher.edit && editMode)">
|
||||
<div class="box list" *ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -24,6 +24,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@ -72,6 +73,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -92,6 +94,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,6 +110,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
this.folderId = params.folderId;
|
||||
}
|
||||
if (params.collectionId) {
|
||||
this.collectionId = params.collectionId;
|
||||
const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
|
||||
if (collection != null) {
|
||||
this.collectionIds = [collection.id];
|
||||
|
@ -198,7 +198,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: cipher.id },
|
||||
queryParams: { cipherId: cipher.id, collectionId: this.collectionId },
|
||||
});
|
||||
}
|
||||
this.preventSelected = false;
|
||||
|
@ -644,7 +644,7 @@
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
*ngIf="cipher.edit"
|
||||
*ngIf="canDeleteCipher$ | async"
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
|
@ -26,6 +26,7 @@ import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/a
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@ -102,6 +103,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
datePipe: DatePipe,
|
||||
accountService: AccountService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -127,6 +129,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
datePipe,
|
||||
accountService,
|
||||
billingAccountProfileStateService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
@ -143,7 +146,13 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (params.collectionId) {
|
||||
this.collectionId = params.collectionId;
|
||||
}
|
||||
|
||||
if (!params.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.close();
|
||||
@ -197,7 +206,12 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/edit-cipher"], {
|
||||
queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false },
|
||||
queryParams: {
|
||||
cipherId: this.cipher.id,
|
||||
type: this.cipher.type,
|
||||
isNew: false,
|
||||
collectionId: this.collectionId,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.apiService,
|
||||
this.serviceContainer.folderApiService,
|
||||
this.serviceContainer.billingAccountProfileStateService,
|
||||
this.serviceContainer.cipherAuthorizationService,
|
||||
);
|
||||
this.confirmCommand = new ConfirmCommand(
|
||||
this.serviceContainer.apiService,
|
||||
|
@ -129,6 +129,10 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.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";
|
||||
@ -255,6 +259,7 @@ export class ServiceContainer {
|
||||
kdfConfigService: KdfConfigServiceAbstraction;
|
||||
taskSchedulerService: TaskSchedulerService;
|
||||
sdkService: SdkService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@ -805,6 +810,11 @@ export class ServiceContainer {
|
||||
this.apiService,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.cipherAuthorizationService = new DefaultCipherAuthorizationService(
|
||||
this.collectionService,
|
||||
this.organizationService,
|
||||
);
|
||||
}
|
||||
|
||||
async logout() {
|
||||
|
@ -319,6 +319,7 @@ export class VaultProgram extends BaseProgram {
|
||||
this.serviceContainer.apiService,
|
||||
this.serviceContainer.folderApiService,
|
||||
this.serviceContainer.billingAccountProfileStateService,
|
||||
this.serviceContainer.cipherAuthorizationService,
|
||||
);
|
||||
const response = await command.run(object, id, cmd);
|
||||
this.processResponse(response);
|
||||
|
@ -6,6 +6,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { CliUtils } from "../utils";
|
||||
@ -17,6 +18,7 @@ export class DeleteCommand {
|
||||
private apiService: ApiService,
|
||||
private folderApiService: FolderApiServiceAbstraction,
|
||||
private accountProfileService: BillingAccountProfileStateService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {}
|
||||
|
||||
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
|
||||
@ -45,6 +47,14 @@ export class DeleteCommand {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
const canDeleteCipher = await firstValueFrom(
|
||||
this.cipherAuthorizationService.canDeleteCipher$(cipher),
|
||||
);
|
||||
|
||||
if (!canDeleteCipher) {
|
||||
return Response.error("You do not have permission to delete this item.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (options.permanent) {
|
||||
await this.cipherService.deleteWithServer(id);
|
||||
|
@ -710,7 +710,7 @@
|
||||
(click)="delete()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
*ngIf="editMode && !cloneMode"
|
||||
*ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
[appApiAction]="deletePromise"
|
||||
>
|
||||
|
@ -18,6 +18,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@ -50,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -70,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
class="details"
|
||||
*ngIf="cipherId && action === 'view'"
|
||||
[cipherId]="cipherId"
|
||||
[collectionId]="activeFilter?.selectedCollectionId"
|
||||
(onCloneCipher)="cloneCipherWithoutPasswordPrompt($event)"
|
||||
(onEditCipher)="editCipherWithoutPasswordPrompt($event)"
|
||||
(onViewCipherPasswordHistory)="viewCipherPasswordHistory($event)"
|
||||
@ -29,6 +30,7 @@
|
||||
[folderId]="action === 'add' && folderId !== 'none' ? folderId : null"
|
||||
[organizationId]="action === 'add' ? addOrganizationId : null"
|
||||
[collectionIds]="action === 'add' ? addCollectionIds : null"
|
||||
[collectionId]="activeFilter?.selectedCollectionId"
|
||||
[type]="action === 'add' ? (addType ? addType : type) : null"
|
||||
[cipherId]="action === 'edit' || action === 'clone' ? cipherId : null"
|
||||
(onSavedCipher)="savedCipher($event)"
|
||||
|
@ -566,7 +566,7 @@
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="right" *ngIf="cipher.edit">
|
||||
<div class="right" *ngIf="canDeleteCipher$ | async">
|
||||
<button
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
|
@ -30,6 +30,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@ -66,6 +67,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
datePipe: DatePipe,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -91,6 +93,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
datePipe,
|
||||
accountService,
|
||||
billingAccountProfileStateService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
ngOnInit() {
|
||||
|
@ -18,6 +18,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
@ -54,6 +55,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -76,6 +78,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
|
||||
datePipe,
|
||||
configService,
|
||||
billingAccountProfileStateService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@
|
||||
buttonType="danger"
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
[disabled]="!canDelete"
|
||||
[disabled]="!(canDeleteCipher$ | async)"
|
||||
data-testid="delete-cipher-btn"
|
||||
></button>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
import { firstValueFrom, Observable, Subject } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
@ -12,12 +12,13 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@ -63,6 +64,16 @@ export interface VaultItemDialogParams {
|
||||
* If true, the "edit" button will be disabled in the dialog.
|
||||
*/
|
||||
disableForm?: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the active collection. This is know the collection filter selected by the user.
|
||||
*/
|
||||
activeCollectionId?: CollectionId;
|
||||
|
||||
/**
|
||||
* If true, the dialog is being opened from the admin console.
|
||||
*/
|
||||
isAdminConsoleAction?: boolean;
|
||||
}
|
||||
|
||||
export enum VaultItemDialogResult {
|
||||
@ -204,6 +215,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected formConfig: CipherFormConfig = this.params.formConfig;
|
||||
|
||||
protected canDeleteCipher$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
|
||||
private dialogRef: DialogRef<VaultItemDialogResult>,
|
||||
@ -217,6 +230,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
private router: Router,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private premiumUpgradeService: PremiumUpgradePromptService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
this.updateTitle();
|
||||
}
|
||||
@ -231,6 +245,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
this.organization = this.formConfig.organizations.find(
|
||||
(o) => o.id === this.cipher.organizationId,
|
||||
);
|
||||
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
|
||||
this.cipher,
|
||||
[this.params.activeCollectionId],
|
||||
this.params.isAdminConsoleAction,
|
||||
);
|
||||
}
|
||||
|
||||
this.performingInitialLoad = false;
|
||||
|
@ -132,7 +132,7 @@
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem *ngIf="canEditCipher" (click)="deleteCipher()" type="button">
|
||||
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
|
@ -35,6 +35,7 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
@Input() collections: CollectionView[];
|
||||
@Input() viewingOrgVault: boolean;
|
||||
@Input() canEditCipher: boolean;
|
||||
@Input() canManageCollection: boolean;
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
|
||||
|
@ -64,12 +64,7 @@
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restoreSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showDelete() || showBulkTrashOptions"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkDelete()"
|
||||
>
|
||||
<button *ngIf="showDelete" type="button" bitMenuItem (click)="bulkDelete()">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (showBulkTrashOptions ? "permanentlyDeleteSelected" : "delete") | i18n }}
|
||||
@ -123,6 +118,7 @@
|
||||
[collections]="allCollections"
|
||||
[checked]="selection.isSelected(item)"
|
||||
[canEditCipher]="canEditCipher(item.cipher)"
|
||||
[canManageCollection]="canManageCollection(item.cipher)"
|
||||
(checkedToggled)="selection.toggle(item)"
|
||||
(onEvent)="event($event)"
|
||||
></tr>
|
||||
|
@ -45,6 +45,7 @@ export class VaultItemsComponent {
|
||||
@Input() viewingOrgVault: boolean;
|
||||
@Input() addAccessStatus: number;
|
||||
@Input() addAccessToggle: boolean;
|
||||
@Input() activeCollection: CollectionView | undefined;
|
||||
|
||||
private _ciphers?: CipherView[] = [];
|
||||
@Input() get ciphers(): CipherView[] {
|
||||
@ -90,11 +91,39 @@ export class VaultItemsComponent {
|
||||
);
|
||||
}
|
||||
|
||||
get showDelete(): boolean {
|
||||
if (this.selection.selected.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasPersonalItems = this.hasPersonalItems();
|
||||
const uniqueCipherOrgIds = this.getUniqueOrganizationIds();
|
||||
|
||||
const canManageCollectionCiphers = this.selection.selected
|
||||
.filter((item) => item.cipher)
|
||||
.every(({ cipher }) => this.canManageCollection(cipher));
|
||||
|
||||
const canDeleteCollections = this.selection.selected
|
||||
.filter((item) => item.collection)
|
||||
.every((item) => item.collection && this.canDeleteCollection(item.collection));
|
||||
|
||||
const userCanDeleteAccess = canManageCollectionCiphers && canDeleteCollections;
|
||||
|
||||
if (
|
||||
userCanDeleteAccess ||
|
||||
(hasPersonalItems && (!uniqueCipherOrgIds.size || userCanDeleteAccess))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get disableMenu() {
|
||||
return (
|
||||
!this.bulkMoveAllowed &&
|
||||
!this.showAssignToCollections() &&
|
||||
!this.showDelete() &&
|
||||
!this.showDelete &&
|
||||
!this.showBulkEditCollectionAccess
|
||||
);
|
||||
}
|
||||
@ -198,6 +227,37 @@ export class VaultItemsComponent {
|
||||
return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit;
|
||||
}
|
||||
|
||||
protected canManageCollection(cipher: CipherView) {
|
||||
// If the cipher is not part of an organization (personal item), user can manage it
|
||||
if (cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for admin access in AC vault
|
||||
if (this.showAdminActions) {
|
||||
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
|
||||
// If the user is an admin, they can delete an unassigned cipher
|
||||
if (cipher.collectionIds.length === 0) {
|
||||
return organization?.canEditUnmanagedCollections === true;
|
||||
}
|
||||
|
||||
if (
|
||||
organization?.permissions.editAnyCollection ||
|
||||
(organization?.allowAdminAccessToAllCollectionItems && organization.isAdmin)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.activeCollection) {
|
||||
return this.activeCollection.manage === true;
|
||||
}
|
||||
|
||||
return this.allCollections
|
||||
.filter((c) => cipher.collectionIds.includes(c.id))
|
||||
.some((collection) => collection.manage);
|
||||
}
|
||||
|
||||
private refreshItems() {
|
||||
const collections: VaultItem[] = this.collections.map((collection) => ({ collection }));
|
||||
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
|
||||
@ -267,37 +327,6 @@ export class VaultItemsComponent {
|
||||
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
|
||||
}
|
||||
|
||||
protected showDelete(): boolean {
|
||||
if (this.selection.selected.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasPersonalItems = this.hasPersonalItems();
|
||||
const uniqueCipherOrgIds = this.getUniqueOrganizationIds();
|
||||
const organizations = Array.from(uniqueCipherOrgIds, (orgId) =>
|
||||
this.allOrganizations.find((o) => o.id === orgId),
|
||||
);
|
||||
|
||||
const canEditOrManageAllCiphers =
|
||||
organizations.length > 0 && organizations.every((org) => org?.canEditAllCiphers);
|
||||
|
||||
const canDeleteCollections = this.selection.selected
|
||||
.filter((item) => item.collection)
|
||||
.every((item) => item.collection && this.canDeleteCollection(item.collection));
|
||||
|
||||
const userCanDeleteAccess =
|
||||
(canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && canDeleteCollections;
|
||||
|
||||
if (
|
||||
userCanDeleteAccess ||
|
||||
(hasPersonalItems && (!uniqueCipherOrgIds.size || userCanDeleteAccess))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private hasPersonalItems(): boolean {
|
||||
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
|
||||
}
|
||||
|
@ -995,7 +995,7 @@
|
||||
(click)="delete()"
|
||||
class="btn btn-outline-danger"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
*ngIf="editMode && !cloneMode && !(!cipher.edit && editMode)"
|
||||
*ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
[appApiAction]="deletePromise"
|
||||
>
|
||||
|
@ -25,6 +25,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
@ -71,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -91,6 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@ -173,6 +173,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||
protected canCreateCollections = false;
|
||||
protected currentSearchText$: Observable<string>;
|
||||
private activeUserId: UserId;
|
||||
private searchText$ = new Subject<string>();
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
@ -219,6 +220,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
: "trashCleanupWarning",
|
||||
);
|
||||
|
||||
this.activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const firstSetup$ = this.route.queryParams.pipe(
|
||||
first(),
|
||||
switchMap(async (params: Params) => {
|
||||
@ -603,11 +608,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
* Open the combined view / edit dialog for a cipher.
|
||||
* @param mode - Starting mode of the dialog.
|
||||
* @param formConfig - Configuration for the form when editing/adding a cipher.
|
||||
* @param activeCollectionId - The active collection ID.
|
||||
*/
|
||||
async openVaultItemDialog(mode: VaultItemDialogMode, formConfig: CipherFormConfig) {
|
||||
async openVaultItemDialog(
|
||||
mode: VaultItemDialogMode,
|
||||
formConfig: CipherFormConfig,
|
||||
activeCollectionId?: CollectionId,
|
||||
) {
|
||||
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
|
||||
mode,
|
||||
formConfig,
|
||||
activeCollectionId,
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
|
||||
@ -713,6 +724,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.cipherAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = id;
|
||||
comp.collectionId = this.selectedCollection?.node.id;
|
||||
|
||||
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
modal.close();
|
||||
this.refresh();
|
||||
@ -787,7 +800,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
cipher.type,
|
||||
);
|
||||
|
||||
await this.openVaultItemDialog("view", cipherFormConfig);
|
||||
await this.openVaultItemDialog(
|
||||
"view",
|
||||
cipherFormConfig,
|
||||
this.selectedCollection?.node.id as CollectionId,
|
||||
);
|
||||
}
|
||||
|
||||
async addCollection() {
|
||||
|
@ -22,7 +22,7 @@
|
||||
buttonType="danger"
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
[disabled]="!cipher.edit"
|
||||
[disabled]="!(canDeleteCipher$ | async)"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
@ -14,6 +14,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component";
|
||||
@ -62,6 +63,12 @@ describe("ViewComponent", () => {
|
||||
useValue: mock<BillingAccountProfileStateService>(),
|
||||
},
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{
|
||||
provide: CipherAuthorizationService,
|
||||
useValue: {
|
||||
canDeleteCipher$: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@ -8,10 +9,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
DialogModule,
|
||||
@ -34,6 +37,11 @@ export interface ViewCipherDialogParams {
|
||||
*/
|
||||
collections?: CollectionView[];
|
||||
|
||||
/**
|
||||
* Optional collection ID used to know the collection filter selected.
|
||||
*/
|
||||
activeCollectionId?: CollectionId;
|
||||
|
||||
/**
|
||||
* If true, the edit button will be disabled in the dialog.
|
||||
*/
|
||||
@ -71,6 +79,8 @@ export class ViewComponent implements OnInit {
|
||||
cipherTypeString: string;
|
||||
organization: Organization;
|
||||
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) public params: ViewCipherDialogParams,
|
||||
private dialogRef: DialogRef<ViewCipherDialogCloseResult>,
|
||||
@ -81,6 +91,7 @@ export class ViewComponent implements OnInit {
|
||||
private cipherService: CipherService,
|
||||
private toastService: ToastService,
|
||||
private organizationService: OrganizationService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -93,6 +104,10 @@ export class ViewComponent implements OnInit {
|
||||
if (this.cipher.organizationId) {
|
||||
this.organization = await this.organizationService.get(this.cipher.organizationId);
|
||||
}
|
||||
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||
this.params.activeCollectionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,6 +21,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
@ -57,6 +58,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@ -79,6 +81,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
datePipe,
|
||||
configService,
|
||||
billingAccountProfileStateService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
@ -90,6 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
this.isAdminConsoleAction = true;
|
||||
// Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin
|
||||
const firstCipherCheck = await super.loadCipher();
|
||||
|
||||
|
@ -70,6 +70,7 @@
|
||||
[viewingOrgVault]="true"
|
||||
[addAccessStatus]="addAccessStatus$ | async"
|
||||
[addAccessToggle]="showAddAccessToggle"
|
||||
[activeCollection]="selectedCollection?.node"
|
||||
>
|
||||
</app-vault-items>
|
||||
<ng-container *ngIf="!performingInitialLoad && isEmpty">
|
||||
|
@ -828,6 +828,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
comp.organization = this.organization;
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.cipherId = cipher?.id;
|
||||
comp.collectionId = this.activeFilter.collectionId;
|
||||
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
modal.close();
|
||||
this.refresh();
|
||||
@ -897,7 +898,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
cipher.type,
|
||||
);
|
||||
|
||||
await this.openVaultItemDialog("view", cipherFormConfig, cipher);
|
||||
await this.openVaultItemDialog(
|
||||
"view",
|
||||
cipherFormConfig,
|
||||
cipher,
|
||||
this.activeFilter.collectionId as CollectionId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -907,6 +913,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
mode: VaultItemDialogMode,
|
||||
formConfig: CipherFormConfig,
|
||||
cipher?: CipherView,
|
||||
activeCollectionId?: CollectionId,
|
||||
) {
|
||||
const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false;
|
||||
// If the form is disabled, force the mode into `view`
|
||||
@ -915,6 +922,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
mode: dialogMode,
|
||||
formConfig,
|
||||
disableForm,
|
||||
activeCollectionId,
|
||||
isAdminConsoleAction: true,
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
|
||||
|
@ -242,6 +242,10 @@ import {
|
||||
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.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";
|
||||
@ -1340,6 +1344,11 @@ const safeProviders: SafeProvider[] = [
|
||||
ApiServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherAuthorizationService,
|
||||
useClass: DefaultCipherAuthorizationService,
|
||||
deps: [CollectionService, OrganizationServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -23,7 +23,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||
@ -36,6 +36,7 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@ -47,6 +48,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@Input() type: CipherType;
|
||||
@Input() collectionIds: string[];
|
||||
@Input() organizationId: string = null;
|
||||
@Input() collectionId: string = null;
|
||||
@Output() onSavedCipher = new EventEmitter<CipherView>();
|
||||
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
||||
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
||||
@ -57,6 +59,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@Output() onGeneratePassword = new EventEmitter();
|
||||
@Output() onGenerateUsername = new EventEmitter();
|
||||
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
|
||||
editMode = false;
|
||||
cipher: CipherView;
|
||||
folders$: Observable<FolderView[]>;
|
||||
@ -83,6 +87,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
reprompt = false;
|
||||
canUseReprompt = true;
|
||||
organization: Organization;
|
||||
/**
|
||||
* Flag to determine if the action is being performed from the admin console.
|
||||
*/
|
||||
isAdminConsoleAction: boolean = false;
|
||||
|
||||
protected componentName = "";
|
||||
protected destroy$ = new Subject<void>();
|
||||
@ -118,6 +126,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected win: Window,
|
||||
protected datePipe: DatePipe,
|
||||
protected configService: ConfigService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
@ -314,6 +323,12 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
if (this.reprompt) {
|
||||
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
|
||||
}
|
||||
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
|
||||
this.cipher,
|
||||
[this.collectionId as CollectionId],
|
||||
this.isAdminConsoleAction,
|
||||
);
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
@ -28,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@ -37,6 +38,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@ -45,12 +47,14 @@ const BroadcasterSubscriptionId = "ViewComponent";
|
||||
@Directive()
|
||||
export class ViewComponent implements OnDestroy, OnInit {
|
||||
@Input() cipherId: string;
|
||||
@Input() collectionId: string;
|
||||
@Output() onEditCipher = new EventEmitter<CipherView>();
|
||||
@Output() onCloneCipher = new EventEmitter<CipherView>();
|
||||
@Output() onShareCipher = new EventEmitter<CipherView>();
|
||||
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
||||
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
||||
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
cipher: CipherView;
|
||||
showPassword: boolean;
|
||||
showPasswordCount: boolean;
|
||||
@ -105,6 +109,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
protected datePipe: DatePipe,
|
||||
protected accountService: AccountService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@ -144,6 +149,9 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
);
|
||||
this.showPremiumRequiredTotp =
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||
this.collectionId as CollectionId,
|
||||
]);
|
||||
|
||||
if (this.cipher.folderId) {
|
||||
this.folder = await (
|
||||
|
@ -0,0 +1,200 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "./cipher-authorization.service";
|
||||
|
||||
describe("CipherAuthorizationService", () => {
|
||||
let cipherAuthorizationService: CipherAuthorizationService;
|
||||
|
||||
const mockCollectionService = mock<CollectionService>();
|
||||
const mockOrganizationService = mock<OrganizationService>();
|
||||
|
||||
// Mock factories
|
||||
const createMockCipher = (
|
||||
organizationId: string | null,
|
||||
collectionIds: string[],
|
||||
edit: boolean = true,
|
||||
) => ({
|
||||
organizationId,
|
||||
collectionIds,
|
||||
edit,
|
||||
});
|
||||
|
||||
const createMockCollection = (id: string, manage: boolean) => ({
|
||||
id,
|
||||
manage,
|
||||
});
|
||||
|
||||
const createMockOrganization = ({
|
||||
allowAdminAccessToAllCollectionItems = false,
|
||||
canEditAllCiphers = false,
|
||||
canEditUnassignedCiphers = false,
|
||||
} = {}) => ({
|
||||
allowAdminAccessToAllCollectionItems,
|
||||
canEditAllCiphers,
|
||||
canEditUnassignedCiphers,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
cipherAuthorizationService = new DefaultCipherAuthorizationService(
|
||||
mockCollectionService,
|
||||
mockOrganizationService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("canDeleteCipher$", () => {
|
||||
it("should return true if cipher has no organizationId", (done) => {
|
||||
const cipher = createMockCipher(null, []) as CipherView;
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => {
|
||||
const cipher = createMockCipher("org1", []) as CipherView;
|
||||
const organization = createMockOrganization({ canEditUnassignedCiphers: true });
|
||||
mockOrganizationService.get$.mockReturnValue(of(organization as Organization));
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => {
|
||||
const cipher = createMockCipher("org1", ["col1"]) as CipherView;
|
||||
const organization = createMockOrganization({ canEditAllCiphers: true });
|
||||
mockOrganizationService.get$.mockReturnValue(of(organization as Organization));
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => {
|
||||
const cipher = createMockCipher("org1", []) as CipherView;
|
||||
const organization = createMockOrganization({ canEditUnassignedCiphers: false });
|
||||
mockOrganizationService.get$.mockReturnValue(of(organization as Organization));
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if activeCollectionId is provided and has manage permission", (done) => {
|
||||
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
|
||||
const activeCollectionId = "col1" as CollectionId;
|
||||
const org = createMockOrganization();
|
||||
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
|
||||
|
||||
const allCollections = [
|
||||
createMockCollection("col1", true),
|
||||
createMockCollection("col2", false),
|
||||
];
|
||||
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
|
||||
of(allCollections as CollectionView[]),
|
||||
);
|
||||
|
||||
cipherAuthorizationService
|
||||
.canDeleteCipher$(cipher, [activeCollectionId])
|
||||
.subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
|
||||
"col1",
|
||||
"col2",
|
||||
] as CollectionId[]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if activeCollectionId is provided and manage permission is not present", (done) => {
|
||||
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
|
||||
const activeCollectionId = "col1" as CollectionId;
|
||||
const org = createMockOrganization();
|
||||
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
|
||||
|
||||
const allCollections = [
|
||||
createMockCollection("col1", false),
|
||||
createMockCollection("col2", true),
|
||||
];
|
||||
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
|
||||
of(allCollections as CollectionView[]),
|
||||
);
|
||||
|
||||
cipherAuthorizationService
|
||||
.canDeleteCipher$(cipher, [activeCollectionId])
|
||||
.subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
|
||||
"col1",
|
||||
"col2",
|
||||
] as CollectionId[]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if any collection has manage permission", (done) => {
|
||||
const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView;
|
||||
const org = createMockOrganization();
|
||||
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
|
||||
|
||||
const allCollections = [
|
||||
createMockCollection("col1", false),
|
||||
createMockCollection("col2", true),
|
||||
createMockCollection("col3", false),
|
||||
];
|
||||
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
|
||||
of(allCollections as CollectionView[]),
|
||||
);
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
|
||||
"col1",
|
||||
"col2",
|
||||
"col3",
|
||||
] as CollectionId[]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if no collection has manage permission", (done) => {
|
||||
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
|
||||
const org = createMockOrganization();
|
||||
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
|
||||
|
||||
const allCollections = [
|
||||
createMockCollection("col1", false),
|
||||
createMockCollection("col2", false),
|
||||
];
|
||||
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
|
||||
of(allCollections as CollectionView[]),
|
||||
);
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
|
||||
"col1",
|
||||
"col2",
|
||||
] as CollectionId[]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,86 @@
|
||||
import { map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
/**
|
||||
* Represents either a cipher or a cipher view.
|
||||
*/
|
||||
type CipherLike = Cipher | CipherView;
|
||||
|
||||
/**
|
||||
* Service for managing user cipher authorization.
|
||||
*/
|
||||
export abstract class CipherAuthorizationService {
|
||||
/**
|
||||
* Determines if the user can delete the specified cipher.
|
||||
*
|
||||
* @param {CipherLike} cipher - The cipher object to evaluate for deletion permissions.
|
||||
* @param {CollectionId[]} [allowedCollections] - Optional. The selected collection id from the vault filter.
|
||||
* @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console.
|
||||
*
|
||||
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can delete the cipher.
|
||||
*/
|
||||
canDeleteCipher$: (
|
||||
cipher: CipherLike,
|
||||
allowedCollections?: CollectionId[],
|
||||
isAdminConsoleAction?: boolean,
|
||||
) => Observable<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link CipherAuthorizationService}
|
||||
*/
|
||||
export class DefaultCipherAuthorizationService implements CipherAuthorizationService {
|
||||
constructor(
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@link CipherAuthorizationService.canDeleteCipher$}
|
||||
*/
|
||||
canDeleteCipher$(
|
||||
cipher: CipherLike,
|
||||
allowedCollections?: CollectionId[],
|
||||
isAdminConsoleAction?: boolean,
|
||||
): Observable<boolean> {
|
||||
if (cipher.organizationId == null) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return this.organizationService.get$(cipher.organizationId).pipe(
|
||||
switchMap((organization) => {
|
||||
if (isAdminConsoleAction) {
|
||||
// If the user is an admin, they can delete an unassigned cipher
|
||||
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
|
||||
return of(organization?.canEditUnassignedCiphers === true);
|
||||
}
|
||||
|
||||
if (organization?.canEditAllCiphers) {
|
||||
return of(true);
|
||||
}
|
||||
}
|
||||
|
||||
return this.collectionService
|
||||
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
|
||||
.pipe(
|
||||
map((allCollections) => {
|
||||
const shouldFilter = allowedCollections?.some(Boolean);
|
||||
|
||||
const collections = shouldFilter
|
||||
? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId))
|
||||
: allCollections;
|
||||
|
||||
return collections.some((collection) => collection.manage);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user