From 4253f05de7ea766245348d80d5732487222d43f3 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 16 Jan 2026 16:21:48 -0600 Subject: [PATCH] Added encrypted default collection name to new feature flagged restore user methods/endpoint. --- .../bulk/bulk-restore-revoke.component.ts | 44 ++++++++++++++++-- .../member-dialog/member-dialog.component.ts | 29 ++++++++++-- .../member-actions/member-actions.service.ts | 19 ++++++-- .../organization-user-api.service.ts | 25 ++++++++++ .../abstractions/organization-user.service.ts | 7 +++ .../organization-user-bulk-restore.request.ts | 11 +++++ .../organization-user-restore.request.ts | 5 ++ .../default-organization-user-api.service.ts | 30 ++++++++++++ .../default-organization-user.service.ts | 46 ++++++++++++++++++- libs/common/src/enums/feature-flag.enum.ts | 2 + 10 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts create mode 100644 libs/admin-console/src/common/organization-user/models/requests/organization-user-restore.request.ts diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts index dc7b079fefe..1f4aa4a5856 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts @@ -1,10 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, Inject } from "@angular/core"; +import { combineLatest, firstValueFrom, from, Observable, switchMap } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + OrganizationUserApiService, + OrganizationUserService, +} from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { DIALOG_DATA, DialogService } from "@bitwarden/components"; import { BulkUserDetails } from "./bulk-status.component"; @@ -34,10 +44,15 @@ export class BulkRestoreRevokeComponent { error: string; showNoMasterPasswordWarning = false; nonCompliantMembers: boolean = false; + organization$: Observable; constructor( protected i18nService: I18nService, private organizationUserApiService: OrganizationUserApiService, + private organizationUserService: OrganizationUserService, + private accountService: AccountService, + private organizationService: OrganizationService, + private configService: ConfigService, @Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams, ) { this.isRevoking = data.isRevoking; @@ -46,6 +61,12 @@ export class BulkRestoreRevokeComponent { this.showNoMasterPasswordWarning = this.users.some( (u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false, ); + + this.organization$ = accountService.activeAccount$.pipe( + switchMap((account) => + organizationService.organizations$(account?.id).pipe(getById(this.organizationId)), + ), + ); } get bulkTitle() { @@ -83,9 +104,24 @@ export class BulkRestoreRevokeComponent { userIds, ); } else { - return await this.organizationUserApiService.restoreManyOrganizationUsers( - this.organizationId, - userIds, + return await firstValueFrom( + combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.DefaultUserCollectionRestore), + this.organization$, + ]).pipe( + switchMap(([enabled, organization]) => { + if (enabled) { + return this.organizationUserService.bulkRestoreUsers(organization, userIds); + } else { + return from( + this.organizationUserApiService.restoreManyOrganizationUsers( + this.organizationId, + userIds, + ), + ); + } + }), + ), ); } } 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 1fa4c8bf8f7..917570e34bf 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 @@ -5,6 +5,7 @@ import { FormBuilder, Validators } from "@angular/forms"; import { combineLatest, firstValueFrom, + from, map, Observable, of, @@ -17,6 +18,7 @@ import { import { CollectionAdminService, OrganizationUserApiService, + OrganizationUserService, } from "@bitwarden/admin-console/common"; import { getOrganizationById, @@ -36,6 +38,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { @@ -197,6 +200,7 @@ export class MemberDialogComponent implements OnDestroy { private toastService: ToastService, private configService: ConfigService, private deleteManagedMemberWarningService: DeleteManagedMemberWarningService, + private organizationUserService: OrganizationUserService, ) { this.organization$ = accountService.activeAccount$.pipe( switchMap((account) => @@ -633,9 +637,28 @@ export class MemberDialogComponent implements OnDestroy { return; } - await this.organizationUserApiService.restoreOrganizationUser( - this.params.organizationId, - this.params.organizationUserId, + await firstValueFrom( + combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.DefaultUserCollectionRestore), + this.organization$, + this.editParams$, + ]).pipe( + switchMap(([enabled, organization, params]) => { + if (enabled) { + return this.organizationUserService.restoreUser( + organization, + params.organizationUserId, + ); + } else { + return from( + this.organizationUserApiService.restoreOrganizationUser( + params.organizationId, + params.organizationUserId, + ), + ); + } + }), + ), ); this.toastService.showToast({ diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index 5833238209c..a6c782cb2e0 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -1,5 +1,5 @@ import { inject, Injectable, signal } from "@angular/core"; -import { lastValueFrom, firstValueFrom } from "rxjs"; +import { lastValueFrom, firstValueFrom, from, switchMap } from "rxjs"; import { OrganizationUserApiService, @@ -10,8 +10,8 @@ import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { - OrganizationUserType, OrganizationUserStatusType, + OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { assertNonNullish } from "@bitwarden/common/auth/utils"; @@ -119,7 +119,20 @@ export class MemberActionsService { async restoreUser(organization: Organization, userId: string): Promise { this.startProcessing(); try { - await this.organizationUserApiService.restoreOrganizationUser(organization.id, userId); + await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.DefaultUserCollectionRestore).pipe( + switchMap((enabled) => { + if (enabled) { + return this.organizationUserService.restoreUser(organization, userId); + } else { + return from( + this.organizationUserApiService.restoreOrganizationUser(organization.id, userId), + ); + } + }), + ), + ); + this.organizationMetadataService.refreshMetadataCache(); return { success: true }; } catch (error) { diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index cbaece1b442..1dc0e0b3bef 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -10,6 +10,8 @@ import { OrganizationUserResetPasswordRequest, OrganizationUserUpdateRequest, } from "../models/requests"; +import { OrganizationUserBulkRestoreRequest } from "../models/requests/organization-user-bulk-restore.request"; +import { OrganizationUserRestoreRequest } from "../models/requests/organization-user-restore.request"; import { OrganizationUserBulkPublicKeyResponse, OrganizationUserBulkResponse, @@ -278,6 +280,18 @@ export abstract class OrganizationUserApiService { */ abstract restoreOrganizationUser(organizationId: string, id: string): Promise; + /** + * Restore an organization user's access to the organization + * @param organizationId - Identifier for the organization the user belongs to + * @param id - Organization user identifier + * @param request - Restore request containing default user collection name + */ + abstract restoreOrganizationUser_vNext( + organizationId: string, + id: string, + request: OrganizationUserRestoreRequest, + ): Promise; + /** * Restore many organization users' access to the organization * @param organizationId - Identifier for the organization the users belongs to @@ -289,6 +303,17 @@ export abstract class OrganizationUserApiService { ids: string[], ): Promise>; + /** + * Restore many organization users' access to the organization + * @param organizationId - Identifier for the organization the users belongs to + * @param request - Restore request containing default user collection name + * @return List of user ids, including both those that were successfully restored and those that had an error + */ + abstract restoreManyOrganizationUsers_vNext( + organizationId: string, + request: OrganizationUserBulkRestoreRequest, + ): Promise>; + /** * Remove an organization user's access to the organization and delete their account data * @param organizationId - Identifier for the organization the user belongs to diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts index 844a0f412be..03e6840d786 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts @@ -42,4 +42,11 @@ export abstract class OrganizationUserService { organization: Organization, userIdsWithKeys: { id: string; key: string }[], ): Observable>; + + abstract restoreUser(organization: Organization, userId: string): Observable; + + abstract bulkRestoreUsers( + organization: Organization, + userIds: string[], + ): Observable>; } diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts new file mode 100644 index 00000000000..74a91897a58 --- /dev/null +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts @@ -0,0 +1,11 @@ +import { EncString } from "@bitwarden/sdk-internal"; + +export class OrganizationUserBulkRestoreRequest { + userIds: string[]; + defaultUserCollectionName: EncString | undefined; + + constructor(userIds: string[], defaultUserCollectionName?: EncString) { + this.userIds = userIds; + this.defaultUserCollectionName = defaultUserCollectionName; + } +} diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-restore.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-restore.request.ts new file mode 100644 index 00000000000..e6d59ce5929 --- /dev/null +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-restore.request.ts @@ -0,0 +1,5 @@ +import { EncString } from "@bitwarden/sdk-internal"; + +export class OrganizationUserRestoreRequest { + defaultUserCollectionName: EncString | undefined; +} diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index 536afd2b3f6..388624d2274 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -13,6 +13,8 @@ import { OrganizationUserUpdateRequest, OrganizationUserBulkRequest, } from "../models/requests"; +import { OrganizationUserBulkRestoreRequest } from "../models/requests/organization-user-bulk-restore.request"; +import { OrganizationUserRestoreRequest } from "../models/requests/organization-user-restore.request"; import { OrganizationUserBulkPublicKeyResponse, OrganizationUserBulkResponse, @@ -359,6 +361,20 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer ); } + restoreOrganizationUser_vNext( + organizationId: string, + id: string, + request: OrganizationUserRestoreRequest, + ): Promise { + return this.apiService.send( + "PUT", + "/organizations/" + organizationId + "/users/" + id + "/restore", + request, + true, + false, + ); + } + async restoreManyOrganizationUsers( organizationId: string, ids: string[], @@ -373,6 +389,20 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer return new ListResponse(r, OrganizationUserBulkResponse); } + async restoreManyOrganizationUsers_vNext( + organizationId: string, + request: OrganizationUserBulkRestoreRequest, + ): Promise> { + const r = await this.apiService.send( + "PUT", + "/organizations/" + organizationId + "/users/restore", + request, + true, + true, + ); + return new ListResponse(r, OrganizationUserBulkResponse); + } + deleteOrganizationUser(organizationId: string, id: string): Promise { return this.apiService.send( "DELETE", diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts index 4f503a92675..973a7423f34 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts @@ -1,10 +1,10 @@ import { combineLatest, filter, map, Observable, switchMap } from "rxjs"; import { - OrganizationUserConfirmRequest, - OrganizationUserBulkConfirmRequest, OrganizationUserApiService, + OrganizationUserBulkConfirmRequest, OrganizationUserBulkResponse, + OrganizationUserConfirmRequest, OrganizationUserService, } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -16,6 +16,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; +import { OrganizationUserBulkRestoreRequest } from "../models/requests/organization-user-bulk-restore.request"; +import { OrganizationUserRestoreRequest } from "../models/requests/organization-user-restore.request"; + export class DefaultOrganizationUserService implements OrganizationUserService { constructor( protected keyService: KeyService, @@ -83,6 +86,45 @@ export class DefaultOrganizationUserService implements OrganizationUserService { ); } + buildRestoreUserRequest(organization: Organization): Observable { + return this.getEncryptedDefaultCollectionName$(organization).pipe( + map((collectionName) => ({ + defaultUserCollectionName: collectionName.encryptedString, + })), + ); + } + + restoreUser(organization: Organization, userId: string): Observable { + return this.buildRestoreUserRequest(organization).pipe( + switchMap((request) => + this.organizationUserApiService.restoreOrganizationUser_vNext( + organization.id, + userId, + request, + ), + ), + ); + } + + bulkRestoreUsers( + organization: Organization, + userIds: string[], + ): Observable> { + return this.getEncryptedDefaultCollectionName$(organization).pipe( + switchMap((collectionName) => { + const request = new OrganizationUserBulkRestoreRequest( + userIds, + collectionName.encryptedString, + ); + + return this.organizationUserApiService.restoreManyOrganizationUsers_vNext( + organization.id, + request, + ); + }), + ); + } + private getEncryptedDefaultCollectionName$(organization: Organization) { return this.orgKey$(organization).pipe( switchMap((orgKey) => diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0f834abbe2a..5559e852297 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -13,6 +13,7 @@ export enum FeatureFlag { /* Admin Console Team */ AutoConfirm = "pm-19934-auto-confirm-organization-users", BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration", + DefaultUserCollectionRestore = "pm-30883-my-items-restored-users", IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud", MembersComponentRefactor = "pm-29503-refactor-members-inheritance", @@ -100,6 +101,7 @@ export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.AutoConfirm]: FALSE, [FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE, + [FeatureFlag.DefaultUserCollectionRestore]: FALSE, [FeatureFlag.IncreaseBulkReinviteLimitForCloud]: FALSE, [FeatureFlag.MembersComponentRefactor]: FALSE,