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:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user