1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 14:53:33 +00:00

[AC-2170] Group modal - limit admin access - collections tab (#8758)

* Update Group modal -> Collections tab to respect collection management settings,
  e.g. only allow admins to assign access to collections they can manage
* Update collectionAdminView getters for custom permissions
This commit is contained in:
Thomas Rittson
2024-05-02 09:54:18 +10:00
committed by GitHub
parent 66d9ec19a3
commit 9dda5e8ee1
4 changed files with 140 additions and 82 deletions

View File

@@ -50,7 +50,12 @@
</bit-tab> </bit-tab>
<bit-tab label="{{ 'collections' | i18n }}"> <bit-tab label="{{ 'collections' | i18n }}">
<p>{{ "editGroupCollectionsDesc" | i18n }}</p> <p>
{{ "editGroupCollectionsDesc" | i18n }}
<span *ngIf="!(allowAdminAccessToAllCollectionItems$ | async)">
{{ "editGroupCollectionsRestrictionsDesc" | i18n }}
</span>
</p>
<div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3"> <div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3">
<input type="checkbox" formControlName="accessAll" id="accessAll" /> <input type="checkbox" formControlName="accessAll" id="accessAll" />
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{ <label class="tw-mb-0 tw-text-lg" for="accessAll">{{

View File

@@ -11,13 +11,13 @@ import {
of, of,
shareReplay, shareReplay,
Subject, Subject,
switchMap,
takeUntil, takeUntil,
} from "rxjs"; } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -26,12 +26,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
import { InternalGroupService as GroupService, GroupView } from "../core"; import { InternalGroupService as GroupService, GroupView } from "../core";
import { import {
AccessItemType, AccessItemType,
@@ -95,9 +93,15 @@ export const openGroupAddEditDialog = (
templateUrl: "group-add-edit.component.html", templateUrl: "group-add-edit.component.html",
}) })
export class GroupAddEditComponent implements OnInit, OnDestroy { export class GroupAddEditComponent implements OnInit, OnDestroy {
protected flexibleCollectionsEnabled$ = this.organizationService private organization$ = this.organizationService
.get$(this.organizationId) .get$(this.organizationId)
.pipe(map((o) => o?.flexibleCollections)); .pipe(shareReplay({ refCount: true }));
protected flexibleCollectionsEnabled$ = this.organization$.pipe(
map((o) => o?.flexibleCollections),
);
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
);
protected PermissionMode = PermissionMode; protected PermissionMode = PermissionMode;
protected ResultType = GroupAddEditDialogResultType; protected ResultType = GroupAddEditDialogResultType;
@@ -131,27 +135,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private get orgCollections$() { private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe(
return from(this.apiService.getCollections(this.organizationId)).pipe( shareReplay({ refCount: false }),
switchMap((response) => {
return from(
this.collectionService.decryptMany(
response.data.map(
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
),
),
); );
}),
map((collections) =>
collections.map<AccessItemView>((c) => ({
id: c.id,
type: AccessItemType.Collection,
labelName: c.name,
listName: c.name,
})),
),
);
}
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> { private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
@@ -197,23 +183,24 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
restrictGroupAccess$ = combineLatest([ allowAdminAccessToAllCollectionItems$ = combineLatest([
this.organizationService.get$(this.organizationId), this.organization$,
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), this.flexibleCollectionsV1Enabled$,
this.groupDetails$,
]).pipe( ]).pipe(
map( map(([organization, flexibleCollectionsV1Enabled]) => {
([organization, flexibleCollectionsV1Enabled, group]) => if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) {
// Feature flag conditionals return true;
flexibleCollectionsV1Enabled && }
organization.flexibleCollections &&
// Business logic conditionals return organization.allowAdminAccessToAllCollectionItems;
!organization.allowAdminAccessToAllCollectionItems && }),
group !== undefined,
),
shareReplay({ refCount: true, bufferSize: 1 }),
); );
restrictGroupAccess$ = combineLatest([
this.allowAdminAccessToAllCollectionItems$,
this.groupDetails$,
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
constructor( constructor(
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
private dialogRef: DialogRef<GroupAddEditDialogResultType>, private dialogRef: DialogRef<GroupAddEditDialogResultType>,
@@ -221,7 +208,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private organizationUserService: OrganizationUserService, private organizationUserService: OrganizationUserService,
private groupService: GroupService, private groupService: GroupService,
private i18nService: I18nService, private i18nService: I18nService,
private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private logService: LogService, private logService: LogService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@@ -230,6 +216,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService, private organizationService: OrganizationService,
private configService: ConfigService, private configService: ConfigService,
private accountService: AccountService, private accountService: AccountService,
private collectionAdminService: CollectionAdminService,
) { ) {
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
} }
@@ -244,12 +231,28 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
this.groupDetails$, this.groupDetails$,
this.restrictGroupAccess$, this.restrictGroupAccess$,
this.accountService.activeAccount$, this.accountService.activeAccount$,
this.organization$,
this.flexibleCollectionsV1Enabled$,
]) ])
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => { .subscribe(
this.collections = collections; ([
collections,
members,
group,
restrictGroupAccess,
activeAccount,
organization,
flexibleCollectionsV1Enabled,
]) => {
this.members = members; this.members = members;
this.group = group; this.group = group;
this.collections = mapToAccessItemViews(
collections,
organization,
flexibleCollectionsV1Enabled,
group,
);
if (this.group != undefined) { if (this.group != undefined) {
// Must detect changes so that AccessSelector @Inputs() are aware of the latest // Must detect changes so that AccessSelector @Inputs() are aware of the latest
@@ -264,11 +267,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
id: m, id: m,
type: AccessItemType.Member, type: AccessItemType.Member,
})), })),
collections: this.group.collections.map((gc) => ({ collections: mapToAccessSelections(group, this.collections),
id: gc.id,
type: AccessItemType.Collection,
permission: convertToPermission(gc),
})),
}); });
} }
@@ -285,7 +284,8 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
} }
this.loading = false; this.loading = false;
}); },
);
} }
ngOnDestroy() { ngOnDestroy() {
@@ -355,3 +355,46 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
this.dialogRef.close(GroupAddEditDialogResultType.Deleted); this.dialogRef.close(GroupAddEditDialogResultType.Deleted);
}; };
} }
/**
* Maps the group's current collection access to AccessItemValues to populate the access-selector's FormControl
*/
function mapToAccessSelections(group: GroupView, items: AccessItemView[]): AccessItemValue[] {
return (
group.collections
// The FormControl value only represents editable collection access - exclude readonly access selections
.filter((selection) => !items.find((item) => item.id == selection.id).readonly)
.map((gc) => ({
id: gc.id,
type: AccessItemType.Collection,
permission: convertToPermission(gc),
}))
);
}
/**
* Maps the organization's collections to AccessItemViews to populate the access-selector's multi-select
*/
function mapToAccessItemViews(
collections: CollectionAdminView[],
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
group?: GroupView,
): AccessItemView[] {
return (
collections
.map<AccessItemView>((c) => {
const accessSelection = group?.collections.find((access) => access.id == c.id) ?? undefined;
return {
id: c.id,
type: AccessItemType.Collection,
labelName: c.name,
listName: c.name,
readonly: !c.canEditGroupAccess(organization, flexibleCollectionsV1Enabled),
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
};
})
// Remove any collection views that are not already assigned and that we don't have permissions to assign access to
.filter((item) => !item.readonly || group?.collections.some((access) => access.id == item.id))
);
}

View File

@@ -51,6 +51,13 @@ export class CollectionAdminView extends CollectionView {
* Whether the user can modify user access to this collection * Whether the user can modify user access to this collection
*/ */
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.canManageUsers; return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers;
}
/**
* Whether the user can modify group access to this collection
*/
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
} }
} }

View File

@@ -6480,6 +6480,9 @@
"editGroupCollectionsDesc": { "editGroupCollectionsDesc": {
"message": "Grant access to collections by adding them to this group." "message": "Grant access to collections by adding them to this group."
}, },
"editGroupCollectionsRestrictionsDesc": {
"message": "You can only assign collections you manage."
},
"accessAllCollectionsDesc": { "accessAllCollectionsDesc": {
"message": "Grant access to all current and future collections." "message": "Grant access to all current and future collections."
}, },