[AC-2500] Update inline menu for collections based on collection permissions (#9080)

* Add view collection options to collection row menus

* Prevent DeleteAnyCollection custom users from viewing collections
This commit is contained in:
Thomas Rittson 2024-05-10 10:50:34 +10:00 committed by GitHub
parent fb3766b6c1
commit 8e97c1c8e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 126 additions and 47 deletions

View File

@ -58,7 +58,7 @@ export class VaultCipherRowComponent {
}
protected editCollections() {
this.onEvent.emit({ type: "viewCollections", item: this.cipher });
this.onEvent.emit({ type: "viewCipherCollections", item: this.cipher });
}
protected events() {

View File

@ -63,7 +63,7 @@
</td>
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
<button
*ngIf="canEditCollection || canDeleteCollection"
*ngIf="canEditCollection || canDeleteCollection || canViewCollectionInfo"
[disabled]="disabled"
[bitMenuTriggerFor]="collectionOptions"
size="small"
@ -73,14 +73,28 @@
appStopProp
></button>
<bit-menu #collectionOptions>
<button *ngIf="canEditCollection" type="button" bitMenuItem (click)="edit()">
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "editInfo" | i18n }}
</button>
<button *ngIf="canEditCollection" type="button" bitMenuItem (click)="access()">
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
<ng-container *ngIf="canEditCollection">
<button type="button" bitMenuItem (click)="edit(false)">
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "editInfo" | i18n }}
</button>
<button type="button" bitMenuItem (click)="access(false)">
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
</ng-container>
<ng-container
*ngIf="flexibleCollectionsV1Enabled && !canEditCollection && canViewCollectionInfo"
>
<button type="button" bitMenuItem (click)="edit(true)">
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "viewInfo" | i18n }}
</button>
<button type="button" bitMenuItem (click)="access(true)">
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "viewAccess" | i18n }}
</button>
</ng-container>
<button *ngIf="canDeleteCollection" type="button" bitMenuItem (click)="deleteCollection()">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>

View File

@ -30,9 +30,11 @@ export class VaultCollectionRowComponent {
@Input() showGroups: boolean;
@Input() canEditCollection: boolean;
@Input() canDeleteCollection: boolean;
@Input() canViewCollectionInfo: boolean;
@Input() organizations: Organization[];
@Input() groups: GroupView[];
@Input() showPermissionsColumn: boolean;
@Input() flexibleCollectionsV1Enabled: boolean;
@Output() onEvent = new EventEmitter<VaultItemEvent>();
@ -71,12 +73,12 @@ export class VaultCollectionRowComponent {
return "";
}
protected edit() {
this.onEvent.next({ type: "editCollection", item: this.collection });
protected edit(readonly: boolean) {
this.onEvent.next({ type: "editCollection", item: this.collection, readonly: readonly });
}
protected access() {
this.onEvent.next({ type: "viewCollectionAccess", item: this.collection });
protected access(readonly: boolean) {
this.onEvent.next({ type: "viewCollectionAccess", item: this.collection, readonly: readonly });
}
protected deleteCollection() {

View File

@ -5,11 +5,11 @@ import { VaultItem } from "./vault-item";
export type VaultItemEvent =
| { type: "viewAttachments"; item: CipherView }
| { type: "viewCollections"; item: CipherView }
| { type: "viewCipherCollections"; item: CipherView }
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
| { type: "viewCollectionAccess"; item: CollectionView }
| { type: "viewCollectionAccess"; item: CollectionView; readonly: boolean }
| { type: "viewEvents"; item: CipherView }
| { type: "editCollection"; item: CollectionView }
| { type: "editCollection"; item: CollectionView; readonly: boolean }
| { type: "clone"; item: CipherView }
| { type: "restore"; items: CipherView[] }
| { type: "delete"; items: VaultItem[] }

View File

@ -95,13 +95,15 @@
[groups]="allGroups"
[canDeleteCollection]="canDeleteCollection(item.collection)"
[canEditCollection]="canEditCollection(item.collection)"
[canViewCollectionInfo]="canViewCollectionInfo(item.collection)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
[checked]="selection.isSelected(item)"
(checkedToggled)="selection.toggle(item)"
(onEvent)="event($event)"
></tr>
<!--
addAccessStatus check here so ciphers do not show if user
has filtered for collections with addAccess
<!--
addAccessStatus check here so ciphers do not show if user
has filtered for collections with addAccess
-->
<tr
*ngIf="item.cipher && (!addAccessToggle || (addAccessToggle && addAccessStatus !== 1))"

View File

@ -165,6 +165,11 @@ export class VaultItemsComponent {
return collection.canDelete(organization);
}
protected canViewCollectionInfo(collection: CollectionView) {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canViewCollectionInfo(organization);
}
protected toggleAll() {
this.isAllSelected
? this.selection.clear()

View File

@ -4,6 +4,7 @@ import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/mod
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view";
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
export class CollectionAdminView extends CollectionView {
groups: CollectionAccessSelectionView[] = [];
@ -89,4 +90,19 @@ export class CollectionAdminView extends CollectionView {
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
}
/**
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
*/
override canViewCollectionInfo(org: Organization | undefined): boolean {
if (this.isUnassignedCollection) {
return false;
}
return this.manage || org?.isAdmin || org?.permissions.editAnyCollection;
}
get isUnassignedCollection() {
return this.id === Unassigned;
}
}

View File

@ -434,7 +434,7 @@ export class VaultComponent implements OnInit, OnDestroy {
try {
if (event.type === "viewAttachments") {
await this.editCipherAttachments(event.item);
} else if (event.type === "viewCollections") {
} else if (event.type === "viewCipherCollections") {
await this.editCipherCollections(event.item);
} else if (event.type === "clone") {
await this.cloneCipher(event.item);

View File

@ -37,24 +37,44 @@
aria-haspopup="true"
></button>
<bit-menu #editCollectionMenu>
<button
type="button"
*ngIf="canEditCollection"
bitMenuItem
(click)="editCollection(CollectionDialogTabType.Info)"
<ng-container *ngIf="canEditCollection">
<button
type="button"
bitMenuItem
(click)="editCollection(CollectionDialogTabType.Info, false)"
>
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "editInfo" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="editCollection(CollectionDialogTabType.Access, false)"
>
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
</ng-container>
<ng-container
*ngIf="flexibleCollectionsV1Enabled && !canEditCollection && canViewCollectionInfo"
>
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "editInfo" | i18n }}
</button>
<button
type="button"
*ngIf="canEditCollection"
bitMenuItem
(click)="editCollection(CollectionDialogTabType.Access)"
>
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="editCollection(CollectionDialogTabType.Info, true)"
>
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "viewInfo" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="editCollection(CollectionDialogTabType.Access, true)"
>
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "viewAccess" | i18n }}
</button>
</ng-container>
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>

View File

@ -53,7 +53,10 @@ export class VaultHeaderComponent implements OnInit {
@Output() onAddCollection = new EventEmitter<void>();
/** Emits an event when the edit collection button is clicked in the header */
@Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType }>();
@Output() onEditCollection = new EventEmitter<{
tab: CollectionDialogTabType;
readonly: boolean;
}>();
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
@ -64,7 +67,7 @@ export class VaultHeaderComponent implements OnInit {
protected CollectionDialogTabType = CollectionDialogTabType;
protected organizations$ = this.organizationService.organizations$;
private flexibleCollectionsV1Enabled = false;
protected flexibleCollectionsV1Enabled = false;
private restrictProviderAccessFlag = false;
constructor(
@ -193,8 +196,8 @@ export class VaultHeaderComponent implements OnInit {
this.onAddCollection.emit();
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {
this.onEditCollection.emit({ tab });
async editCollection(tab: CollectionDialogTabType, readonly: boolean): Promise<void> {
this.onEditCollection.emit({ tab, readonly });
}
get canDeleteCollection(): boolean {
@ -207,6 +210,10 @@ export class VaultHeaderComponent implements OnInit {
return this.collection.node.canDelete(this.organization);
}
get canViewCollectionInfo(): boolean {
return this.collection.node.canViewCollectionInfo(this.organization);
}
get canCreateCollection(): boolean {
return this.organization?.canCreateNewCollections;
}

View File

@ -6,7 +6,7 @@
[searchText]="currentSearchText$ | async"
(onAddCipher)="addCipher()"
(onAddCollection)="addCollection()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab, $event.readonly)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
(searchTextChanged)="filterSearchText($event)"
></app-org-vault-header>

View File

@ -736,7 +736,7 @@ export class VaultComponent implements OnInit, OnDestroy {
try {
if (event.type === "viewAttachments") {
await this.editCipherAttachments(event.item);
} else if (event.type === "viewCollections") {
} else if (event.type === "viewCipherCollections") {
await this.editCipherCollections(event.item);
} else if (event.type === "clone") {
await this.cloneCipher(event.item);
@ -761,9 +761,9 @@ export class VaultComponent implements OnInit, OnDestroy {
} else if (event.type === "copyField") {
await this.copy(event.item, event.field);
} else if (event.type === "editCollection") {
await this.editCollection(event.item, CollectionDialogTabType.Info);
await this.editCollection(event.item, CollectionDialogTabType.Info, event.readonly);
} else if (event.type === "viewCollectionAccess") {
await this.editCollection(event.item, CollectionDialogTabType.Access);
await this.editCollection(event.item, CollectionDialogTabType.Access, event.readonly);
} else if (event.type === "bulkEditCollectionAccess") {
await this.bulkEditCollectionAccess(event.items);
} else if (event.type === "assignToCollections") {
@ -1190,7 +1190,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async editCollection(
c: CollectionView,
tab: CollectionDialogTabType,
readonly: boolean = false,
readonly: boolean,
): Promise<void> {
const dialog = openCollectionDialog(this.dialogService, {
data: {

View File

@ -8081,5 +8081,11 @@
},
"manageBillingFromProviderPortalMessage": {
"message": "Manage billing from the Provider Portal"
},
"viewInfo": {
"message": "View info"
},
"viewAccess": {
"message": "View access"
}
}

View File

@ -87,6 +87,13 @@ export class CollectionView implements View, ITreeNodeObject {
: org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
}
/**
* Returns true if the user can view collection info and access in a read-only state from the individual vault
*/
canViewCollectionInfo(org: Organization | undefined): boolean {
return false;
}
static fromJSON(obj: Jsonify<CollectionView>) {
return Object.assign(new CollectionView(new Collection()), obj);
}