diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.spec.ts b/apps/web/src/app/admin-console/common/people-table-data-source.spec.ts new file mode 100644 index 00000000000..e9cf87a114d --- /dev/null +++ b/apps/web/src/app/admin-console/common/people-table-data-source.spec.ts @@ -0,0 +1,130 @@ +import { TestBed } from "@angular/core/testing"; +import { ReplaySubject } from "rxjs"; + +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { + Environment, + EnvironmentService, +} from "@bitwarden/common/platform/abstractions/environment.service"; + +import { PeopleTableDataSource } from "./people-table-data-source"; + +interface MockUser { + id: string; + name: string; + email: string; + status: OrganizationUserStatusType; + checked?: boolean; +} + +class TestPeopleTableDataSource extends PeopleTableDataSource { + protected statusType = OrganizationUserStatusType; +} + +describe("PeopleTableDataSource", () => { + let dataSource: TestPeopleTableDataSource; + + const createMockUser = (id: string, checked: boolean = false): MockUser => ({ + id, + name: `User ${id}`, + email: `user${id}@example.com`, + status: OrganizationUserStatusType.Confirmed, + checked, + }); + + const createMockUsers = (count: number, checked: boolean = false): MockUser[] => { + return Array.from({ length: count }, (_, i) => createMockUser(`${i + 1}`, checked)); + }; + + beforeEach(() => { + const featureFlagSubject = new ReplaySubject(1); + featureFlagSubject.next(false); + + const environmentSubject = new ReplaySubject(1); + environmentSubject.next({ + isCloud: () => false, + } as Environment); + + const mockConfigService = { + getFeatureFlag$: jest.fn(() => featureFlagSubject.asObservable()), + } as any; + + const mockEnvironmentService = { + environment$: environmentSubject.asObservable(), + } as any; + + TestBed.configureTestingModule({ + providers: [ + { provide: ConfigService, useValue: mockConfigService }, + { provide: EnvironmentService, useValue: mockEnvironmentService }, + ], + }); + + dataSource = TestBed.runInInjectionContext( + () => new TestPeopleTableDataSource(mockConfigService, mockEnvironmentService), + ); + }); + + describe("limitAndUncheckExcess", () => { + it("should return all users when under limit", () => { + const users = createMockUsers(10, true); + dataSource.data = users; + + const result = dataSource.limitAndUncheckExcess(users, 500); + + expect(result).toHaveLength(10); + expect(result).toEqual(users); + expect(users.every((u) => u.checked)).toBe(true); + }); + + it("should limit users and uncheck excess", () => { + const users = createMockUsers(600, true); + dataSource.data = users; + + const result = dataSource.limitAndUncheckExcess(users, 500); + + expect(result).toHaveLength(500); + expect(result).toEqual(users.slice(0, 500)); + expect(users.slice(0, 500).every((u) => u.checked)).toBe(true); + expect(users.slice(500).every((u) => u.checked)).toBe(false); + }); + + it("should only affect users in the provided array", () => { + const allUsers = createMockUsers(1000, true); + dataSource.data = allUsers; + + // Pass only a subset (simulates filtering by status) + const subset = allUsers.slice(0, 600); + + const result = dataSource.limitAndUncheckExcess(subset, 500); + + expect(result).toHaveLength(500); + expect(subset.slice(0, 500).every((u) => u.checked)).toBe(true); + expect(subset.slice(500).every((u) => u.checked)).toBe(false); + // Users outside subset remain checked + expect(allUsers.slice(600).every((u) => u.checked)).toBe(true); + }); + }); + + describe("status counts", () => { + it("should correctly count users by status", () => { + const users: MockUser[] = [ + { ...createMockUser("1"), status: OrganizationUserStatusType.Invited }, + { ...createMockUser("2"), status: OrganizationUserStatusType.Invited }, + { ...createMockUser("3"), status: OrganizationUserStatusType.Accepted }, + { ...createMockUser("4"), status: OrganizationUserStatusType.Confirmed }, + { ...createMockUser("5"), status: OrganizationUserStatusType.Confirmed }, + { ...createMockUser("6"), status: OrganizationUserStatusType.Confirmed }, + { ...createMockUser("7"), status: OrganizationUserStatusType.Revoked }, + ]; + dataSource.data = users; + + expect(dataSource.invitedUserCount).toBe(2); + expect(dataSource.acceptedUserCount).toBe(1); + expect(dataSource.confirmedUserCount).toBe(3); + expect(dataSource.revokedUserCount).toBe(1); + expect(dataSource.activeUserCount).toBe(6); // All except revoked + }); + }); +}); diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts index 4696f8a6738..0228edb1e8c 100644 --- a/apps/web/src/app/admin-console/common/people-table-data-source.ts +++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts @@ -1,14 +1,30 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { computed, Signal } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + import { OrganizationUserStatusType, ProviderUserStatusType, } from "@bitwarden/common/admin-console/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { TableDataSource } from "@bitwarden/components"; import { StatusType, UserViewTypes } from "./base-members.component"; -const MaxCheckedCount = 500; +/** + * Default maximum for most bulk operations (confirm, remove, delete, etc.) + */ +export const MaxCheckedCount = 500; + +/** + * Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud + * feature flag is enabled on cloud environments. + */ +export const CloudBulkReinviteLimit = 4000; /** * Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked). @@ -56,6 +72,20 @@ export abstract class PeopleTableDataSource extends Tab confirmedUserCount: number; revokedUserCount: number; + /** True when increased bulk limit feature is enabled (feature flag + cloud environment) */ + readonly isIncreasedBulkLimitEnabled: Signal; + + constructor(configService: ConfigService, environmentService: EnvironmentService) { + super(); + + const featureFlagEnabled = toSignal( + configService.getFeatureFlag$(FeatureFlag.IncreaseBulkReinviteLimitForCloud), + ); + const isCloud = toSignal(environmentService.environment$.pipe(map((env) => env.isCloud()))); + + this.isIncreasedBulkLimitEnabled = computed(() => featureFlagEnabled() && isCloud()); + } + override set data(data: T[]) { super.data = data; @@ -89,6 +119,14 @@ export abstract class PeopleTableDataSource extends Tab return this.data.filter((u) => (u as any).checked); } + /** + * Gets checked users in the order they appear in the filtered/sorted table view. + * Use this when enforcing limits to ensure visual consistency (top N visible rows stay checked). + */ + getCheckedUsersInVisibleOrder() { + return this.filteredData.filter((u) => (u as any).checked); + } + /** * Check all filtered users (i.e. those rows that are currently visible) * @param select check the filtered users (true) or uncheck the filtered users (false) @@ -101,8 +139,13 @@ export abstract class PeopleTableDataSource extends Tab const filteredUsers = this.filteredData; - const selectCount = - filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length; + // When the increased bulk limit feature is enabled, allow checking all users. + // Individual bulk operations will enforce their specific limits. + // When disabled, enforce the legacy limit at check time. + const selectCount = this.isIncreasedBulkLimitEnabled() + ? filteredUsers.length + : Math.min(filteredUsers.length, MaxCheckedCount); + for (let i = 0; i < selectCount; i++) { this.checkUser(filteredUsers[i], select); } @@ -132,4 +175,41 @@ export abstract class PeopleTableDataSource extends Tab this.data = updatedData; } } + + /** + * Limits an array of users and unchecks those beyond the limit. + * Returns the limited array. + * + * @param users The array of users to limit + * @param limit The maximum number of users to keep + * @returns The users array limited to the specified count + */ + limitAndUncheckExcess(users: T[], limit: number): T[] { + if (users.length <= limit) { + return users; + } + + // Uncheck users beyond the limit + users.slice(limit).forEach((user) => this.checkUser(user, false)); + + return users.slice(0, limit); + } + + /** + * Gets checked users with optional limiting based on the IncreaseBulkReinviteLimitForCloud feature flag. + * + * When the feature flag is enabled: Returns checked users in visible order, limited to the specified count. + * When the feature flag is disabled: Returns all checked users without applying any limit. + * + * @param limit The maximum number of users to return (only applied when feature flag is enabled) + * @returns The checked users array + */ + getCheckedUsersWithLimit(limit: number): T[] { + if (this.isIncreasedBulkLimitEnabled()) { + const allUsers = this.getCheckedUsersInVisibleOrder(); + return this.limitAndUncheckExcess(allUsers, limit); + } else { + return this.getCheckedUsers(); + } + } } diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 59c4c4898ea..ac25278a636 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -33,6 +33,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -44,7 +46,11 @@ import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/membe import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { BaseMembersComponent } from "../../common/base-members.component"; -import { PeopleTableDataSource } from "../../common/people-table-data-source"; +import { + CloudBulkReinviteLimit, + MaxCheckedCount, + PeopleTableDataSource, +} from "../../common/people-table-data-source"; import { OrganizationUserView } from "../core/views/organization-user.view"; import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; @@ -70,7 +76,7 @@ export class MembersComponent extends BaseMembersComponent userType = OrganizationUserType; userStatusType = OrganizationUserStatusType; memberTab = MemberDialogTab; - protected dataSource = new MembersTableDataSource(); + protected dataSource: MembersTableDataSource; readonly organization: Signal; status: OrganizationUserStatusType | undefined; @@ -113,6 +119,8 @@ export class MembersComponent extends BaseMembersComponent private policyService: PolicyService, private policyApiService: PolicyApiServiceAbstraction, private organizationMetadataService: OrganizationMetadataServiceAbstraction, + private configService: ConfigService, + private environmentService: EnvironmentService, ) { super( apiService, @@ -126,6 +134,8 @@ export class MembersComponent extends BaseMembersComponent toastService, ); + this.dataSource = new MembersTableDataSource(this.configService, this.environmentService); + const organization$ = this.route.params.pipe( concatMap((params) => this.userId$.pipe( @@ -356,10 +366,9 @@ export class MembersComponent extends BaseMembersComponent return; } - await this.memberDialogManager.openBulkRemoveDialog( - organization, - this.dataSource.getCheckedUsers(), - ); + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); + + await this.memberDialogManager.openBulkRemoveDialog(organization, users); this.organizationMetadataService.refreshMetadataCache(); await this.load(organization); } @@ -369,10 +378,9 @@ export class MembersComponent extends BaseMembersComponent return; } - await this.memberDialogManager.openBulkDeleteDialog( - organization, - this.dataSource.getCheckedUsers(), - ); + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); + + await this.memberDialogManager.openBulkDeleteDialog(organization, users); await this.load(organization); } @@ -389,11 +397,9 @@ export class MembersComponent extends BaseMembersComponent return; } - await this.memberDialogManager.openBulkRestoreRevokeDialog( - organization, - this.dataSource.getCheckedUsers(), - isRevoking, - ); + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); + + await this.memberDialogManager.openBulkRestoreRevokeDialog(organization, users, isRevoking); await this.load(organization); } @@ -402,8 +408,28 @@ export class MembersComponent extends BaseMembersComponent return; } - const users = this.dataSource.getCheckedUsers(); - const filteredUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited); + let users: OrganizationUserView[]; + if (this.dataSource.isIncreasedBulkLimitEnabled()) { + users = this.dataSource.getCheckedUsersInVisibleOrder(); + } else { + users = this.dataSource.getCheckedUsers(); + } + + const allInvitedUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited); + + // Capture the original count BEFORE enforcing the limit + const originalInvitedCount = allInvitedUsers.length; + + // When feature flag is enabled, limit invited users and uncheck the excess + let filteredUsers: OrganizationUserView[]; + if (this.dataSource.isIncreasedBulkLimitEnabled()) { + filteredUsers = this.dataSource.limitAndUncheckExcess( + allInvitedUsers, + CloudBulkReinviteLimit, + ); + } else { + filteredUsers = allInvitedUsers; + } if (filteredUsers.length <= 0) { this.toastService.showToast({ @@ -424,13 +450,37 @@ export class MembersComponent extends BaseMembersComponent throw new Error(); } - // Bulk Status component open - await this.memberDialogManager.openBulkStatusDialog( - users, - filteredUsers, - Promise.resolve(result.successful), - this.i18nService.t("bulkReinviteMessage"), - ); + // When feature flag is enabled, show toast instead of dialog + if (this.dataSource.isIncreasedBulkLimitEnabled()) { + const selectedCount = originalInvitedCount; + const invitedCount = filteredUsers.length; + + if (selectedCount > CloudBulkReinviteLimit) { + const excludedCount = selectedCount - CloudBulkReinviteLimit; + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t( + "bulkReinviteLimitedSuccessToast", + CloudBulkReinviteLimit.toLocaleString(), + selectedCount.toLocaleString(), + excludedCount.toLocaleString(), + ), + }); + } else { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()), + }); + } + } else { + // Feature flag disabled - show legacy dialog + await this.memberDialogManager.openBulkStatusDialog( + users, + filteredUsers, + Promise.resolve(result.successful), + this.i18nService.t("bulkReinviteMessage"), + ); + } } catch (e) { this.validationService.showError(e); } @@ -442,15 +492,14 @@ export class MembersComponent extends BaseMembersComponent return; } - await this.memberDialogManager.openBulkConfirmDialog( - organization, - this.dataSource.getCheckedUsers(), - ); + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); + + await this.memberDialogManager.openBulkConfirmDialog(organization, users); await this.load(organization); } async bulkEnableSM(organization: Organization) { - const users = this.dataSource.getCheckedUsers(); + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index efd33ff64b8..3d5303c8e82 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6457,6 +6457,32 @@ "bulkReinviteMessage": { "message": "Reinvited successfully" }, + "bulkReinviteSuccessToast": { + "message": "$COUNT$ users re-invited", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, + "bulkReinviteLimitedSuccessToast": { + "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", + "placeholders": { + "limit": { + "content": "$1", + "example": "4,000" + }, + "selectedCount": { + "content": "$2", + "example": "4,005" + }, + "excludedCount": { + "content": "$3", + "example": "5" + } + } + }, "bulkRemovedMessage": { "message": "Removed successfully" }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 268a82ac12f..6e9209be882 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -19,6 +19,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -27,6 +29,8 @@ import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; import { + CloudBulkReinviteLimit, + MaxCheckedCount, peopleFilter, PeopleTableDataSource, } from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; @@ -56,7 +60,7 @@ class MembersTableDataSource extends PeopleTableDataSource { }) export class MembersComponent extends BaseMembersComponent { accessEvents = false; - dataSource = new MembersTableDataSource(); + dataSource: MembersTableDataSource; loading = true; providerId: string; rowHeight = 70; @@ -81,6 +85,8 @@ export class MembersComponent extends BaseMembersComponent { private providerService: ProviderService, private router: Router, private accountService: AccountService, + private configService: ConfigService, + private environmentService: EnvironmentService, ) { super( apiService, @@ -94,6 +100,8 @@ export class MembersComponent extends BaseMembersComponent { toastService, ); + this.dataSource = new MembersTableDataSource(this.configService, this.environmentService); + combineLatest([ this.activatedRoute.parent.params, this.activatedRoute.queryParams.pipe(first()), @@ -134,10 +142,12 @@ export class MembersComponent extends BaseMembersComponent { return; } + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); + const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { data: { providerId: this.providerId, - users: this.dataSource.getCheckedUsers(), + users: users, }, }); @@ -150,10 +160,28 @@ export class MembersComponent extends BaseMembersComponent { return; } - const checkedUsers = this.dataSource.getCheckedUsers(); - const checkedInvitedUsers = checkedUsers.filter( - (user) => user.status === ProviderUserStatusType.Invited, - ); + let users: ProviderUser[]; + if (this.dataSource.isIncreasedBulkLimitEnabled()) { + users = this.dataSource.getCheckedUsersInVisibleOrder(); + } else { + users = this.dataSource.getCheckedUsers(); + } + + const allInvitedUsers = users.filter((user) => user.status === ProviderUserStatusType.Invited); + + // Capture the original count BEFORE enforcing the limit + const originalInvitedCount = allInvitedUsers.length; + + // When feature flag is enabled, limit invited users and uncheck the excess + let checkedInvitedUsers: ProviderUser[]; + if (this.dataSource.isIncreasedBulkLimitEnabled()) { + checkedInvitedUsers = this.dataSource.limitAndUncheckExcess( + allInvitedUsers, + CloudBulkReinviteLimit, + ); + } else { + checkedInvitedUsers = allInvitedUsers; + } if (checkedInvitedUsers.length <= 0) { this.toastService.showToast({ @@ -165,20 +193,50 @@ export class MembersComponent extends BaseMembersComponent { } try { - const request = this.apiService.postManyProviderUserReinvite( - this.providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); + // When feature flag is enabled, show toast instead of dialog + if (this.dataSource.isIncreasedBulkLimitEnabled()) { + await this.apiService.postManyProviderUserReinvite( + this.providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ); - const dialogRef = BulkStatusComponent.open(this.dialogService, { - data: { - users: checkedUsers, - filteredUsers: checkedInvitedUsers, - request, - successfulMessage: this.i18nService.t("bulkReinviteMessage"), - }, - }); - await lastValueFrom(dialogRef.closed); + const selectedCount = originalInvitedCount; + const invitedCount = checkedInvitedUsers.length; + + if (selectedCount > CloudBulkReinviteLimit) { + const excludedCount = selectedCount - CloudBulkReinviteLimit; + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t( + "bulkReinviteLimitedSuccessToast", + CloudBulkReinviteLimit.toLocaleString(), + selectedCount.toLocaleString(), + excludedCount.toLocaleString(), + ), + }); + } else { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()), + }); + } + } else { + // Feature flag disabled - show legacy dialog + const request = this.apiService.postManyProviderUserReinvite( + this.providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ); + + const dialogRef = BulkStatusComponent.open(this.dialogService, { + data: { + users: users, + filteredUsers: checkedInvitedUsers, + request, + successfulMessage: this.i18nService.t("bulkReinviteMessage"), + }, + }); + await lastValueFrom(dialogRef.closed); + } } catch (error) { this.validationService.showError(error); } @@ -193,10 +251,12 @@ export class MembersComponent extends BaseMembersComponent { return; } + const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); + const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { data: { providerId: this.providerId, - users: this.dataSource.getCheckedUsers(), + users: users, }, }); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 23c1a07601e..371081a89d9 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -14,6 +14,7 @@ export enum FeatureFlag { CreateDefaultLocation = "pm-19467-create-default-location", AutoConfirm = "pm-19934-auto-confirm-organization-users", BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration", + IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud", /* Auth */ PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin", @@ -97,6 +98,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CreateDefaultLocation]: FALSE, [FeatureFlag.AutoConfirm]: FALSE, [FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE, + [FeatureFlag.IncreaseBulkReinviteLimitForCloud]: FALSE, /* Autofill */ [FeatureFlag.MacOsNativeCredentialSync]: FALSE,