1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00

[PM-19746] Add new permission check to browser (#14075)

* add new permisssions check to browser

* add permission logic to view

* fix tests

* cleanup

* fix permissions model for CLI and desktop

* feedback
This commit is contained in:
Brandon Treston
2025-04-02 12:49:08 -04:00
committed by GitHub
parent 0d9794e968
commit 9b3c28fcea
11 changed files with 82 additions and 9 deletions

View File

@@ -5,7 +5,7 @@
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
<popup-footer slot="footer" *ngIf="showFooter()">
<popup-footer slot="footer" *ngIf="showFooter$ | async">
<button
*ngIf="!cipher.isDeleted"
buttonType="primary"
@@ -17,7 +17,11 @@
</button>
<button
*ngIf="cipher.isDeleted && cipher.edit"
*ngIf="
(limitItemDeletion$ | async)
? cipher.isDeleted && cipher.permissions.restore
: cipher.isDeleted && cipher.edit
"
buttonType="primary"
type="button"
bitButton

View File

@@ -1,7 +1,7 @@
import { ComponentFixture, fakeAsync, flush, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router";
import { mock } from "jest-mock-extended";
import { Subject } from "rxjs";
import { of, Subject } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -51,6 +51,7 @@ describe("ViewV2Component", () => {
const openSimpleDialog = jest.fn().mockResolvedValue(true);
const stop = jest.fn();
const showToast = jest.fn();
const getFeatureFlag$ = jest.fn().mockReturnValue(of(true));
const mockCipher = {
id: "122-333-444",
@@ -105,6 +106,7 @@ describe("ViewV2Component", () => {
{ provide: VaultPopupScrollPositionService, useValue: { stop } },
{ provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService },
{ provide: ToastService, useValue: { showToast } },
{ provide: ConfigService, useValue: { getFeatureFlag$ } },
{
provide: I18nService,
useValue: {

View File

@@ -5,7 +5,7 @@ import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, Observable, switchMap } from "rxjs";
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import { CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -21,6 +21,8 @@ import {
SHOW_AUTOFILL_BUTTON,
} from "@bitwarden/common/autofill/constants";
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 { UserId } from "@bitwarden/common/types/guid";
@@ -107,6 +109,9 @@ export class ViewV2Component {
loadAction: LoadAction;
senderTabId?: number;
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
protected showFooter$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private router: Router,
@@ -122,6 +127,7 @@ export class ViewV2Component {
protected cipherAuthorizationService: CipherAuthorizationService,
private copyCipherFieldService: CopyCipherFieldService,
private popupScrollPositionService: VaultPopupScrollPositionService,
private configService: ConfigService,
) {
this.subscribeToParams();
}
@@ -150,6 +156,19 @@ export class ViewV2Component {
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(cipher);
this.showFooter$ = this.limitItemDeletion$.pipe(
map((enabled) => {
if (enabled) {
return (
cipher &&
(!cipher.isDeleted ||
(cipher.isDeleted && (cipher.permissions.restore || cipher.permissions.delete)))
);
}
return this.showFooterLegacy();
}),
);
await this.eventCollectionService.collect(
EventType.Cipher_ClientViewed,
cipher.id,
@@ -247,7 +266,8 @@ export class ViewV2Component {
: this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId);
}
protected showFooter(): boolean {
//@TODO: remove this when the LimitItemDeletion feature flag is removed
protected showFooterLegacy(): boolean {
return (
this.cipher &&
(!this.cipher.isDeleted ||

View File

@@ -31,7 +31,14 @@
></i>
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>
<ng-container slot="end" *ngIf="cipher.edit && cipher.viewPassword">
<ng-container
slot="end"
*ngIf="
(limitItemDeletion$ | async)
? cipher.permissions.restore
: cipher.edit && cipher.viewPassword
"
>
<bit-item-action>
<button
type="button"

View File

@@ -8,6 +8,8 @@ import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId } from "@bitwarden/common/types/guid";
@@ -70,8 +72,11 @@ export class TrashListItemsContainerComponent {
private passwordRepromptService: PasswordRepromptService,
private accountService: AccountService,
private router: Router,
private configService: ConfigService,
) {}
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
/**
* The tooltip text for the organization icon for ciphers that belong to an organization.
*/

View File

@@ -1,8 +1,11 @@
import { firstValueFrom } from "rxjs";
import { combineLatest, firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { Response } from "../models/response";
@@ -10,6 +13,8 @@ export class RestoreCommand {
constructor(
private cipherService: CipherService,
private accountService: AccountService,
private configService: ConfigService,
private cipherAuthorizationService: CipherAuthorizationService,
) {}
async run(object: string, id: string): Promise<Response> {
@@ -27,8 +32,8 @@ export class RestoreCommand {
private async restoreCipher(id: string) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(id, activeUserId);
if (cipher == null) {
return Response.notFound();
}
@@ -36,6 +41,24 @@ export class RestoreCommand {
return Response.badRequest("Cipher is not in trash.");
}
const canRestore = await firstValueFrom(
combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion),
this.cipherAuthorizationService.canRestoreCipher$(cipher),
]).pipe(
map(([enabled, canRestore]) => {
if (enabled && !canRestore) {
return false;
}
return true;
}),
),
);
if (!canRestore) {
return Response.error("You do not have permission to restore this item");
}
try {
await this.cipherService.restoreWithServer(id, activeUserId);
return Response.success();

View File

@@ -127,6 +127,8 @@ export class OssServeConfigurator {
this.restoreCommand = new RestoreCommand(
this.serviceContainer.cipherService,
this.serviceContainer.accountService,
this.serviceContainer.configService,
this.serviceContainer.cipherAuthorizationService,
);
this.shareCommand = new ShareCommand(
this.serviceContainer.cipherService,

View File

@@ -350,6 +350,8 @@ export class VaultProgram extends BaseProgram {
const command = new RestoreCommand(
this.serviceContainer.cipherService,
this.serviceContainer.accountService,
this.serviceContainer.configService,
this.serviceContainer.cipherAuthorizationService,
);
const response = await command.run(object, id);
this.processResponse(response);

View File

@@ -656,7 +656,7 @@
class="primary"
(click)="restore()"
appA11yTitle="{{ 'restore' | i18n }}"
*ngIf="cipher.isDeleted"
*ngIf="(limitItemDeletion$ | async) ? (canRestoreCipher$ | async) : cipher.isDeleted"
>
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
</button>

View File

@@ -17,8 +17,10 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -70,6 +72,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
accountService: AccountService,
toastService: ToastService,
cipherAuthorizationService: CipherAuthorizationService,
private configService: ConfigService,
) {
super(
cipherService,
@@ -99,6 +102,9 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
cipherAuthorizationService,
);
}
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
ngOnInit() {
super.ngOnInit();