mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[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:
parent
fb3766b6c1
commit
8e97c1c8e4
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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[] }
|
||||||
|
@ -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))"
|
||||||
|
@ -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()
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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: {
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user