[AC-2169] Group modal - limit admin access - members tab (#8650)
* Restrict user from adding themselves to existing group
This commit is contained in:
parent
65f1bd2e3a
commit
f45eec1a4f
|
@ -31,7 +31,12 @@
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
|
|
||||||
<bit-tab label="{{ 'members' | i18n }}">
|
<bit-tab label="{{ 'members' | i18n }}">
|
||||||
<p>{{ "editGroupMembersDesc" | i18n }}</p>
|
<p>
|
||||||
|
{{ "editGroupMembersDesc" | i18n }}
|
||||||
|
<span *ngIf="restrictGroupAccess$ | async">
|
||||||
|
{{ "restrictedGroupAccessDesc" | i18n }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
<bit-access-selector
|
<bit-access-selector
|
||||||
formControlName="members"
|
formControlName="members"
|
||||||
[items]="members"
|
[items]="members"
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil } from "rxjs";
|
import {
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
from,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
shareReplay,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
} 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
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";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||||
|
@ -88,10 +104,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
tabIndex: GroupAddEditTabType;
|
tabIndex: GroupAddEditTabType;
|
||||||
loading = true;
|
loading = true;
|
||||||
editMode = false;
|
|
||||||
title: string;
|
title: string;
|
||||||
collections: AccessItemView[] = [];
|
collections: AccessItemView[] = [];
|
||||||
members: AccessItemView[] = [];
|
members: Array<AccessItemView & { userId: UserId }> = [];
|
||||||
group: GroupView;
|
group: GroupView;
|
||||||
|
|
||||||
groupForm = this.formBuilder.group({
|
groupForm = this.formBuilder.group({
|
||||||
|
@ -110,6 +125,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
return this.params.organizationId;
|
return this.params.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get editMode(): boolean {
|
||||||
|
return this.groupId != null;
|
||||||
|
}
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
private get orgCollections$() {
|
private get orgCollections$() {
|
||||||
|
@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get orgMembers$() {
|
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
||||||
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
|
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
|
||||||
map((response) =>
|
map((response) =>
|
||||||
response.data.map((m) => ({
|
response.data.map((m) => ({
|
||||||
|
@ -145,34 +164,55 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email,
|
listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email,
|
||||||
labelName: m.name || m.email,
|
labelName: m.name || m.email,
|
||||||
status: m.status,
|
status: m.status,
|
||||||
|
userId: m.userId as UserId,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get groupDetails$() {
|
private groupDetails$: Observable<GroupView | undefined> = of(this.editMode).pipe(
|
||||||
if (!this.editMode) {
|
concatMap((editMode) => {
|
||||||
return of(undefined);
|
if (!editMode) {
|
||||||
}
|
|
||||||
|
|
||||||
return combineLatest([
|
|
||||||
this.groupService.get(this.organizationId, this.groupId),
|
|
||||||
this.apiService.getGroupUsers(this.organizationId, this.groupId),
|
|
||||||
]).pipe(
|
|
||||||
map(([groupView, users]) => {
|
|
||||||
groupView.members = users;
|
|
||||||
return groupView;
|
|
||||||
}),
|
|
||||||
catchError((e: unknown) => {
|
|
||||||
if (e instanceof ErrorResponse) {
|
|
||||||
this.logService.error(e.message);
|
|
||||||
} else {
|
|
||||||
this.logService.error(e.toString());
|
|
||||||
}
|
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
}
|
return combineLatest([
|
||||||
|
this.groupService.get(this.organizationId, this.groupId),
|
||||||
|
this.apiService.getGroupUsers(this.organizationId, this.groupId),
|
||||||
|
]).pipe(
|
||||||
|
map(([groupView, users]) => {
|
||||||
|
groupView.members = users;
|
||||||
|
return groupView;
|
||||||
|
}),
|
||||||
|
catchError((e: unknown) => {
|
||||||
|
if (e instanceof ErrorResponse) {
|
||||||
|
this.logService.error(e.message);
|
||||||
|
} else {
|
||||||
|
this.logService.error(e.toString());
|
||||||
|
}
|
||||||
|
return of(undefined);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
shareReplay({ refCount: false }),
|
||||||
|
);
|
||||||
|
|
||||||
|
restrictGroupAccess$ = combineLatest([
|
||||||
|
this.organizationService.get$(this.organizationId),
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||||
|
this.groupDetails$,
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([organization, flexibleCollectionsV1Enabled, group]) =>
|
||||||
|
// Feature flag conditionals
|
||||||
|
flexibleCollectionsV1Enabled &&
|
||||||
|
organization.flexibleCollections &&
|
||||||
|
// Business logic conditionals
|
||||||
|
!organization.allowAdminAccessToAllCollectionItems &&
|
||||||
|
group !== undefined,
|
||||||
|
),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||||
|
@ -188,17 +228,25 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.editMode = this.loading = this.groupId != null;
|
this.loading = true;
|
||||||
this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup");
|
this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup");
|
||||||
|
|
||||||
combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$])
|
combineLatest([
|
||||||
|
this.orgCollections$,
|
||||||
|
this.orgMembers$,
|
||||||
|
this.groupDetails$,
|
||||||
|
this.restrictGroupAccess$,
|
||||||
|
this.accountService.activeAccount$,
|
||||||
|
])
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(([collections, members, group]) => {
|
.subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => {
|
||||||
this.collections = collections;
|
this.collections = collections;
|
||||||
this.members = members;
|
this.members = members;
|
||||||
this.group = group;
|
this.group = group;
|
||||||
|
@ -224,6 +272,18 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the current user is not already in the group and cannot add themselves, remove them from the list
|
||||||
|
if (restrictGroupAccess) {
|
||||||
|
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
|
||||||
|
const isAlreadyInGroup = this.groupForm.value.members.some(
|
||||||
|
(m) => m.id === organizationUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAlreadyInGroup) {
|
||||||
|
this.members = this.members.filter((m) => m.id !== organizationUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7905,5 +7905,8 @@
|
||||||
},
|
},
|
||||||
"unassignedItemsBannerSelfHost": {
|
"unassignedItemsBannerSelfHost": {
|
||||||
"message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
|
"message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
|
||||||
|
},
|
||||||
|
"restrictedGroupAccessDesc": {
|
||||||
|
"message": "You cannot add yourself to a group."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,7 +297,6 @@ export abstract class ApiService {
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
|
|
||||||
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
||||||
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
|
|
||||||
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
||||||
|
|
||||||
getSync: () => Promise<SyncResponse>;
|
getSync: () => Promise<SyncResponse>;
|
||||||
|
|
|
@ -866,16 +866,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
async putGroupUsers(organizationId: string, id: string, request: string[]): Promise<any> {
|
|
||||||
await this.send(
|
|
||||||
"PUT",
|
|
||||||
"/organizations/" + organizationId + "/groups/" + id + "/users",
|
|
||||||
request,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> {
|
deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> {
|
||||||
return this.send(
|
return this.send(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
|
|
Loading…
Reference in New Issue