1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-18089] Update cipher permissions model and consumers (#13606)

* update cipher permissions model and consumers

* add new property to tests

* fix test, add property to toCipherData()

* add missing ConfigService

* fix story

* refactor

* fix error, cleanup

* revert refactor

* refactor

* remove uneeded test

* cleanup

* fix build error

* refactor

* clean up

* add tests

* move validation check to after featrue flagged logic

* iterate on feedback

* feedback
This commit is contained in:
Brandon Treston
2025-03-14 09:51:40 -04:00
committed by GitHub
parent b73e6cf2fe
commit 4d68952ef3
18 changed files with 372 additions and 23 deletions

View File

@@ -1281,6 +1281,7 @@ export default class MainBackground {
this.collectionService,
this.organizationService,
this.accountService,
this.configService,
);
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();

View File

@@ -845,6 +845,7 @@ export class ServiceContainer {
this.collectionService,
this.organizationService,
this.accountService,
this.configService,
);
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService);

View File

@@ -15,6 +15,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
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";
@@ -231,7 +233,10 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
* A user may restore items if they have delete permissions and the item is in the trash.
*/
protected async canUserRestore() {
return this.isTrashFilter && this.cipher?.isDeleted && this.canDelete;
const featureFlagEnabled = await firstValueFrom(this.limitItemDeletion$);
return this.isTrashFilter && this.cipher?.isDeleted && featureFlagEnabled
? this.cipher?.permissions.restore
: this.canDelete;
}
protected showRestore: boolean;
@@ -277,6 +282,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
protected canDelete = false;
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
constructor(
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
private dialogRef: DialogRef<VaultItemDialogResult>,
@@ -294,6 +301,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
private apiService: ApiService,
private eventCollectionService: EventCollectionService,
private routedVaultFilterService: RoutedVaultFilterService,
private configService: ConfigService,
) {
this.updateTitle();
}

View File

@@ -86,7 +86,12 @@
appStopProp
></button>
<bit-menu #corruptedCipherOptions>
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
<button
bitMenuItem
*ngIf="(limitItemDeletion$ | async) ? canDeleteCipher : 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 }}
@@ -151,11 +156,21 @@
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</button>
<button bitMenuItem (click)="restore()" type="button" *ngIf="cipher.isDeleted">
<button
bitMenuItem
(click)="restore()"
type="button"
*ngIf="(limitItemDeletion$ | async) ? cipher.isDeleted && canRestoreCipher : cipher.isDeleted"
>
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restore" | i18n }}
</button>
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
<button
bitMenuItem
*ngIf="(limitItemDeletion$ | async) ? canDeleteCipher : 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 }}

View File

@@ -4,6 +4,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -36,12 +38,21 @@ export class VaultCipherRowComponent implements OnInit {
@Input() canEditCipher: boolean;
@Input() canAssignCollections: boolean;
@Input() canManageCollection: boolean;
/**
* uses new permission delete logic from PM-15493
*/
@Input() canDeleteCipher: boolean;
/**
* uses new permission restore logic from PM-15493
*/
@Input() canRestoreCipher: boolean;
@Output() onEvent = new EventEmitter<VaultItemEvent>();
@Input() checked: boolean;
@Output() checkedToggled = new EventEmitter<void>();
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
protected CipherType = CipherType;
private permissionList = getPermissionList();
private permissionPriority = [
@@ -53,7 +64,10 @@ export class VaultCipherRowComponent implements OnInit {
];
protected organization?: Organization;
constructor(private i18nService: I18nService) {}
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {}
/**
* Lifecycle hook for component initialization.

View File

@@ -86,11 +86,23 @@
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "assignToCollections" | i18n }}
</button>
<button *ngIf="showBulkTrashOptions" type="button" bitMenuItem (click)="bulkRestore()">
<button
*ngIf="
(limitItemDeletion$ | async) ? (canRestoreSelected$ | async) : showBulkTrashOptions
"
type="button"
bitMenuItem
(click)="bulkRestore()"
>
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restoreSelected" | i18n }}
</button>
<button *ngIf="showDelete" type="button" bitMenuItem (click)="bulkDelete()">
<button
*ngIf="(limitItemDeletion$ | async) ? (canDeleteSelected$ | async) : 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 }}
@@ -146,6 +158,16 @@
[canEditCipher]="canEditCipher(item.cipher)"
[canAssignCollections]="canAssignCollections(item.cipher)"
[canManageCollection]="canManageCollection(item.cipher)"
[canDeleteCipher]="
cipherAuthorizationService.canDeleteCipher$(
item.cipher,
[item.cipher.collectionId],
showAdminActions
) | async
"
[canRestoreCipher]="
cipherAuthorizationService.canRestoreCipher$(item.cipher, showAdminActions) | async
"
(checkedToggled)="selection.toggle(item)"
(onEvent)="event($event)"
></tr>

View File

@@ -2,10 +2,14 @@
// @ts-strict-ignore
import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { SortDirection, TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core";
@@ -75,9 +79,67 @@ export class VaultItemsComponent {
@Output() onEvent = new EventEmitter<VaultItemEvent>();
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
protected editableItems: VaultItem[] = [];
protected dataSource = new TableDataSource<VaultItem>();
protected selection = new SelectionModel<VaultItem>(true, [], true);
protected canDeleteSelected$: Observable<boolean>;
protected canRestoreSelected$: Observable<boolean>;
constructor(
protected cipherAuthorizationService: CipherAuthorizationService,
private configService: ConfigService,
) {
this.canDeleteSelected$ = this.selection.changed.pipe(
startWith(null),
switchMap(() => {
const ciphers = this.selection.selected
.filter((item) => item.cipher)
.map((item) => item.cipher);
if (this.selection.selected.length === 0) {
return of(true);
}
const canDeleteCiphers$ = ciphers.map((c) =>
cipherAuthorizationService.canDeleteCipher$(c, [], this.showAdminActions),
);
const canDeleteCollections = this.selection.selected
.filter((item) => item.collection)
.every((item) => item.collection && this.canDeleteCollection(item.collection));
const canDelete$ = combineLatest(canDeleteCiphers$).pipe(
map((results) => results.every((item) => item) && canDeleteCollections),
);
return canDelete$;
}),
);
this.canRestoreSelected$ = this.selection.changed.pipe(
startWith(null),
switchMap(() => {
const ciphers = this.selection.selected
.filter((item) => item.cipher)
.map((item) => item.cipher);
if (this.selection.selected.length === 0) {
return of(true);
}
const canRestoreCiphers$ = ciphers.map((c) =>
cipherAuthorizationService.canRestoreCipher$(c, this.showAdminActions),
);
const canRestore$ = combineLatest(canRestoreCiphers$).pipe(
map((results) => results.every((item) => item)),
);
return canRestore$;
}),
);
}
get showExtraColumn() {
return this.showCollections || this.showGroups || this.showOwner;
@@ -99,6 +161,7 @@ export class VaultItemsComponent {
);
}
//@TODO: remove this function when removing the limitItemDeletion$ feature flag.
get showDelete(): boolean {
if (this.selection.selected.length === 0) {
return true;

View File

@@ -28,6 +28,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";
@@ -104,12 +105,20 @@ export default {
{
provide: ConfigService,
useValue: {
getFeatureFlag() {
getFeatureFlag$() {
// does not currently affect any display logic, default all to OFF
return false;
},
},
},
{
provide: CipherAuthorizationService,
useValue: {
canDeleteCipher$() {
return of(true);
},
},
},
],
}),
applicationConfig({