Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
type="checkbox"
bitCheckbox
appStopProp
[disabled]="disabled || decryptionFailure"
[disabled]="disabled || decryptionFailure || isPartial"
[checked]="checked"
(change)="$event ? this.checkedToggled.next() : null"
[attr.aria-label]="'vaultItemSelect' | i18n"
Expand Down Expand Up @@ -97,6 +97,13 @@
</p>
</td>
}
@if (showControlledAccess) {
<td bitCell [ngClass]="RowHeightClass" class="tw-hidden lg:tw-table-cell">
@if (leaseBadge) {
<ng-container *ngComponentOutlet="leaseBadge; inputs: { cipher: cipher }" />
}
</td>
}
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
@if (decryptionFailure) {
<button
Expand Down Expand Up @@ -293,31 +300,31 @@
<bit-menu-divider></bit-menu-divider>
}
}
@if (showFavorite) {
@if (showFavorite && !isPartial) {
<button bitMenuItem type="button" (click)="toggleFavorite()">
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>
{{ (cipher.favorite ? "unfavorite" : "favorite") | i18n }}
</button>
}
@if (!isDeleted && canEditCipher) {
@if (!isDeleted && canEditCipher && !isPartial) {
<button bitMenuItem type="button" (click)="editCipher()">
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "edit" | i18n }}
</button>
}
@if (showAttachments) {
@if (showAttachments && !isPartial) {
<button bitMenuItem type="button" (click)="attachments()">
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }}
</button>
}
@if (showClone) {
@if (showClone && !isPartial) {
<button bitMenuItem type="button" (click)="clone()">
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
{{ "clone" | i18n }}
</button>
}
@if (showAssignToCollections) {
@if (showAssignToCollections && !isPartial) {
<button bitMenuItem type="button" (click)="assignToCollections()">
<bit-icon fixedWidth [name]="'bwi-collection-shared' | vfo1Icon" slot="start" />
{{ "assignToCollections" | vfo1I18n: "addToSharedFolder" }}
Expand All @@ -329,7 +336,7 @@
{{ "eventLogs" | i18n }}
</button>
}
@if (showArchiveButton) {
@if (showArchiveButton && !isPartial) {
@if (userCanArchive) {
<button bitMenuItem (click)="archive()" type="button">
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
Expand All @@ -348,20 +355,20 @@
}
}

@if (showUnArchiveButton) {
@if (showUnArchiveButton && !isPartial) {
<button bitMenuItem (click)="unarchive()" type="button">
<i class="bwi bwi-fw bwi-unarchive" aria-hidden="true"></i>
{{ "unArchive" | i18n }}
</button>
}

@if (isDeleted && canRestoreCipher) {
@if (isDeleted && canRestoreCipher && !isPartial) {
<button bitMenuItem (click)="restore()" type="button">
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restore" | i18n }}
</button>
}
@if (canDeleteCipher) {
@if (canDeleteCipher && !isPartial) {
<button bitMenuItem variant="danger" (click)="deleteCipher()" type="button">
<bit-icon fixedWidth name="bwi-trash" slot="start" />
{{ (isDeleted ? "permanentlyDelete" : "delete") | i18n }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { OverlayContainer } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { RouterModule } from "@angular/router";
import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
Expand Down Expand Up @@ -29,6 +31,17 @@ import {
} from "@bitwarden/vault";

import { VaultCipherRowComponent } from "./vault-cipher-row.component";
import { VAULT_ROW_LEASE_BADGE } from "./vault-row-lease-badge.token";

/** Stand-in for a host-provided row badge; captures the cipher the slot passes through. */
@Component({
selector: "test-vault-row-lease-badge",
template: "<span data-testid='test-badge'></span>",
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestLeaseBadgeComponent {
readonly cipher = input<CipherViewLike>();
}

// eslint-disable-next-line no-console
const originalError = console.error;
Expand Down Expand Up @@ -169,6 +182,52 @@ describe("VaultCipherRowComponent", () => {
});
});

describe("partial (PAM-gated) row", () => {
let cipher: CipherView;

beforeEach(() => {
cipher = new CipherView();
cipher.id = "cipher-1";
cipher.name = "Gated";
cipher.type = CipherType.Login;
cipher.login = new LoginView();
cipher.organizationId = undefined;
cipher.deletedDate = null;
cipher.archivedDate = null;

component.cipher = cipher;
component.disabled = false;
});

it("isPartial reflects the cipher's partial flag", () => {
cipher.partial = true;
expect(component["isPartial"]).toBe(true);

cipher.partial = false;
expect(component["isPartial"]).toBe(false);
});

it("disables the selection checkbox for a partial row so it cannot be selected (or bulk-acted)", () => {
cipher.partial = true;
fixture.detectChanges();

const checkbox = fixture.nativeElement.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement;
expect(checkbox.disabled).toBe(true);
});

it("leaves the selection checkbox enabled for a normal row", () => {
cipher.partial = false;
fixture.detectChanges();

const checkbox = fixture.nativeElement.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement;
expect(checkbox.disabled).toBe(false);
});
});

describe("hasBankAccountOptions", () => {
let bankAccountCipher: CipherView;

Expand Down Expand Up @@ -259,4 +318,87 @@ describe("VaultCipherRowComponent", () => {
expect(component["showAssignToCollections"]).toBeFalsy();
});
});

describe("lease badge slot (VAULT_ROW_LEASE_BADGE)", () => {
async function setupBadge(provideBadge: boolean): Promise<void> {
TestBed.resetTestingModule();
await TestBed.configureTestingModule({
declarations: [VaultCipherRowComponent],
imports: [
CommonModule,
RouterModule.forRoot([]),
MenuModule,
IconButtonModule,
JslibModule,
CopyCipherFieldDirective,
OrganizationNameBadgeComponent,
PremiumBadgeComponent,
TestLeaseBadgeComponent,
],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
{
provide: EnvironmentService,
useValue: { environment$: new BehaviorSubject({}).asObservable() },
},
{
provide: DomainSettingsService,
useValue: { showFavicons$: new BehaviorSubject(false).asObservable() },
},
{ provide: CopyCipherFieldService, useValue: mock<CopyCipherFieldService>() },
{ provide: AccountService, useValue: mock<AccountService>() },
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: PremiumUpgradePromptService, useValue: mock<PremiumUpgradePromptService>() },
{
provide: ConfigService,
useValue: { getFeatureFlag$: jest.fn().mockReturnValue(of(false)) },
},
{
provide: BillingAccountProfileStateService,
useValue: mock<BillingAccountProfileStateService>(),
},
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{
provide: VaultCopyButtonsService,
useValue: { showQuickCopyActions$: new BehaviorSubject(false).asObservable() },
},
...(provideBadge
? [{ provide: VAULT_ROW_LEASE_BADGE, useValue: TestLeaseBadgeComponent }]
: []),
],
}).compileComponents();

fixture = TestBed.createComponent(VaultCipherRowComponent);
component = fixture.componentInstance;

const cipher = new CipherView();
cipher.id = "cipher-1";
cipher.name = "Test Login";
cipher.type = CipherType.Login;
cipher.login = new LoginView();
component.cipher = cipher;
component.organizations = [];
component.collections = [];
// The badge lives in the Controlled access column, which the table shows only when the
// seam is provided — mirror that gating here.
component.showControlledAccess = provideBadge;
fixture.detectChanges();
}

it("injects null and renders no badge when the host provides none", async () => {
await setupBadge(false);

expect(component["leaseBadge"]).toBeNull();
expect(fixture.debugElement.query(By.directive(TestLeaseBadgeComponent))).toBeNull();
});

it("renders the host badge in the Controlled access column with the row's cipher", async () => {
await setupBadge(true);

expect(component["leaseBadge"]).toBe(TestLeaseBadgeComponent);
const badge = fixture.debugElement.query(By.directive(TestLeaseBadgeComponent));
expect(badge).not.toBeNull();
expect((badge.componentInstance as TestLeaseBadgeComponent).cipher()).toBe(component.cipher);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import {
computed,
EventEmitter,
HostListener,
Inject,
inject,
Input,
OnInit,
Optional,
Output,
Type,
ViewChild,
} from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
Expand Down Expand Up @@ -40,6 +43,7 @@ import {
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { VaultItemEvent } from "./vault-item-event";
import { RowHeightClass } from "./vault-items.component";
import { VAULT_ROW_LEASE_BADGE } from "./vault-row-lease-badge.token";

// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
Expand Down Expand Up @@ -106,6 +110,9 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() viewingOrgVault: boolean;

// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() showControlledAccess: boolean;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() canEditCipher: boolean;
Expand Down Expand Up @@ -172,6 +179,7 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
@Optional() @Inject(VAULT_ROW_LEASE_BADGE) protected leaseBadge: Type<unknown> | null,
) {
this.showCopyAndLaunchActions$ = this.configService.getFeatureFlag$(
FeatureFlag.PM28091_AddCopyAndQuickLaunchActions,
Expand Down Expand Up @@ -262,6 +270,16 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
return CipherViewLikeUtils.decryptionFailure(this.cipher);
}

/**
* True when the row is a PAM-gated ("partial") cipher — the server suppressed its sensitive
* fields. Such a row is read-only: it renders (with the Controlled access badge) but must not
* be selectable or offer any modify action, since re-saving it would clobber the suppressed
* fields. See {@link CipherViewLikeUtils.isPartial}.
*/
protected get isPartial() {
return CipherViewLikeUtils.isPartial(this.cipher);
}

protected get showAssignToCollections() {
return (
this.organizations?.length &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
</p>
</td>
}
@if (showControlledAccess) {
<td bitCell [ngClass]="RowHeightClass" class="tw-hidden lg:tw-table-cell"></td>
}
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
@if (canEditCollection || canDeleteCollection || canViewCollectionInfo) {
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export class VaultCollectionRowComponent<C extends CipherViewLike> {
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() showPermissionsColumn: boolean;

// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() showControlledAccess: boolean;

// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onEvent = new EventEmitter<VaultItemEvent<C>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
{{ "permission" | i18n }}
</th>
}
@if (showControlledAccess) {
<th bitCell class="tw-hidden tw-w-40 lg:tw-table-cell">
{{ "controlledAccess" | i18n }}
</th>
}
<th
bitCell
[ngClass]="
Expand Down Expand Up @@ -155,6 +160,7 @@
[showGroups]="showGroups"
[organizations]="allOrganizations"
[showPermissionsColumn]="showPermissionsColumn"
[showControlledAccess]="showControlledAccess"
[groups]="allGroups"
[canDeleteCollection]="canDeleteCollection(item.collection)"
[canEditCollection]="canEditCollection(item.collection)"
Expand All @@ -178,6 +184,7 @@
[showOwner]="showOwner"
[showCollections]="showCollections"
[showGroups]="showGroups"
[showControlledAccess]="showControlledAccess"
[showPremiumFeatures]="showPremiumFeatures"
[useEvents]="useEvents"
[viewingOrgVault]="viewingOrgVault"
Expand Down
Loading
Loading