diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 27a3f9fc669..46d8c6a8671 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -16,7 +16,10 @@ > {{ "loading" | i18n }} - +

{{ "inviteUserDesc" | i18n }}

@@ -60,7 +63,10 @@ -
+
- +

{{ "secretsManager" | i18n }}
{{ "userPermissionOverrideHelper" | i18n }}
-
+
@@ -434,7 +440,7 @@ [columnHeader]="'collection' | i18n" [selectorLabelText]="'selectCollections' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n" - [flexibleCollectionsEnabled]="flexibleCollectionsEnabled" + [flexibleCollectionsEnabled]="organization.flexibleCollections" > diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 5461aecfcc1..4d6442e8988 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -1,7 +1,16 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { Component, Inject, OnDestroy } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + Observable, + of, + shareReplay, + Subject, + switchMap, + takeUntil, +} from "rxjs"; 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"; @@ -18,7 +27,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { DialogService } from "@bitwarden/components"; -import { flagEnabled } from "../../../../../../utils/flags"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAccessSelectionView, @@ -66,7 +74,7 @@ export enum MemberDialogResult { @Component({ templateUrl: "member-dialog.component.html", }) -export class MemberDialogComponent implements OnInit, OnDestroy { +export class MemberDialogComponent implements OnDestroy { loading = true; editMode = false; isRevoked = false; @@ -74,12 +82,10 @@ export class MemberDialogComponent implements OnInit, OnDestroy { access: "all" | "selected" = "selected"; collections: CollectionView[] = []; organizationUserType = OrganizationUserType; - canUseCustomPermissions: boolean; PermissionMode = PermissionMode; - canUseSecretsManager: boolean; showNoMasterPasswordWarning = false; - protected organization: Organization; + protected organization$: Observable; protected collectionAccessItems: AccessItemView[] = []; protected groupAccessItems: AccessItemView[] = []; protected tabIndex: MemberDialogTab; @@ -130,7 +136,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy { private dialogRef: DialogRef, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private organizationService: OrganizationService, private formBuilder: FormBuilder, // TODO: We should really look into consolidating naming conventions for these services private collectionAdminService: CollectionAdminService, @@ -139,28 +144,26 @@ export class MemberDialogComponent implements OnInit, OnDestroy { private organizationUserService: OrganizationUserService, private dialogService: DialogService, private configService: ConfigServiceAbstraction, - ) {} + organizationService: OrganizationService, + ) { + this.organization$ = organizationService + .get$(this.params.organizationId) + .pipe(shareReplay({ refCount: true, bufferSize: 1 })); - async ngOnInit() { this.editMode = this.params.organizationUserId != null; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); - const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe( - shareReplay({ refCount: true, bufferSize: 1 }), - ); - const groups$ = organization$.pipe( - switchMap((organization) => { - if (!organization.useGroups) { - return of([] as GroupView[]); - } - - return this.groupService.getAll(this.params.organizationId); - }), + const groups$ = this.organization$.pipe( + switchMap((organization) => + organization.useGroups + ? this.groupService.getAll(this.params.organizationId) + : of([] as GroupView[]), + ), ); combineLatest({ - organization: organization$, + organization: this.organization$, collections: this.collectionAdminService.getAll(this.params.organizationId), userDetails: this.params.organizationUserId ? this.userService.get(this.params.organizationId, this.params.organizationUserId) @@ -169,23 +172,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy { }) .pipe(takeUntil(this.destroy$)) .subscribe(({ organization, collections, userDetails, groups }) => { - this.organization = organization; - this.canUseCustomPermissions = organization.useCustomPermissions; - this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager"); - - const emailsControlValidators = [ - Validators.required, - commaSeparatedEmails, - orgSeatLimitReachedValidator( - this.organization, - this.params.allOrganizationUserEmails, - this.i18nService.t("subscriptionUpgrade", organization.seats), - ), - ]; - - const emailsControl = this.formGroup.get("emails"); - emailsControl.setValidators(emailsControlValidators); - emailsControl.updateValueAndValidity(); + this.setFormValidators(organization); this.collectionAccessItems = [].concat( collections.map((c) => mapCollectionToAccessItemView(c)), @@ -196,77 +183,101 @@ export class MemberDialogComponent implements OnInit, OnDestroy { ); if (this.params.organizationUserId) { - if (!userDetails) { - throw new Error("Could not find user to edit."); - } - this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked; - this.showNoMasterPasswordWarning = - userDetails.status > OrganizationUserStatusType.Invited && - userDetails.hasMasterPassword === false; - const assignedCollectionsPermissions = { - editAssignedCollections: userDetails.permissions.editAssignedCollections, - deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections, - manageAssignedCollections: - userDetails.permissions.editAssignedCollections && - userDetails.permissions.deleteAssignedCollections, - }; - const allCollectionsPermissions = { - createNewCollections: userDetails.permissions.createNewCollections, - editAnyCollection: userDetails.permissions.editAnyCollection, - deleteAnyCollection: userDetails.permissions.deleteAnyCollection, - manageAllCollections: - userDetails.permissions.createNewCollections && - userDetails.permissions.editAnyCollection && - userDetails.permissions.deleteAnyCollection, - }; - if (userDetails.type === OrganizationUserType.Custom) { - this.permissionsGroup.patchValue({ - accessEventLogs: userDetails.permissions.accessEventLogs, - accessImportExport: userDetails.permissions.accessImportExport, - accessReports: userDetails.permissions.accessReports, - manageGroups: userDetails.permissions.manageGroups, - manageSso: userDetails.permissions.manageSso, - managePolicies: userDetails.permissions.managePolicies, - manageUsers: userDetails.permissions.manageUsers, - manageResetPassword: userDetails.permissions.manageResetPassword, - manageAssignedCollectionsGroup: assignedCollectionsPermissions, - manageAllCollectionsGroup: allCollectionsPermissions, - }); - } - - const collectionsFromGroups = groups - .filter((group) => userDetails.groups.includes(group.id)) - .flatMap((group) => - group.collections.map((accessSelection) => { - const collection = collections.find((c) => c.id === accessSelection.id); - return { group, collection, accessSelection }; - }), - ); - - this.collectionAccessItems = this.collectionAccessItems.concat( - collectionsFromGroups.map(({ collection, accessSelection, group }) => - mapCollectionToAccessItemView(collection, accessSelection, group), - ), - ); - - const accessSelections = mapToAccessSelections(userDetails); - const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); - - this.formGroup.removeControl("emails"); - this.formGroup.patchValue({ - type: userDetails.type, - externalId: userDetails.externalId, - accessAllCollections: userDetails.accessAll, - access: accessSelections, - accessSecretsManager: userDetails.accessSecretsManager, - groups: groupAccessSelections, - }); + this.loadOrganizationUser(userDetails, groups, collections); } this.loading = false; }); } + private setFormValidators(organization: Organization) { + const emailsControlValidators = [ + Validators.required, + commaSeparatedEmails, + orgSeatLimitReachedValidator( + organization, + this.params.allOrganizationUserEmails, + this.i18nService.t("subscriptionUpgrade", organization.seats), + ), + ]; + + const emailsControl = this.formGroup.get("emails"); + emailsControl.setValidators(emailsControlValidators); + emailsControl.updateValueAndValidity(); + } + + private loadOrganizationUser( + userDetails: OrganizationUserAdminView, + groups: GroupView[], + collections: CollectionView[], + ) { + if (!userDetails) { + throw new Error("Could not find user to edit."); + } + this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked; + this.showNoMasterPasswordWarning = + userDetails.status > OrganizationUserStatusType.Invited && + userDetails.hasMasterPassword === false; + const assignedCollectionsPermissions = { + editAssignedCollections: userDetails.permissions.editAssignedCollections, + deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections, + manageAssignedCollections: + userDetails.permissions.editAssignedCollections && + userDetails.permissions.deleteAssignedCollections, + }; + const allCollectionsPermissions = { + createNewCollections: userDetails.permissions.createNewCollections, + editAnyCollection: userDetails.permissions.editAnyCollection, + deleteAnyCollection: userDetails.permissions.deleteAnyCollection, + manageAllCollections: + userDetails.permissions.createNewCollections && + userDetails.permissions.editAnyCollection && + userDetails.permissions.deleteAnyCollection, + }; + if (userDetails.type === OrganizationUserType.Custom) { + this.permissionsGroup.patchValue({ + accessEventLogs: userDetails.permissions.accessEventLogs, + accessImportExport: userDetails.permissions.accessImportExport, + accessReports: userDetails.permissions.accessReports, + manageGroups: userDetails.permissions.manageGroups, + manageSso: userDetails.permissions.manageSso, + managePolicies: userDetails.permissions.managePolicies, + manageUsers: userDetails.permissions.manageUsers, + manageResetPassword: userDetails.permissions.manageResetPassword, + manageAssignedCollectionsGroup: assignedCollectionsPermissions, + manageAllCollectionsGroup: allCollectionsPermissions, + }); + } + + const collectionsFromGroups = groups + .filter((group) => userDetails.groups.includes(group.id)) + .flatMap((group) => + group.collections.map((accessSelection) => { + const collection = collections.find((c) => c.id === accessSelection.id); + return { group, collection, accessSelection }; + }), + ); + + this.collectionAccessItems = this.collectionAccessItems.concat( + collectionsFromGroups.map(({ collection, accessSelection, group }) => + mapCollectionToAccessItemView(collection, accessSelection, group), + ), + ); + + const accessSelections = mapToAccessSelections(userDetails); + const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); + + this.formGroup.removeControl("emails"); + this.formGroup.patchValue({ + type: userDetails.type, + externalId: userDetails.externalId, + accessAllCollections: userDetails.accessAll, + access: accessSelections, + accessSecretsManager: userDetails.accessSecretsManager, + groups: groupAccessSelections, + }); + } + check(c: CollectionView, select?: boolean) { (c as any).checked = select == null ? !(c as any).checked : select; if (!(c as any).checked) { @@ -335,7 +346,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy { return; } - if (!this.canUseCustomPermissions && this.customUserTypeSelected) { + const organization = await firstValueFrom(this.organization$); + + if (!organization.useCustomPermissions && this.customUserTypeSelected) { this.platformUtilsService.showToast( "error", null, @@ -363,8 +376,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy { await this.userService.save(userView); } else { userView.id = this.params.organizationUserId; - const maxEmailsCount = - this.organization.planProductType === ProductType.TeamsStarter ? 10 : 20; + const maxEmailsCount = organization.planProductType === ProductType.TeamsStarter ? 10 : 20; const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; if (emails.length > maxEmailsCount) { this.formGroup.controls.emails.setErrors({ @@ -373,8 +385,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy { return; } if ( - this.organization.hasReseller && - this.params.numConfirmedMembers + emails.length > this.organization.seats + organization.hasReseller && + this.params.numConfirmedMembers + emails.length > organization.seats ) { this.formGroup.controls.emails.setErrors({ tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") }, @@ -515,10 +527,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy { }); } - protected get flexibleCollectionsEnabled() { - return this.organization?.flexibleCollections; - } - protected readonly ProductType = ProductType; }