mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +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,
|
OrganizationUserBulkResponse,
|
||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
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 { 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 { 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
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 { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { OrganizationUserService } from "../../services/organization-user/organization-user.service";
|
||||||
|
|
||||||
import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component";
|
import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component";
|
||||||
import { BulkUserDetails } from "./bulk-status.component";
|
import { BulkUserDetails } from "./bulk-status.component";
|
||||||
|
|
||||||
type BulkConfirmDialogParams = {
|
type BulkConfirmDialogParams = {
|
||||||
organizationId: string;
|
organization: Organization;
|
||||||
users: BulkUserDetails[];
|
users: BulkUserDetails[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +41,7 @@ type BulkConfirmDialogParams = {
|
|||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||||
organizationId: string;
|
organization: Organization;
|
||||||
organizationKey$: Observable<OrgKey>;
|
organizationKey$: Observable<OrgKey>;
|
||||||
users: BulkUserDetails[];
|
users: BulkUserDetails[];
|
||||||
|
|
||||||
@@ -47,13 +52,15 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
|||||||
private organizationUserApiService: OrganizationUserApiService,
|
private organizationUserApiService: OrganizationUserApiService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
|
private organizationUserService: OrganizationUserService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super(keyService, encryptService, i18nService);
|
super(keyService, encryptService, i18nService);
|
||||||
|
|
||||||
this.organizationId = dialogParams.organizationId;
|
this.organization = dialogParams.organization;
|
||||||
this.organizationKey$ = this.stateProvider.activeUserId$.pipe(
|
this.organizationKey$ = this.stateProvider.activeUserId$.pipe(
|
||||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||||
map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]),
|
map((organizationKeysById) => organizationKeysById[this.organization.id as OrganizationId]),
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
);
|
);
|
||||||
this.users = dialogParams.users;
|
this.users = dialogParams.users;
|
||||||
@@ -66,7 +73,7 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
|||||||
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
||||||
> =>
|
> =>
|
||||||
await this.organizationUserApiService.postOrganizationUsersPublicKey(
|
await this.organizationUserApiService.postOrganizationUsersPublicKey(
|
||||||
this.organizationId,
|
this.organization.id,
|
||||||
this.filteredUsers.map((user) => user.id),
|
this.filteredUsers.map((user) => user.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,11 +83,19 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
|||||||
protected postConfirmRequest = async (
|
protected postConfirmRequest = async (
|
||||||
userIdsWithKeys: { id: string; key: string }[],
|
userIdsWithKeys: { id: string; key: string }[],
|
||||||
): Promise<ListResponse<OrganizationUserBulkResponse | ProviderUserBulkResponse>> => {
|
): Promise<ListResponse<OrganizationUserBulkResponse | ProviderUserBulkResponse>> => {
|
||||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
if (
|
||||||
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||||
this.organizationId,
|
) {
|
||||||
request,
|
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>) {
|
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogParams>) {
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
|
|
||||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
organizationId: this.organization.id,
|
organization: this.organization,
|
||||||
users: this.dataSource.getCheckedUsers(),
|
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 {
|
import {
|
||||||
OrganizationUserConfirmRequest,
|
OrganizationUserConfirmRequest,
|
||||||
|
OrganizationUserBulkConfirmRequest,
|
||||||
OrganizationUserApiService,
|
OrganizationUserApiService,
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
@@ -41,11 +44,7 @@ export class OrganizationUserService {
|
|||||||
user: OrganizationUserView,
|
user: OrganizationUserView,
|
||||||
publicKey: Uint8Array,
|
publicKey: Uint8Array,
|
||||||
): Observable<void> {
|
): Observable<void> {
|
||||||
const encryptedCollectionName$ = this.orgKey$(organization).pipe(
|
const encryptedCollectionName$ = this.getEncryptedDefaultCollectionName$(organization);
|
||||||
switchMap((orgKey) =>
|
|
||||||
this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const encryptedKey$ = this.orgKey$(organization).pipe(
|
const encryptedKey$ = this.orgKey$(organization).pipe(
|
||||||
switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)),
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { EncString as SdkEncString } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
type OrganizationUserBulkRequestEntry = {
|
type OrganizationUserBulkRequestEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -5,8 +7,10 @@ type OrganizationUserBulkRequestEntry = {
|
|||||||
|
|
||||||
export class OrganizationUserBulkConfirmRequest {
|
export class OrganizationUserBulkConfirmRequest {
|
||||||
keys: OrganizationUserBulkRequestEntry[];
|
keys: OrganizationUserBulkRequestEntry[];
|
||||||
|
defaultUserCollectionName: SdkEncString | undefined;
|
||||||
|
|
||||||
constructor(keys: OrganizationUserBulkRequestEntry[]) {
|
constructor(keys: OrganizationUserBulkRequestEntry[], defaultUserCollectionName?: SdkEncString) {
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
|
this.defaultUserCollectionName = defaultUserCollectionName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user