[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,6 +95,8 @@
[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)"

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);
} }