[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() { protected editCollections() {
this.onEvent.emit({ type: "viewCollections", item: this.cipher }); this.onEvent.emit({ type: "viewCipherCollections", item: this.cipher });
} }
protected events() { protected events() {

View File

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

View File

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

View File

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

View File

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

View File

@ -165,6 +165,11 @@ export class VaultItemsComponent {
return collection.canDelete(organization); return collection.canDelete(organization);
} }
protected canViewCollectionInfo(collection: CollectionView) {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canViewCollectionInfo(organization);
}
protected toggleAll() { protected toggleAll() {
this.isAllSelected this.isAllSelected
? this.selection.clear() ? 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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.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 { export class CollectionAdminView extends CollectionView {
groups: CollectionAccessSelectionView[] = []; groups: CollectionAccessSelectionView[] = [];
@ -89,4 +90,19 @@ export class CollectionAdminView extends CollectionView {
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups; 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 { try {
if (event.type === "viewAttachments") { if (event.type === "viewAttachments") {
await this.editCipherAttachments(event.item); await this.editCipherAttachments(event.item);
} else if (event.type === "viewCollections") { } else if (event.type === "viewCipherCollections") {
await this.editCipherCollections(event.item); await this.editCipherCollections(event.item);
} else if (event.type === "clone") { } else if (event.type === "clone") {
await this.cloneCipher(event.item); await this.cloneCipher(event.item);

View File

@ -37,24 +37,44 @@
aria-haspopup="true" aria-haspopup="true"
></button> ></button>
<bit-menu #editCollectionMenu> <bit-menu #editCollectionMenu>
<button <ng-container *ngIf="canEditCollection">
type="button" <button
*ngIf="canEditCollection" type="button"
bitMenuItem bitMenuItem
(click)="editCollection(CollectionDialogTabType.Info)" (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> <button
{{ "editInfo" | i18n }} type="button"
</button> bitMenuItem
<button (click)="editCollection(CollectionDialogTabType.Info, true)"
type="button" >
*ngIf="canEditCollection" <i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
bitMenuItem {{ "viewInfo" | i18n }}
(click)="editCollection(CollectionDialogTabType.Access)" </button>
> <button
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i> type="button"
{{ "access" | i18n }} bitMenuItem
</button> (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()"> <button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
<span class="tw-text-danger"> <span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i> <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>(); @Output() onAddCollection = new EventEmitter<void>();
/** Emits an event when the edit collection button is clicked in the header */ /** 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 */ /** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>(); @Output() onDeleteCollection = new EventEmitter<void>();
@ -64,7 +67,7 @@ export class VaultHeaderComponent implements OnInit {
protected CollectionDialogTabType = CollectionDialogTabType; protected CollectionDialogTabType = CollectionDialogTabType;
protected organizations$ = this.organizationService.organizations$; protected organizations$ = this.organizationService.organizations$;
private flexibleCollectionsV1Enabled = false; protected flexibleCollectionsV1Enabled = false;
private restrictProviderAccessFlag = false; private restrictProviderAccessFlag = false;
constructor( constructor(
@ -193,8 +196,8 @@ export class VaultHeaderComponent implements OnInit {
this.onAddCollection.emit(); this.onAddCollection.emit();
} }
async editCollection(tab: CollectionDialogTabType): Promise<void> { async editCollection(tab: CollectionDialogTabType, readonly: boolean): Promise<void> {
this.onEditCollection.emit({ tab }); this.onEditCollection.emit({ tab, readonly });
} }
get canDeleteCollection(): boolean { get canDeleteCollection(): boolean {
@ -207,6 +210,10 @@ export class VaultHeaderComponent implements OnInit {
return this.collection.node.canDelete(this.organization); return this.collection.node.canDelete(this.organization);
} }
get canViewCollectionInfo(): boolean {
return this.collection.node.canViewCollectionInfo(this.organization);
}
get canCreateCollection(): boolean { get canCreateCollection(): boolean {
return this.organization?.canCreateNewCollections; return this.organization?.canCreateNewCollections;
} }

View File

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

View File

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

View File

@ -8081,5 +8081,11 @@
}, },
"manageBillingFromProviderPortalMessage": { "manageBillingFromProviderPortalMessage": {
"message": "Manage billing from the Provider Portal" "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; : 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>) { static fromJSON(obj: Jsonify<CollectionView>) {
return Object.assign(new CollectionView(new Collection()), obj); return Object.assign(new CollectionView(new Collection()), obj);
} }