1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 10:54:00 +00:00

[PM-30891] - Create My Items On Restore (#18454)

* Added encrypted default collection name to new feature flagged restore user methods/endpoint.

* corrected filter to use null check with imperative code
This commit is contained in:
Jared McCannon
2026-01-29 13:56:35 -06:00
committed by jaasen-livefront
parent 1311b0bf4c
commit 08a98d2c5e
12 changed files with 378 additions and 36 deletions

View File

@@ -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<void>;
/**
* 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<void>;
/**
* 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<ListResponse<OrganizationUserBulkResponse>>;
/**
* 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<ListResponse<OrganizationUserBulkResponse>>;
/**
* Remove an organization user's access to the organization and delete their account data
* @param organizationId - Identifier for the organization the user belongs to

View File

@@ -42,4 +42,11 @@ export abstract class OrganizationUserService {
organization: Organization,
userIdsWithKeys: { id: string; key: string }[],
): Observable<ListResponse<OrganizationUserBulkResponse>>;
abstract restoreUser(organization: Organization, userId: string): Observable<void>;
abstract bulkRestoreUsers(
organization: Organization,
userIds: string[],
): Observable<ListResponse<OrganizationUserBulkResponse>>;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
import { EncString } from "@bitwarden/sdk-internal";
export class OrganizationUserRestoreRequest {
defaultUserCollectionName: EncString | undefined;
constructor(defaultUserCollectionName?: EncString) {
this.defaultUserCollectionName = defaultUserCollectionName;
}
}

View File

@@ -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<void> {
return this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/users/" + id + "/restore/vnext",
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<ListResponse<OrganizationUserBulkResponse>> {
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<void> {
return this.apiService.send(
"DELETE",

View File

@@ -61,6 +61,8 @@ describe("DefaultOrganizationUserService", () => {
organizationUserApiService = {
postOrganizationUserConfirm: jest.fn(),
postOrganizationUserBulkConfirm: jest.fn(),
restoreOrganizationUser_vNext: jest.fn(),
restoreManyOrganizationUsers_vNext: jest.fn(),
} as any;
accountService = {
@@ -174,4 +176,97 @@ describe("DefaultOrganizationUserService", () => {
});
});
});
describe("buildRestoreUserRequest", () => {
beforeEach(() => {
setupCommonMocks();
});
it("should build a restore request with encrypted collection name", (done) => {
service.buildRestoreUserRequest(mockOrganization).subscribe({
next: (request) => {
expect(i18nService.t).toHaveBeenCalledWith("myItems");
expect(encryptService.encryptString).toHaveBeenCalledWith(
mockDefaultCollectionName,
mockOrgKey,
);
expect(request).toEqual({
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
});
done();
},
error: done,
});
});
});
describe("restoreUser", () => {
beforeEach(() => {
setupCommonMocks();
organizationUserApiService.restoreOrganizationUser_vNext.mockReturnValue(Promise.resolve());
});
it("should restore a user successfully", (done) => {
service.restoreUser(mockOrganization, mockUserId).subscribe({
next: () => {
expect(i18nService.t).toHaveBeenCalledWith("myItems");
expect(encryptService.encryptString).toHaveBeenCalledWith(
mockDefaultCollectionName,
mockOrgKey,
);
expect(organizationUserApiService.restoreOrganizationUser_vNext).toHaveBeenCalledWith(
mockOrganization.id,
mockUserId,
{
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
},
);
done();
},
error: done,
});
});
});
describe("bulkRestoreUsers", () => {
const mockUserIds = ["user-1", "user-2"];
const mockBulkResponse = {
data: [
{ id: "user-1", error: null } as OrganizationUserBulkResponse,
{ id: "user-2", error: null } as OrganizationUserBulkResponse,
],
} as ListResponse<OrganizationUserBulkResponse>;
beforeEach(() => {
setupCommonMocks();
organizationUserApiService.restoreManyOrganizationUsers_vNext.mockReturnValue(
Promise.resolve(mockBulkResponse),
);
});
it("should bulk restore users successfully", (done) => {
service.bulkRestoreUsers(mockOrganization, mockUserIds).subscribe({
next: (response) => {
expect(i18nService.t).toHaveBeenCalledWith("myItems");
expect(encryptService.encryptString).toHaveBeenCalledWith(
mockDefaultCollectionName,
mockOrgKey,
);
expect(
organizationUserApiService.restoreManyOrganizationUsers_vNext,
).toHaveBeenCalledWith(
mockOrganization.id,
expect.objectContaining({
userIds: mockUserIds,
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
}),
);
expect(response).toEqual(mockBulkResponse);
done();
},
error: done,
});
});
});
});

View File

@@ -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,43 @@ export class DefaultOrganizationUserService implements OrganizationUserService {
);
}
buildRestoreUserRequest(organization: Organization): Observable<OrganizationUserRestoreRequest> {
return this.getEncryptedDefaultCollectionName$(organization).pipe(
map((collectionName) => new OrganizationUserRestoreRequest(collectionName.encryptedString)),
);
}
restoreUser(organization: Organization, userId: string): Observable<void> {
return this.buildRestoreUserRequest(organization).pipe(
switchMap((request) =>
this.organizationUserApiService.restoreOrganizationUser_vNext(
organization.id,
userId,
request,
),
),
);
}
bulkRestoreUsers(
organization: Organization,
userIds: string[],
): Observable<ListResponse<OrganizationUserBulkResponse>> {
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) =>