mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-22241] Add DefaultUserCollectionName support to bulk organization user confirmation (#15873)
* Add bulk user confirmation method to OrganizationUserService * Update OrganizationUserBulkConfirmRequest to include optional defaultUserCollectionName property * Implement conditional bulk user confirmation logic in BulkConfirmDialogComponent. Its gated behind the feature flag for default user collection. * Refactor OrganizationUserBulkConfirmRequest to use SdkEncString for defaultUserCollectionName * Refactor BulkConfirmDialogComponent to use organization object instead of organizationId for improved clarity and type safety. * Add unit tests for OrganizationUserService to validate user single/bulk confirmation logic * Refactor OrganizationUserService to streamline encrypted collection name retrieval by introducing getEncryptedDefaultCollectionName$ method. * Refactor unit tests for OrganizationUserService to reduce duplication by introducing a setupCommonMocks function for common mock configurations. * refactor(organization-user.service): streamline retrieval of encrypted collection name in bulk confirmation process
This commit is contained in:
@@ -11,10 +11,13 @@ import {
|
||||
OrganizationUserBulkResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
|
||||
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -23,11 +26,13 @@ import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserService } from "../../services/organization-user/organization-user.service";
|
||||
|
||||
import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component";
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkConfirmDialogParams = {
|
||||
organizationId: string;
|
||||
organization: Organization;
|
||||
users: BulkUserDetails[];
|
||||
};
|
||||
|
||||
@@ -36,7 +41,7 @@ type BulkConfirmDialogParams = {
|
||||
standalone: false,
|
||||
})
|
||||
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
organizationId: string;
|
||||
organization: Organization;
|
||||
organizationKey$: Observable<OrgKey>;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
@@ -47,13 +52,15 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
protected i18nService: I18nService,
|
||||
private stateProvider: StateProvider,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(keyService, encryptService, i18nService);
|
||||
|
||||
this.organizationId = dialogParams.organizationId;
|
||||
this.organization = dialogParams.organization;
|
||||
this.organizationKey$ = this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]),
|
||||
map((organizationKeysById) => organizationKeysById[this.organization.id as OrganizationId]),
|
||||
takeUntilDestroyed(),
|
||||
);
|
||||
this.users = dialogParams.users;
|
||||
@@ -66,7 +73,7 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
||||
> =>
|
||||
await this.organizationUserApiService.postOrganizationUsersPublicKey(
|
||||
this.organizationId,
|
||||
this.organization.id,
|
||||
this.filteredUsers.map((user) => user.id),
|
||||
);
|
||||
|
||||
@@ -76,11 +83,19 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
protected postConfirmRequest = async (
|
||||
userIdsWithKeys: { id: string; key: string }[],
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse | ProviderUserBulkResponse>> => {
|
||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
||||
this.organizationId,
|
||||
request,
|
||||
);
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||
) {
|
||||
return await firstValueFrom(
|
||||
this.organizationUserService.bulkConfirmUsers(this.organization, userIdsWithKeys),
|
||||
);
|
||||
} else {
|
||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
||||
this.organization.id,
|
||||
request,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogParams>) {
|
||||
|
||||
@@ -721,7 +721,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
organization: this.organization,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserConfirmRequest,
|
||||
OrganizationUserBulkConfirmRequest,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserBulkResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||
|
||||
import { OrganizationUserService } from "./organization-user.service";
|
||||
|
||||
describe("OrganizationUserService", () => {
|
||||
let service: OrganizationUserService;
|
||||
let keyService: jest.Mocked<KeyService>;
|
||||
let encryptService: jest.Mocked<EncryptService>;
|
||||
let organizationUserApiService: jest.Mocked<OrganizationUserApiService>;
|
||||
let accountService: jest.Mocked<AccountService>;
|
||||
let i18nService: jest.Mocked<I18nService>;
|
||||
|
||||
const mockOrganization = new Organization();
|
||||
mockOrganization.id = "org-123" as OrganizationId;
|
||||
|
||||
const mockOrganizationUser = new OrganizationUserView();
|
||||
mockOrganizationUser.id = "user-123";
|
||||
|
||||
const mockPublicKey = new Uint8Array(64) as CsprngArray;
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||
const mockEncryptedKey = { encryptedString: "encrypted-key" } as EncString;
|
||||
const mockEncryptedCollectionName = { encryptedString: "encrypted-collection-name" } as EncString;
|
||||
const mockDefaultCollectionName = "My Items";
|
||||
|
||||
const setupCommonMocks = () => {
|
||||
keyService.orgKeys$.mockReturnValue(
|
||||
of({ [mockOrganization.id]: mockOrgKey } as Record<OrganizationId, OrgKey>),
|
||||
);
|
||||
encryptService.encryptString.mockResolvedValue(mockEncryptedCollectionName);
|
||||
i18nService.t.mockReturnValue(mockDefaultCollectionName);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = {
|
||||
orgKeys$: jest.fn(),
|
||||
} as any;
|
||||
|
||||
encryptService = {
|
||||
encryptString: jest.fn(),
|
||||
encapsulateKeyUnsigned: jest.fn(),
|
||||
} as any;
|
||||
|
||||
organizationUserApiService = {
|
||||
postOrganizationUserConfirm: jest.fn(),
|
||||
postOrganizationUserBulkConfirm: jest.fn(),
|
||||
} as any;
|
||||
|
||||
accountService = {
|
||||
activeAccount$: of({ id: "user-123" }),
|
||||
} as any;
|
||||
|
||||
i18nService = {
|
||||
t: jest.fn(),
|
||||
} as any;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
OrganizationUserService,
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: EncryptService, useValue: encryptService },
|
||||
{ provide: OrganizationUserApiService, useValue: organizationUserApiService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(OrganizationUserService);
|
||||
});
|
||||
|
||||
describe("confirmUser", () => {
|
||||
beforeEach(() => {
|
||||
setupCommonMocks();
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(mockEncryptedKey);
|
||||
organizationUserApiService.postOrganizationUserConfirm.mockReturnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
it("should confirm a user successfully", (done) => {
|
||||
service.confirmUser(mockOrganization, mockOrganizationUser, mockPublicKey).subscribe({
|
||||
next: () => {
|
||||
expect(i18nService.t).toHaveBeenCalledWith("myItems");
|
||||
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
mockDefaultCollectionName,
|
||||
mockOrgKey,
|
||||
);
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
|
||||
mockOrgKey,
|
||||
mockPublicKey,
|
||||
);
|
||||
|
||||
expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith(
|
||||
mockOrganization.id,
|
||||
mockOrganizationUser.id,
|
||||
{
|
||||
key: mockEncryptedKey.encryptedString,
|
||||
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
|
||||
} as OrganizationUserConfirmRequest,
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("bulkConfirmUsers", () => {
|
||||
const mockUserIdsWithKeys = [
|
||||
{ id: "user-1", key: "key-1" },
|
||||
{ id: "user-2", key: "key-2" },
|
||||
];
|
||||
|
||||
const mockBulkResponse = {
|
||||
data: [
|
||||
{ id: "user-1", error: null } as OrganizationUserBulkResponse,
|
||||
{ id: "user-2", error: null } as OrganizationUserBulkResponse,
|
||||
],
|
||||
} as ListResponse<OrganizationUserBulkResponse>;
|
||||
|
||||
beforeEach(() => {
|
||||
setupCommonMocks();
|
||||
organizationUserApiService.postOrganizationUserBulkConfirm.mockReturnValue(
|
||||
Promise.resolve(mockBulkResponse),
|
||||
);
|
||||
});
|
||||
|
||||
it("should bulk confirm users successfully", (done) => {
|
||||
service.bulkConfirmUsers(mockOrganization, mockUserIdsWithKeys).subscribe({
|
||||
next: (response) => {
|
||||
expect(i18nService.t).toHaveBeenCalledWith("myItems");
|
||||
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
mockDefaultCollectionName,
|
||||
mockOrgKey,
|
||||
);
|
||||
|
||||
expect(organizationUserApiService.postOrganizationUserBulkConfirm).toHaveBeenCalledWith(
|
||||
mockOrganization.id,
|
||||
new OrganizationUserBulkConfirmRequest(
|
||||
mockUserIdsWithKeys,
|
||||
mockEncryptedCollectionName.encryptedString,
|
||||
),
|
||||
);
|
||||
|
||||
expect(response).toEqual(mockBulkResponse);
|
||||
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,12 +3,15 @@ import { combineLatest, filter, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserConfirmRequest,
|
||||
OrganizationUserBulkConfirmRequest,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserBulkResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -41,11 +44,7 @@ export class OrganizationUserService {
|
||||
user: OrganizationUserView,
|
||||
publicKey: Uint8Array,
|
||||
): Observable<void> {
|
||||
const encryptedCollectionName$ = this.orgKey$(organization).pipe(
|
||||
switchMap((orgKey) =>
|
||||
this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey),
|
||||
),
|
||||
);
|
||||
const encryptedCollectionName$ = this.getEncryptedDefaultCollectionName$(organization);
|
||||
|
||||
const encryptedKey$ = this.orgKey$(organization).pipe(
|
||||
switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)),
|
||||
@@ -66,4 +65,31 @@ export class OrganizationUserService {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
bulkConfirmUsers(
|
||||
organization: Organization,
|
||||
userIdsWithKeys: { id: string; key: string }[],
|
||||
): Observable<ListResponse<OrganizationUserBulkResponse>> {
|
||||
return this.getEncryptedDefaultCollectionName$(organization).pipe(
|
||||
switchMap((collectionName) => {
|
||||
const request = new OrganizationUserBulkConfirmRequest(
|
||||
userIdsWithKeys,
|
||||
collectionName.encryptedString,
|
||||
);
|
||||
|
||||
return this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
||||
organization.id,
|
||||
request,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getEncryptedDefaultCollectionName$(organization: Organization) {
|
||||
return this.orgKey$(organization).pipe(
|
||||
switchMap((orgKey) =>
|
||||
this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user