1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-23628] Require userId for fetching provider keys (#16993)

* remove getProviderKey and expose providerKeys$

* update consumers
This commit is contained in:
Thomas Avery
2025-10-27 11:04:17 -05:00
committed by GitHub
parent b335987213
commit bd89c0ce6d
9 changed files with 223 additions and 64 deletions

View File

@@ -1,8 +1,11 @@
import { Component, Inject, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
DIALOG_DATA,
@@ -46,6 +49,7 @@ export class AddExistingOrganizationDialogComponent implements OnInit {
private providerApiService: ProviderApiServiceAbstraction,
private toastService: ToastService,
private webProviderService: WebProviderService,
private accountService: AccountService,
) {}
async ngOnInit() {
@@ -57,9 +61,11 @@ export class AddExistingOrganizationDialogComponent implements OnInit {
addExistingOrganization = async (): Promise<void> => {
if (this.selectedOrganization) {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
await this.webProviderService.addOrganizationToProvider(
this.dialogParams.provider.id,
this.selectedOrganization.id,
userId,
);
this.toastService.showToast({

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Inject } from "@angular/core";
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import {
OrganizationUserBulkPublicKeyResponse,
@@ -12,10 +13,14 @@ import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
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 { 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { ProviderId } from "@bitwarden/common/types/guid";
import { ProviderKey } from "@bitwarden/common/types/key";
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component";
@@ -35,6 +40,7 @@ type BulkConfirmDialogParams = {
})
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
providerId: string;
providerKey$: Observable<ProviderKey>;
constructor(
private apiService: ApiService,
@@ -42,15 +48,21 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
protected encryptService: EncryptService,
@Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams,
protected i18nService: I18nService,
private accountService: AccountService,
) {
super(keyService, encryptService, i18nService);
this.providerId = dialogParams.providerId;
this.providerKey$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.keyService.providerKeys$(userId)),
map((providerKeysById) => providerKeysById?.[this.providerId as ProviderId]),
);
this.users = dialogParams.users;
}
protected getCryptoKey = (): Promise<SymmetricCryptoKey> =>
this.keyService.getProviderKey(this.providerId);
protected getCryptoKey = async (): Promise<SymmetricCryptoKey> =>
await firstValueFrom(this.providerKey$);
protected getPublicKeys = async (): Promise<
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>

View File

@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs";
import { first } from "rxjs/operators";
import { first, map } from "rxjs/operators";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -16,11 +16,13 @@ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/mode
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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 { 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";
import { ProviderId } from "@bitwarden/common/types/guid";
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";
@@ -204,7 +206,15 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise<MemberActionResult> {
try {
const providerKey = await this.keyService.getProviderKey(this.providerId);
const providerKey = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.keyService.providerKeys$(userId)),
map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null),
),
);
assertNonNullish(providerKey, "Provider key not found");
const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;

View File

@@ -1,4 +1,5 @@
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
@@ -8,7 +9,6 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
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";
import { OrgKey, ProviderKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { newGuid } from "@bitwarden/guid";
@@ -24,16 +24,22 @@ describe("WebProviderService", () => {
let apiService: MockProxy<ApiService>;
let i18nService: MockProxy<I18nService>;
let encryptService: MockProxy<EncryptService>;
let stateProvider: MockProxy<StateProvider>;
let providerApiService: MockProxy<ProviderApiServiceAbstraction>;
const activeUserId = newGuid() as UserId;
const providerId = "provider-123";
const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey;
const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey;
const mockProviderKeysById: Record<string, ProviderKey> = {
[providerId]: mockProviderKey,
};
beforeEach(() => {
keyService = mock();
syncService = mock();
apiService = mock();
i18nService = mock();
encryptService = mock();
stateProvider = mock();
providerApiService = mock();
sut = new WebProviderService(
@@ -42,14 +48,69 @@ describe("WebProviderService", () => {
apiService,
i18nService,
encryptService,
stateProvider,
providerApiService,
);
});
describe("addOrganizationToProvider", () => {
const organizationId = "org-789";
const encryptedOrgKey = new EncString("encrypted-org-key");
const mockOrgKeysById: Record<string, OrgKey> = {
[organizationId]: mockOrgKey,
};
beforeEach(() => {
keyService.orgKeys$.mockReturnValue(of(mockOrgKeysById));
keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById));
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedOrgKey);
});
it("adds an organization to a provider with correct encryption", async () => {
await sut.addOrganizationToProvider(providerId, organizationId, activeUserId);
expect(keyService.orgKeys$).toHaveBeenCalledWith(activeUserId);
expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId);
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
expect(providerApiService.addOrganizationToProvider).toHaveBeenCalledWith(providerId, {
key: encryptedOrgKey.encryptedString,
organizationId,
});
expect(syncService.fullSync).toHaveBeenCalledWith(true);
});
it("throws an error if organization key is not found", async () => {
const invalidOrgId = "invalid-org";
await expect(
sut.addOrganizationToProvider(providerId, invalidOrgId, activeUserId),
).rejects.toThrow("Organization key not found");
});
it("throws an error if no organization keys are available", async () => {
keyService.orgKeys$.mockReturnValue(of(null));
await expect(
sut.addOrganizationToProvider(providerId, organizationId, activeUserId),
).rejects.toThrow("Organization key not found");
});
it("throws an error if provider key is not found", async () => {
const invalidProviderId = "invalid-provider";
await expect(
sut.addOrganizationToProvider(invalidProviderId, organizationId, activeUserId),
).rejects.toThrow("Provider key not found");
});
it("throws an error if no provider keys are available", async () => {
keyService.providerKeys$.mockReturnValue(of(null));
await expect(
sut.addOrganizationToProvider(providerId, organizationId, activeUserId),
).rejects.toThrow("Provider key not found");
});
});
describe("createClientOrganization", () => {
const activeUserId = newGuid() as UserId;
const providerId = "provider-123";
const name = "Test Org";
const ownerEmail = "owner@example.com";
const planType = PlanType.EnterpriseAnnually;
@@ -59,15 +120,13 @@ describe("WebProviderService", () => {
const encryptedProviderKey = new EncString("encrypted-provider-key");
const encryptedCollectionName = new EncString("encrypted-collection-name");
const defaultCollectionTranslation = "Default Collection";
const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey;
const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey;
beforeEach(() => {
keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]);
keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]);
i18nService.t.mockReturnValue(defaultCollectionTranslation);
encryptService.encryptString.mockResolvedValue(encryptedCollectionName);
keyService.getProviderKey.mockResolvedValue(mockProviderKey);
keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById));
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey);
});
@@ -88,7 +147,7 @@ describe("WebProviderService", () => {
defaultCollectionTranslation,
mockOrgKey,
);
expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId);
expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId);
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith(
@@ -107,5 +166,27 @@ describe("WebProviderService", () => {
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(syncService.fullSync).toHaveBeenCalledWith(true);
});
it("throws an error if provider key is not found", async () => {
const invalidProviderId = "invalid-provider";
await expect(
sut.createClientOrganization(
invalidProviderId,
name,
ownerEmail,
planType,
seats,
activeUserId,
),
).rejects.toThrow("Provider key not found");
});
it("throws an error if no provider keys are available", async () => {
keyService.providerKeys$.mockReturnValue(of(null));
await expect(
sut.createClientOrganization(providerId, name, ownerEmail, planType, seats, activeUserId),
).rejects.toThrow("Provider key not found");
});
});
});

View File

@@ -1,18 +1,17 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { switchMap } from "rxjs/operators";
import { combineLatest, firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { CreateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/create-provider-organization.request";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { assertNonNullish } from "@bitwarden/common/auth/utils";
import { PlanType } from "@bitwarden/common/billing/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { KeyService } from "@bitwarden/key-management";
@@ -25,18 +24,26 @@ export class WebProviderService {
private apiService: ApiService,
private i18nService: I18nService,
private encryptService: EncryptService,
private stateProvider: StateProvider,
private providerApiService: ProviderApiServiceAbstraction,
) {}
async addOrganizationToProvider(providerId: string, organizationId: string): Promise<void> {
const orgKey = await firstValueFrom(
this.stateProvider.activeUserId$.pipe(
switchMap((userId) => this.keyService.orgKeys$(userId)),
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
),
async addOrganizationToProvider(
providerId: string,
organizationId: string,
activeUserId: UserId,
): Promise<void> {
const [orgKeysById, providerKeys] = await firstValueFrom(
combineLatest([
this.keyService.orgKeys$(activeUserId),
this.keyService.providerKeys$(activeUserId),
]),
);
const providerKey = await this.keyService.getProviderKey(providerId);
const orgKey = orgKeysById?.[organizationId as OrganizationId];
const providerKey = providerKeys?.[providerId as ProviderId];
assertNonNullish(orgKey, "Organization key not found");
assertNonNullish(providerKey, "Provider key not found");
const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey);
await this.providerApiService.addOrganizationToProvider(providerId, {
key: encryptedOrgKey.encryptedString,
@@ -62,7 +69,12 @@ export class WebProviderService {
organizationKey,
);
const providerKey = await this.keyService.getProviderKey(providerId);
const providerKey = await firstValueFrom(
this.keyService
.providerKeys$(activeUserId)
.pipe(map((providerKeys) => providerKeys?.[providerId as ProviderId])),
);
assertNonNullish(providerKey, "Provider key not found");
const encryptedProviderKey = await this.encryptService.wrapSymmetricKey(
organizationKey,