mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
[PM-11162] Assign To Collections Permission Update (#11367)
Only users with Manage/Edit permissions will be allowed to Assign To Collections. If the user has Can Edit Except Password the collections dropdown will be disabled. --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com> Co-authored-by: kejaeger <138028972+kejaeger@users.noreply.github.com>
This commit is contained in:
@@ -4155,15 +4155,6 @@
|
|||||||
"itemName": {
|
"itemName": {
|
||||||
"message": "Item name"
|
"message": "Item name"
|
||||||
},
|
},
|
||||||
"cannotRemoveViewOnlyCollections": {
|
|
||||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
|
||||||
"placeholders": {
|
|
||||||
"collections": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "Work, Personal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"organizationIsDeactivated": {
|
"organizationIsDeactivated": {
|
||||||
"message": "Organization is deactivated"
|
"message": "Organization is deactivated"
|
||||||
},
|
},
|
||||||
@@ -4896,6 +4887,15 @@
|
|||||||
"extraWide": {
|
"extraWide": {
|
||||||
"message": "Extra wide"
|
"message": "Extra wide"
|
||||||
},
|
},
|
||||||
|
"cannotRemoveViewOnlyCollections": {
|
||||||
|
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||||
|
"placeholders": {
|
||||||
|
"collections": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Work, Personal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"updateDesktopAppOrDisableFingerprintDialogTitle": {
|
"updateDesktopAppOrDisableFingerprintDialogTitle": {
|
||||||
"message": "Please update your desktop application"
|
"message": "Please update your desktop application"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<button type="button" bitMenuItem (click)="toggleFavorite()">
|
<button type="button" bitMenuItem (click)="toggleFavorite()">
|
||||||
{{ favoriteText | i18n }}
|
{{ favoriteText | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<ng-container *ngIf="canEdit">
|
<ng-container *ngIf="canEdit && canViewPassword">
|
||||||
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
|
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
|
||||||
{{ "clone" | i18n }}
|
{{ "clone" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export class ItemMoreOptionsComponent implements OnInit {
|
|||||||
return this.cipher.edit;
|
return this.cipher.edit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canViewPassword() {
|
||||||
|
return this.cipher.viewPassword;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Determines if the cipher can be autofilled.
|
* Determines if the cipher can be autofilled.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ export class EditCommand {
|
|||||||
"Item does not belong to an organization. Consider moving it first.",
|
"Item does not belong to an organization. Consider moving it first.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!cipher.viewPassword) {
|
||||||
|
return Response.noEditPermission();
|
||||||
|
}
|
||||||
|
|
||||||
cipher.collectionIds = req;
|
cipher.collectionIds = req;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export class Response {
|
|||||||
return Response.error("Not found.");
|
return Response.error("Not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static noEditPermission(): Response {
|
||||||
|
return Response.error("You do not have permission to edit this item");
|
||||||
|
}
|
||||||
|
|
||||||
static badRequest(message: string): Response {
|
static badRequest(message: string): Response {
|
||||||
return Response.error(message);
|
return Response.error(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
[(ngModel)]="$any(c).checked"
|
[(ngModel)]="$any(c).checked"
|
||||||
name="Collection[{{ i }}].Checked"
|
name="Collection[{{ i }}].Checked"
|
||||||
|
[disabled]="!cipher.canAssignToCollections"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
@Input() collections: CollectionView[];
|
@Input() collections: CollectionView[];
|
||||||
@Input() viewingOrgVault: boolean;
|
@Input() viewingOrgVault: boolean;
|
||||||
@Input() canEditCipher: boolean;
|
@Input() canEditCipher: boolean;
|
||||||
|
@Input() canAssignCollections: boolean;
|
||||||
@Input() canManageCollection: boolean;
|
@Input() canManageCollection: boolean;
|
||||||
|
|
||||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||||
@@ -101,7 +102,7 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected get showAssignToCollections() {
|
protected get showAssignToCollections() {
|
||||||
return this.organizations?.length && this.canEditCipher && !this.cipher.isDeleted;
|
return this.organizations?.length && this.canAssignCollections && !this.cipher.isDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get showClone() {
|
protected get showClone() {
|
||||||
@@ -208,6 +209,6 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
return true; // Always show checkbox in individual vault or for non-org items
|
return true; // Always show checkbox in individual vault or for non-org items
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.organization.canEditAllCiphers || this.cipher.edit;
|
return this.organization.canEditAllCiphers || (this.cipher.edit && this.cipher.viewPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
[collections]="allCollections"
|
[collections]="allCollections"
|
||||||
[checked]="selection.isSelected(item)"
|
[checked]="selection.isSelected(item)"
|
||||||
[canEditCipher]="canEditCipher(item.cipher)"
|
[canEditCipher]="canEditCipher(item.cipher)"
|
||||||
|
[canAssignCollections]="canAssignCollections(item.cipher)"
|
||||||
[canManageCollection]="canManageCollection(item.cipher)"
|
[canManageCollection]="canManageCollection(item.cipher)"
|
||||||
(checkedToggled)="selection.toggle(item)"
|
(checkedToggled)="selection.toggle(item)"
|
||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
|
|||||||
@@ -236,6 +236,13 @@ export class VaultItemsComponent {
|
|||||||
return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit;
|
return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected canAssignCollections(cipher: CipherView) {
|
||||||
|
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
|
||||||
|
return (
|
||||||
|
(organization?.canEditAllCiphers && this.viewingOrgVault) || cipher.canAssignToCollections
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected canManageCollection(cipher: CipherView) {
|
protected canManageCollection(cipher: CipherView) {
|
||||||
// If the cipher is not part of an organization (personal item), user can manage it
|
// If the cipher is not part of an organization (personal item), user can manage it
|
||||||
if (cipher.organizationId == null) {
|
if (cipher.organizationId == null) {
|
||||||
@@ -461,7 +468,7 @@ export class VaultItemsComponent {
|
|||||||
private allCiphersHaveEditAccess(): boolean {
|
private allCiphersHaveEditAccess(): boolean {
|
||||||
return this.selection.selected
|
return this.selection.selected
|
||||||
.filter(({ cipher }) => cipher)
|
.filter(({ cipher }) => cipher)
|
||||||
.every(({ cipher }) => cipher?.edit);
|
.every(({ cipher }) => cipher?.edit && cipher?.viewPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUniqueOrganizationIds(): Set<string> {
|
private getUniqueOrganizationIds(): Set<string> {
|
||||||
|
|||||||
@@ -749,15 +749,6 @@
|
|||||||
"itemName": {
|
"itemName": {
|
||||||
"message": "Item name"
|
"message": "Item name"
|
||||||
},
|
},
|
||||||
"cannotRemoveViewOnlyCollections": {
|
|
||||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
|
||||||
"placeholders": {
|
|
||||||
"collections": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "Work, Personal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ex": {
|
"ex": {
|
||||||
"message": "ex.",
|
"message": "ex.",
|
||||||
"description": "Short abbreviation for 'example'."
|
"description": "Short abbreviation for 'example'."
|
||||||
@@ -10143,6 +10134,15 @@
|
|||||||
"descriptorCode": {
|
"descriptorCode": {
|
||||||
"message": "Descriptor code"
|
"message": "Descriptor code"
|
||||||
},
|
},
|
||||||
|
"cannotRemoveViewOnlyCollections": {
|
||||||
|
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||||
|
"placeholders": {
|
||||||
|
"collections": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Work, Personal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"importantNotice": {
|
"importantNotice": {
|
||||||
"message": "Important notice"
|
"message": "Important notice"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canAssignToCollections(): boolean {
|
||||||
|
if (this.organizationId == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.edit && this.viewPassword;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Determines if the cipher can be launched in a new browser tab.
|
* Determines if the cipher can be launched in a new browser tab.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -61,8 +61,8 @@
|
|||||||
formControlName="collectionIds"
|
formControlName="collectionIds"
|
||||||
[baseItems]="collectionOptions"
|
[baseItems]="collectionOptions"
|
||||||
></bit-multi-select>
|
></bit-multi-select>
|
||||||
<bit-hint *ngIf="readOnlyCollections.length > 0" data-testid="view-only-hint">
|
<bit-hint *ngIf="readOnlyCollectionsNames.length > 0" data-testid="view-only-hint">
|
||||||
{{ "cannotRemoveViewOnlyCollections" | i18n: readOnlyCollections.join(", ") }}
|
{{ "cannotRemoveViewOnlyCollections" | i18n: readOnlyCollectionsNames.join(", ") }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -17,6 +17,29 @@ import { CipherFormContainer } from "../../cipher-form-container";
|
|||||||
|
|
||||||
import { ItemDetailsSectionComponent } from "./item-details-section.component";
|
import { ItemDetailsSectionComponent } from "./item-details-section.component";
|
||||||
|
|
||||||
|
const createMockCollection = (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
organizationId: string,
|
||||||
|
readOnly = false,
|
||||||
|
canEdit = true,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
organizationId,
|
||||||
|
externalId: "",
|
||||||
|
readOnly,
|
||||||
|
hidePasswords: false,
|
||||||
|
manage: true,
|
||||||
|
assigned: true,
|
||||||
|
canEditItems: jest.fn().mockReturnValue(canEdit),
|
||||||
|
canEdit: jest.fn(),
|
||||||
|
canDelete: jest.fn(),
|
||||||
|
canViewCollectionInfo: jest.fn(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
describe("ItemDetailsSectionComponent", () => {
|
describe("ItemDetailsSectionComponent", () => {
|
||||||
let component: ItemDetailsSectionComponent;
|
let component: ItemDetailsSectionComponent;
|
||||||
let fixture: ComponentFixture<ItemDetailsSectionComponent>;
|
let fixture: ComponentFixture<ItemDetailsSectionComponent>;
|
||||||
@@ -94,13 +117,7 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
component.config.allowPersonalOwnership = true;
|
component.config.allowPersonalOwnership = true;
|
||||||
component.config.organizations = [{ id: "org1" } as Organization];
|
component.config.organizations = [{ id: "org1" } as Organization];
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{
|
createMockCollection("col1", "Collection 1", "org1") as CollectionView,
|
||||||
id: "col1",
|
|
||||||
name: "Collection 1",
|
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
getInitialCipherView.mockReturnValueOnce({
|
getInitialCipherView.mockReturnValueOnce({
|
||||||
@@ -343,8 +360,8 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
component.config.allowPersonalOwnership = true;
|
component.config.allowPersonalOwnership = true;
|
||||||
component.config.organizations = [{ id: "org1" } as Organization];
|
component.config.organizations = [{ id: "org1" } as Organization];
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{ id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView,
|
createMockCollection("col1", "Collection 1", "org1") as CollectionView,
|
||||||
{ id: "col2", name: "Collection 2", organizationId: "org1" } as CollectionView,
|
createMockCollection("col2", "Collection 2", "org1") as CollectionView,
|
||||||
];
|
];
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -374,27 +391,9 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
});
|
});
|
||||||
component.config.organizations = [{ id: "org1" } as Organization];
|
component.config.organizations = [{ id: "org1" } as Organization];
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{
|
createMockCollection("col1", "Collection 1", "org1") as CollectionView,
|
||||||
id: "col1",
|
createMockCollection("col2", "Collection 2", "org1") as CollectionView,
|
||||||
name: "Collection 1",
|
createMockCollection("col3", "Collection 3", "org1") as CollectionView,
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col2",
|
|
||||||
name: "Collection 2",
|
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col3",
|
|
||||||
name: "Collection 3",
|
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -412,13 +411,7 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
component.config.allowPersonalOwnership = true;
|
component.config.allowPersonalOwnership = true;
|
||||||
component.config.organizations = [{ id: "org1" } as Organization];
|
component.config.organizations = [{ id: "org1" } as Organization];
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{
|
createMockCollection("col1", "Collection 1", "org1") as CollectionView,
|
||||||
id: "col1",
|
|
||||||
name: "Collection 1",
|
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -452,27 +445,9 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
} as CipherView;
|
} as CipherView;
|
||||||
component.config.organizations = [{ id: "org1" } as Organization];
|
component.config.organizations = [{ id: "org1" } as Organization];
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{
|
createMockCollection("col1", "Collection 1", "org1", true, false) as CollectionView,
|
||||||
id: "col1",
|
createMockCollection("col2", "Collection 2", "org1", true, false) as CollectionView,
|
||||||
name: "Collection 1",
|
createMockCollection("col3", "Collection 3", "org1", true) as CollectionView,
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col2",
|
|
||||||
name: "Collection 2",
|
|
||||||
organizationId: "org1",
|
|
||||||
assigned: true,
|
|
||||||
readOnly: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col3",
|
|
||||||
name: "Collection 3",
|
|
||||||
organizationId: "org1",
|
|
||||||
readOnly: true,
|
|
||||||
assigned: true,
|
|
||||||
} as CollectionView,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
await component.ngOnInit();
|
await component.ngOnInit();
|
||||||
@@ -490,27 +465,9 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
component.config.allowPersonalOwnership = true;
|
component.config.allowPersonalOwnership = true;
|
||||||
component.config.organizations = [{ id: "org1" } as Organization];
|
component.config.organizations = [{ id: "org1" } as Organization];
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{
|
createMockCollection("col1", "Collection 1", "org1", true, false) as CollectionView,
|
||||||
id: "col1",
|
createMockCollection("col2", "Collection 2", "org1", true, false) as CollectionView,
|
||||||
name: "Collection 1",
|
createMockCollection("col3", "Collection 3", "org1", false, false) as CollectionView,
|
||||||
organizationId: "org1",
|
|
||||||
readOnly: true,
|
|
||||||
assigned: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col2",
|
|
||||||
name: "Collection 2",
|
|
||||||
organizationId: "org1",
|
|
||||||
readOnly: true,
|
|
||||||
assigned: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col3",
|
|
||||||
name: "Collection 3",
|
|
||||||
organizationId: "org1",
|
|
||||||
readOnly: false,
|
|
||||||
assigned: true,
|
|
||||||
} as CollectionView,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -527,26 +484,9 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
component.config.mode = "edit";
|
component.config.mode = "edit";
|
||||||
component.config.admin = true;
|
component.config.admin = true;
|
||||||
component.config.collections = [
|
component.config.collections = [
|
||||||
{
|
createMockCollection("col1", "Collection 1", "org1", true, false) as CollectionView,
|
||||||
id: "col1",
|
createMockCollection("col2", "Collection 2", "org1", false, true) as CollectionView,
|
||||||
name: "Collection 1",
|
createMockCollection("col3", "Collection 3", "org1", true, false) as CollectionView,
|
||||||
organizationId: "org1",
|
|
||||||
readOnly: true,
|
|
||||||
assigned: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col2",
|
|
||||||
name: "Collection 2",
|
|
||||||
organizationId: "org1",
|
|
||||||
assigned: false,
|
|
||||||
} as CollectionView,
|
|
||||||
{
|
|
||||||
id: "col3",
|
|
||||||
name: "Collection 3",
|
|
||||||
organizationId: "org1",
|
|
||||||
readOnly: true,
|
|
||||||
assigned: false,
|
|
||||||
} as CollectionView,
|
|
||||||
];
|
];
|
||||||
component.originalCipherView = {
|
component.originalCipherView = {
|
||||||
name: "cipher1",
|
name: "cipher1",
|
||||||
@@ -562,6 +502,7 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not show collections as readonly when `config.admin` is true", async () => {
|
it("should not show collections as readonly when `config.admin` is true", async () => {
|
||||||
|
component.config.isAdminConsole = true;
|
||||||
await component.ngOnInit();
|
await component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -573,8 +514,7 @@ describe("ItemDetailsSectionComponent", () => {
|
|||||||
|
|
||||||
await component.ngOnInit();
|
await component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
expect(component["readOnlyCollectionsNames"]).toEqual(["Collection 1", "Collection 3"]);
|
||||||
expect(component["readOnlyCollections"]).toEqual(["Collection 1", "Collection 3"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule, NgClass } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, DestroyRef, Input, OnInit } from "@angular/core";
|
import { Component, DestroyRef, Input, OnInit } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
@@ -8,6 +8,7 @@ import { concatMap, map } from "rxjs";
|
|||||||
|
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -43,7 +44,6 @@ import { CipherFormContainer } from "../../cipher-form-container";
|
|||||||
SelectModule,
|
SelectModule,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
NgClass,
|
|
||||||
JslibModule,
|
JslibModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
],
|
],
|
||||||
@@ -67,7 +67,7 @@ export class ItemDetailsSectionComponent implements OnInit {
|
|||||||
* Collections that are already assigned to the cipher and are read-only. These cannot be removed.
|
* Collections that are already assigned to the cipher and are read-only. These cannot be removed.
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected readOnlyCollections: string[] = [];
|
protected readOnlyCollections: CollectionView[] = [];
|
||||||
|
|
||||||
protected showCollectionsControl: boolean;
|
protected showCollectionsControl: boolean;
|
||||||
|
|
||||||
@@ -79,6 +79,10 @@ export class ItemDetailsSectionComponent implements OnInit {
|
|||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
originalCipherView: CipherView;
|
originalCipherView: CipherView;
|
||||||
|
|
||||||
|
get readOnlyCollectionsNames(): string[] {
|
||||||
|
return this.readOnlyCollections.map((c) => c.name);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Whether the form is in partial edit mode. Only the folder and favorite controls are available.
|
* Whether the form is in partial edit mode. Only the folder and favorite controls are available.
|
||||||
*/
|
*/
|
||||||
@@ -133,7 +137,10 @@ export class ItemDetailsSectionComponent implements OnInit {
|
|||||||
name: value.name,
|
name: value.name,
|
||||||
organizationId: value.organizationId,
|
organizationId: value.organizationId,
|
||||||
folderId: value.folderId,
|
folderId: value.folderId,
|
||||||
collectionIds: value.collectionIds?.map((c) => c.id) || [],
|
collectionIds: [
|
||||||
|
...(value.collectionIds?.map((c) => c.id) || []),
|
||||||
|
...this.readOnlyCollections.map((c) => c.id),
|
||||||
|
],
|
||||||
favorite: value.favorite,
|
favorite: value.favorite,
|
||||||
} as CipherView);
|
} as CipherView);
|
||||||
return cipher;
|
return cipher;
|
||||||
@@ -223,6 +230,8 @@ export class ItemDetailsSectionComponent implements OnInit {
|
|||||||
favorite: prefillCipher.favorite,
|
favorite: prefillCipher.favorite,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orgId = this.itemDetailsForm.controls.organizationId.value as OrganizationId;
|
||||||
|
const organization = this.organizations.find((o) => o.id === orgId);
|
||||||
const initializedWithCachedCipher = this.cipherFormContainer.initializedWithCachedCipher();
|
const initializedWithCachedCipher = this.cipherFormContainer.initializedWithCachedCipher();
|
||||||
|
|
||||||
// Configure form for clone mode.
|
// Configure form for clone mode.
|
||||||
@@ -244,20 +253,36 @@ export class ItemDetailsSectionComponent implements OnInit {
|
|||||||
|
|
||||||
await this.updateCollectionOptions(prefillCollections);
|
await this.updateCollectionOptions(prefillCollections);
|
||||||
|
|
||||||
|
if (!organization?.canEditAllCiphers && !prefillCipher.canAssignToCollections) {
|
||||||
|
this.itemDetailsForm.controls.collectionIds.disable();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.partialEdit) {
|
if (this.partialEdit) {
|
||||||
this.itemDetailsForm.disable();
|
this.itemDetailsForm.disable();
|
||||||
this.itemDetailsForm.controls.favorite.enable();
|
this.itemDetailsForm.controls.favorite.enable();
|
||||||
this.itemDetailsForm.controls.folderId.enable();
|
this.itemDetailsForm.controls.folderId.enable();
|
||||||
} else if (this.config.mode === "edit") {
|
} else if (this.config.mode === "edit") {
|
||||||
this.readOnlyCollections = this.collections
|
if (!this.config.isAdminConsole || !this.config.admin) {
|
||||||
.filter(
|
this.readOnlyCollections = this.collections.filter(
|
||||||
// When the configuration is set up for admins, they can alter read only collections
|
// When the configuration is set up for admins, they can alter read only collections
|
||||||
(c) =>
|
(c) =>
|
||||||
|
c.organizationId === orgId &&
|
||||||
c.readOnly &&
|
c.readOnly &&
|
||||||
!this.config.admin &&
|
|
||||||
this.originalCipherView.collectionIds.includes(c.id as CollectionId),
|
this.originalCipherView.collectionIds.includes(c.id as CollectionId),
|
||||||
)
|
);
|
||||||
.map((c) => c.name);
|
|
||||||
|
// When Owners/Admins access setting is turned on.
|
||||||
|
// Disable Collections Options if Owner/Admin does not have Edit/Manage permissions on item
|
||||||
|
// Disable Collections Options if Custom user does not have Edit/Manage permissions on item
|
||||||
|
if (
|
||||||
|
(organization.allowAdminAccessToAllCollectionItems &&
|
||||||
|
(!this.originalCipherView.viewPassword || !this.originalCipherView.edit)) ||
|
||||||
|
(organization.type === OrganizationUserType.Custom &&
|
||||||
|
!this.originalCipherView.viewPassword)
|
||||||
|
) {
|
||||||
|
this.itemDetailsForm.controls.collectionIds.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
|||||||
await this.initializeItems(this.selectedOrgId);
|
await this.initializeItems(this.selectedOrgId);
|
||||||
|
|
||||||
if (this.selectedOrgId && this.selectedOrgId !== MY_VAULT_ID) {
|
if (this.selectedOrgId && this.selectedOrgId !== MY_VAULT_ID) {
|
||||||
await this.handleOrganizationCiphers();
|
await this.handleOrganizationCiphers(this.selectedOrgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupFormSubscriptions();
|
this.setupFormSubscriptions();
|
||||||
@@ -283,7 +283,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
|||||||
private sortItems = (a: SelectItemView, b: SelectItemView) =>
|
private sortItems = (a: SelectItemView, b: SelectItemView) =>
|
||||||
this.i18nService.collator.compare(a.labelName, b.labelName);
|
this.i18nService.collator.compare(a.labelName, b.labelName);
|
||||||
|
|
||||||
private async handleOrganizationCiphers() {
|
private async handleOrganizationCiphers(organizationId: OrganizationId) {
|
||||||
// If no ciphers are editable, cancel the operation
|
// If no ciphers are editable, cancel the operation
|
||||||
if (this.editableItemCount == 0) {
|
if (this.editableItemCount == 0) {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
@@ -296,12 +296,21 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.availableCollections = this.params.availableCollections.map((c) => ({
|
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
icon: "bwi-collection",
|
const org = await firstValueFrom(
|
||||||
id: c.id,
|
this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)),
|
||||||
labelName: c.name,
|
);
|
||||||
listName: c.name,
|
|
||||||
}));
|
this.availableCollections = this.params.availableCollections
|
||||||
|
.filter((collection) => {
|
||||||
|
return collection.canEditItems(org);
|
||||||
|
})
|
||||||
|
.map((c) => ({
|
||||||
|
icon: "bwi-collection",
|
||||||
|
id: c.id,
|
||||||
|
labelName: c.name,
|
||||||
|
listName: c.name,
|
||||||
|
}));
|
||||||
|
|
||||||
// Select assigned collections for a single cipher.
|
// Select assigned collections for a single cipher.
|
||||||
this.selectCollectionsAssignedToSingleCipher();
|
this.selectCollectionsAssignedToSingleCipher();
|
||||||
|
|||||||
Reference in New Issue
Block a user