[AC-2172] Member modal - limit admin access (#8343)

* limit admin permissions to assign members to collections that the admin
doesn't have can manage permissions for
This commit is contained in:
Thomas Rittson 2024-04-17 11:03:48 +10:00 committed by GitHub
parent f45eec1a4f
commit 4db383850f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 30 deletions

View File

@ -31,6 +31,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view";
import { import {
CollectionAccessSelectionView, CollectionAccessSelectionView,
GroupService, GroupService,
@ -206,25 +207,52 @@ export class MemberDialogComponent implements OnDestroy {
collections: this.collectionAdminService.getAll(this.params.organizationId), collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: userDetails$, userDetails: userDetails$,
groups: groups$, groups: groups$,
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
),
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(({ organization, collections, userDetails, groups }) => { .subscribe(
this.setFormValidators(organization); ({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => {
this.setFormValidators(organization);
this.collectionAccessItems = [].concat( // Groups tab: populate available groups
collections.map((c) => mapCollectionToAccessItemView(c)), this.groupAccessItems = [].concat(
); groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)),
);
this.groupAccessItems = [].concat( // Collections tab: Populate all available collections (including current user access where applicable)
groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)), this.collectionAccessItems = collections
); .map((c) =>
mapCollectionToAccessItemView(
c,
organization,
flexibleCollectionsV1Enabled,
userDetails == null
? undefined
: c.users.find((access) => access.id === userDetails.id),
),
)
// But remove collections that we can't assign access to, unless the user is already assigned
.filter(
(item) =>
!item.readonly || userDetails?.collections.some((access) => access.id == item.id),
);
if (this.params.organizationUserId) { if (userDetails != null) {
this.loadOrganizationUser(userDetails, groups, collections); this.loadOrganizationUser(
} userDetails,
groups,
collections,
organization,
flexibleCollectionsV1Enabled,
);
}
this.loading = false; this.loading = false;
}); },
);
} }
private setFormValidators(organization: Organization) { private setFormValidators(organization: Organization) {
@ -246,7 +274,9 @@ export class MemberDialogComponent implements OnDestroy {
private loadOrganizationUser( private loadOrganizationUser(
userDetails: OrganizationUserAdminView, userDetails: OrganizationUserAdminView,
groups: GroupView[], groups: GroupView[],
collections: CollectionView[], collections: CollectionAdminView[],
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
) { ) {
if (!userDetails) { if (!userDetails) {
throw new Error("Could not find user to edit."); throw new Error("Could not find user to edit.");
@ -295,13 +325,22 @@ export class MemberDialogComponent implements OnDestroy {
}), }),
); );
// Populate additional collection access via groups (rendered as separate rows from user access)
this.collectionAccessItems = this.collectionAccessItems.concat( this.collectionAccessItems = this.collectionAccessItems.concat(
collectionsFromGroups.map(({ collection, accessSelection, group }) => collectionsFromGroups.map(({ collection, accessSelection, group }) =>
mapCollectionToAccessItemView(collection, accessSelection, group), mapCollectionToAccessItemView(
collection,
organization,
flexibleCollectionsV1Enabled,
accessSelection,
group,
),
), ),
); );
const accessSelections = mapToAccessSelections(userDetails); // Set current collections and groups the user has access to (excluding collections the current user doesn't have
// permissions to change - they are included as readonly via the CollectionAccessItems)
const accessSelections = mapToAccessSelections(userDetails, this.collectionAccessItems);
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
this.formGroup.removeControl("emails"); this.formGroup.removeControl("emails");
@ -573,6 +612,8 @@ export class MemberDialogComponent implements OnDestroy {
function mapCollectionToAccessItemView( function mapCollectionToAccessItemView(
collection: CollectionView, collection: CollectionView,
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
accessSelection?: CollectionAccessSelectionView, accessSelection?: CollectionAccessSelectionView,
group?: GroupView, group?: GroupView,
): AccessItemView { ): AccessItemView {
@ -581,7 +622,8 @@ function mapCollectionToAccessItemView(
id: group ? `${collection.id}-${group.id}` : collection.id, id: group ? `${collection.id}-${group.id}` : collection.id,
labelName: collection.name, labelName: collection.name,
listName: collection.name, listName: collection.name,
readonly: group !== undefined, readonly:
group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled),
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
viaGroupName: group?.name, viaGroupName: group?.name,
}; };
@ -596,16 +638,23 @@ function mapGroupToAccessItemView(group: GroupView): AccessItemView {
}; };
} }
function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] { function mapToAccessSelections(
user: OrganizationUserAdminView,
items: AccessItemView[],
): AccessItemValue[] {
if (user == undefined) { if (user == undefined) {
return []; return [];
} }
return [].concat(
user.collections.map<AccessItemValue>((selection) => ({ return (
id: selection.id, user.collections
type: AccessItemType.Collection, // The FormControl value only represents editable collection access - exclude readonly access selections
permission: convertToPermission(selection), .filter((selection) => !items.find((item) => item.id == selection.id).readonly)
})), .map<AccessItemValue>((selection) => ({
id: selection.id,
type: AccessItemType.Collection,
permission: convertToPermission(selection),
}))
); );
} }

View File

@ -124,6 +124,9 @@ export class CollectionAdminService {
view.groups = c.groups; view.groups = c.groups;
view.users = c.users; view.users = c.users;
view.assigned = c.assigned; view.assigned = c.assigned;
view.readOnly = c.readOnly;
view.hidePasswords = c.hidePasswords;
view.manage = c.manage;
} }
return view; return view;

View File

@ -21,6 +21,10 @@ export class CollectionDetailsResponse extends CollectionResponse {
readOnly: boolean; readOnly: boolean;
manage: boolean; manage: boolean;
hidePasswords: boolean; hidePasswords: boolean;
/**
* Flag indicating the user has been explicitly assigned to this Collection
*/
assigned: boolean; assigned: boolean;
constructor(response: any) { constructor(response: any) {
@ -35,15 +39,10 @@ export class CollectionDetailsResponse extends CollectionResponse {
} }
} }
export class CollectionAccessDetailsResponse extends CollectionResponse { export class CollectionAccessDetailsResponse extends CollectionDetailsResponse {
groups: SelectionReadOnlyResponse[] = []; groups: SelectionReadOnlyResponse[] = [];
users: SelectionReadOnlyResponse[] = []; users: SelectionReadOnlyResponse[] = [];
/**
* Flag indicating the user has been explicitly assigned to this Collection
*/
assigned: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.assigned = this.getResponseProperty("Assigned") || false; this.assigned = this.getResponseProperty("Assigned") || false;