mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-23628] Require userId for fetching provider keys (#16993)
* remove getProviderKey and expose providerKeys$ * update consumers
This commit is contained in:
@@ -31,6 +31,7 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso
|
|||||||
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
||||||
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 { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||||
import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
||||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
@@ -41,7 +42,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { ToastService } from "@bitwarden/components";
|
import { ToastService } from "@bitwarden/components";
|
||||||
@@ -654,7 +655,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
orgId = this.selfHosted
|
orgId = this.selfHosted
|
||||||
? await this.createSelfHosted(key, collectionCt, orgKeys)
|
? await this.createSelfHosted(key, collectionCt, orgKeys)
|
||||||
: await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
|
: await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
@@ -808,6 +809,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
collectionCt: string,
|
collectionCt: string,
|
||||||
orgKeys: [string, EncString],
|
orgKeys: [string, EncString],
|
||||||
orgKey: SymmetricCryptoKey,
|
orgKey: SymmetricCryptoKey,
|
||||||
|
activeUserId: UserId,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const request = new OrganizationCreateRequest();
|
const request = new OrganizationCreateRequest();
|
||||||
request.key = key;
|
request.key = key;
|
||||||
@@ -855,7 +857,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
this.formGroup.controls.clientOwnerEmail.value,
|
this.formGroup.controls.clientOwnerEmail.value,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
const providerKey = await this.keyService.getProviderKey(this.providerId);
|
|
||||||
|
const providerKey = await firstValueFrom(
|
||||||
|
this.keyService
|
||||||
|
.providerKeys$(activeUserId)
|
||||||
|
.pipe(map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null)),
|
||||||
|
);
|
||||||
|
assertNonNullish(providerKey, "Provider key not found");
|
||||||
|
|
||||||
providerRequest.organizationCreateRequest.key = (
|
providerRequest.organizationCreateRequest.key = (
|
||||||
await this.encryptService.wrapSymmetricKey(orgKey, providerKey)
|
await this.encryptService.wrapSymmetricKey(orgKey, providerKey)
|
||||||
).encryptedString;
|
).encryptedString;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Component, Inject, OnInit } from "@angular/core";
|
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 { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||||
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import {
|
import {
|
||||||
DIALOG_DATA,
|
DIALOG_DATA,
|
||||||
@@ -46,6 +49,7 @@ export class AddExistingOrganizationDialogComponent implements OnInit {
|
|||||||
private providerApiService: ProviderApiServiceAbstraction,
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private webProviderService: WebProviderService,
|
private webProviderService: WebProviderService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -57,9 +61,11 @@ export class AddExistingOrganizationDialogComponent implements OnInit {
|
|||||||
|
|
||||||
addExistingOrganization = async (): Promise<void> => {
|
addExistingOrganization = async (): Promise<void> => {
|
||||||
if (this.selectedOrganization) {
|
if (this.selectedOrganization) {
|
||||||
|
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
await this.webProviderService.addOrganizationToProvider(
|
await this.webProviderService.addOrganizationToProvider(
|
||||||
this.dialogParams.provider.id,
|
this.dialogParams.provider.id,
|
||||||
this.selectedOrganization.id,
|
this.selectedOrganization.id,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
|
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrganizationUserBulkPublicKeyResponse,
|
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 { 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 { 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 { 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 { 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 { 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 { ProviderId } from "@bitwarden/common/types/guid";
|
||||||
|
import { ProviderKey } 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 { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component";
|
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 {
|
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
|
providerKey$: Observable<ProviderKey>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
@@ -42,15 +48,21 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
|||||||
protected encryptService: EncryptService,
|
protected encryptService: EncryptService,
|
||||||
@Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams,
|
@Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(keyService, encryptService, i18nService);
|
super(keyService, encryptService, i18nService);
|
||||||
|
|
||||||
this.providerId = dialogParams.providerId;
|
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;
|
this.users = dialogParams.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getCryptoKey = (): Promise<SymmetricCryptoKey> =>
|
protected getCryptoKey = async (): Promise<SymmetricCryptoKey> =>
|
||||||
this.keyService.getProviderKey(this.providerId);
|
await firstValueFrom(this.providerKey$);
|
||||||
|
|
||||||
protected getPublicKeys = async (): Promise<
|
protected getPublicKeys = async (): Promise<
|
||||||
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
|
|||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs";
|
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 { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
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 { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||||
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 { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.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 { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component";
|
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> {
|
async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise<MemberActionResult> {
|
||||||
try {
|
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 key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey);
|
||||||
const request = new ProviderUserConfirmRequest();
|
const request = new ProviderUserConfirmRequest();
|
||||||
request.key = key.encryptedString;
|
request.key = key.encryptedString;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
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 { OrgKey, ProviderKey } from "@bitwarden/common/types/key";
|
import { OrgKey, ProviderKey } from "@bitwarden/common/types/key";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { newGuid } from "@bitwarden/guid";
|
import { newGuid } from "@bitwarden/guid";
|
||||||
@@ -24,16 +24,22 @@ describe("WebProviderService", () => {
|
|||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
let stateProvider: MockProxy<StateProvider>;
|
|
||||||
let providerApiService: MockProxy<ProviderApiServiceAbstraction>;
|
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(() => {
|
beforeEach(() => {
|
||||||
keyService = mock();
|
keyService = mock();
|
||||||
syncService = mock();
|
syncService = mock();
|
||||||
apiService = mock();
|
apiService = mock();
|
||||||
i18nService = mock();
|
i18nService = mock();
|
||||||
encryptService = mock();
|
encryptService = mock();
|
||||||
stateProvider = mock();
|
|
||||||
providerApiService = mock();
|
providerApiService = mock();
|
||||||
|
|
||||||
sut = new WebProviderService(
|
sut = new WebProviderService(
|
||||||
@@ -42,14 +48,69 @@ describe("WebProviderService", () => {
|
|||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
stateProvider,
|
|
||||||
providerApiService,
|
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", () => {
|
describe("createClientOrganization", () => {
|
||||||
const activeUserId = newGuid() as UserId;
|
|
||||||
const providerId = "provider-123";
|
|
||||||
const name = "Test Org";
|
const name = "Test Org";
|
||||||
const ownerEmail = "owner@example.com";
|
const ownerEmail = "owner@example.com";
|
||||||
const planType = PlanType.EnterpriseAnnually;
|
const planType = PlanType.EnterpriseAnnually;
|
||||||
@@ -59,15 +120,13 @@ describe("WebProviderService", () => {
|
|||||||
const encryptedProviderKey = new EncString("encrypted-provider-key");
|
const encryptedProviderKey = new EncString("encrypted-provider-key");
|
||||||
const encryptedCollectionName = new EncString("encrypted-collection-name");
|
const encryptedCollectionName = new EncString("encrypted-collection-name");
|
||||||
const defaultCollectionTranslation = "Default Collection";
|
const defaultCollectionTranslation = "Default Collection";
|
||||||
const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey;
|
|
||||||
const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]);
|
keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]);
|
||||||
keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]);
|
keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]);
|
||||||
i18nService.t.mockReturnValue(defaultCollectionTranslation);
|
i18nService.t.mockReturnValue(defaultCollectionTranslation);
|
||||||
encryptService.encryptString.mockResolvedValue(encryptedCollectionName);
|
encryptService.encryptString.mockResolvedValue(encryptedCollectionName);
|
||||||
keyService.getProviderKey.mockResolvedValue(mockProviderKey);
|
keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById));
|
||||||
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey);
|
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +147,7 @@ describe("WebProviderService", () => {
|
|||||||
defaultCollectionTranslation,
|
defaultCollectionTranslation,
|
||||||
mockOrgKey,
|
mockOrgKey,
|
||||||
);
|
);
|
||||||
expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId);
|
expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId);
|
||||||
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
|
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
|
||||||
|
|
||||||
expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith(
|
expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith(
|
||||||
@@ -107,5 +166,27 @@ describe("WebProviderService", () => {
|
|||||||
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||||
expect(syncService.fullSync).toHaveBeenCalledWith(true);
|
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
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||||
import { switchMap } from "rxjs/operators";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
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 { 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 { 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 { PlanType } from "@bitwarden/common/billing/enums";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
|
||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
@@ -25,18 +24,26 @@ export class WebProviderService {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private stateProvider: StateProvider,
|
|
||||||
private providerApiService: ProviderApiServiceAbstraction,
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async addOrganizationToProvider(providerId: string, organizationId: string): Promise<void> {
|
async addOrganizationToProvider(
|
||||||
const orgKey = await firstValueFrom(
|
providerId: string,
|
||||||
this.stateProvider.activeUserId$.pipe(
|
organizationId: string,
|
||||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
activeUserId: UserId,
|
||||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
): 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);
|
const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey);
|
||||||
await this.providerApiService.addOrganizationToProvider(providerId, {
|
await this.providerApiService.addOrganizationToProvider(providerId, {
|
||||||
key: encryptedOrgKey.encryptedString,
|
key: encryptedOrgKey.encryptedString,
|
||||||
@@ -62,7 +69,12 @@ export class WebProviderService {
|
|||||||
organizationKey,
|
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(
|
const encryptedProviderKey = await this.encryptService.wrapSymmetricKey(
|
||||||
organizationKey,
|
organizationKey,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { WrappedSigningKey } from "@bitwarden/common/key-management/types";
|
import { WrappedSigningKey } from "@bitwarden/common/key-management/types";
|
||||||
import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums";
|
import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import {
|
import {
|
||||||
UserKey,
|
UserKey,
|
||||||
MasterKey,
|
MasterKey,
|
||||||
@@ -248,17 +248,19 @@ export abstract class KeyService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the provider keys for a given user.
|
* Stores the provider keys for a given user.
|
||||||
* @param orgs The provider orgs for which to save the keys from.
|
* @param providers The provider orgs for which to save the keys from.
|
||||||
* @param userId The user id of the user for which to store the keys for.
|
* @param userId The user id of the user for which to store the keys for.
|
||||||
*/
|
*/
|
||||||
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
|
abstract setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Gets an observable of provider keys for the given user.
|
||||||
* @throws Error when providerId is null or no active user
|
* @param userId The user to get provider keys for.
|
||||||
* @param providerId The desired provider
|
* @return An observable stream of the users providers keys if they are unlocked, or null if the user is not unlocked.
|
||||||
* @returns The provider's symmetric key
|
* @throws If an invalid user id is passed in.
|
||||||
*/
|
*/
|
||||||
abstract getProviderKey(providerId: string): Promise<ProviderKey | null>;
|
abstract providerKeys$(userId: UserId): Observable<Record<ProviderId, ProviderKey> | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new organization key and encrypts it with the user's public key.
|
* Creates a new organization key and encrypts it with the user's public key.
|
||||||
* This method can also return Provider keys for creating new Provider users.
|
* This method can also return Provider keys for creating new Provider users.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
FakeSingleUserState,
|
FakeSingleUserState,
|
||||||
} from "@bitwarden/common/spec";
|
} from "@bitwarden/common/spec";
|
||||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import {
|
import {
|
||||||
UserKey,
|
UserKey,
|
||||||
MasterKey,
|
MasterKey,
|
||||||
@@ -1314,6 +1314,49 @@ describe("keyService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("providerKeys$", () => {
|
||||||
|
let mockUserPrivateKey: Uint8Array;
|
||||||
|
let mockProviderKeys: Record<ProviderId, ProviderKey>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUserPrivateKey = makeStaticByteArray(64, 1);
|
||||||
|
mockProviderKeys = {
|
||||||
|
["provider1" as ProviderId]: makeSymmetricCryptoKey<ProviderKey>(64),
|
||||||
|
["provider2" as ProviderId]: makeSymmetricCryptoKey<ProviderKey>(64),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when userPrivateKey is null", async () => {
|
||||||
|
jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(null));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(keyService.providerKeys$(mockUserId));
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns provider keys when userPrivateKey is available", async () => {
|
||||||
|
jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any));
|
||||||
|
jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(mockProviderKeys));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(keyService.providerKeys$(mockUserId));
|
||||||
|
|
||||||
|
expect(result).toEqual(mockProviderKeys);
|
||||||
|
expect((keyService as any).providerKeysHelper$).toHaveBeenCalledWith(
|
||||||
|
mockUserId,
|
||||||
|
mockUserPrivateKey,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when providerKeysHelper$ returns null", async () => {
|
||||||
|
jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any));
|
||||||
|
jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(null));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(keyService.providerKeys$(mockUserId));
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("makeKeyPair", () => {
|
describe("makeKeyPair", () => {
|
||||||
test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])(
|
test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])(
|
||||||
"throws when the provided key is %s",
|
"throws when the provided key is %s",
|
||||||
|
|||||||
@@ -426,20 +426,16 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Deprecate in favor of observable
|
providerKeys$(userId: UserId): Observable<Record<ProviderId, ProviderKey> | null> {
|
||||||
async getProviderKey(providerId: ProviderId): Promise<ProviderKey | null> {
|
return this.userPrivateKey$(userId).pipe(
|
||||||
if (providerId == null) {
|
switchMap((userPrivateKey) => {
|
||||||
return null;
|
if (userPrivateKey == null) {
|
||||||
}
|
return of(null);
|
||||||
|
}
|
||||||
|
|
||||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
return this.providerKeysHelper$(userId, userPrivateKey);
|
||||||
if (activeUserId == null) {
|
}),
|
||||||
throw new Error("No active user found.");
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
|
|
||||||
|
|
||||||
return providerKeys?.[providerId] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearProviderKeys(userId: UserId): Promise<void> {
|
private async clearProviderKeys(userId: UserId): Promise<void> {
|
||||||
@@ -829,18 +825,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
)) as UserPrivateKey;
|
)) as UserPrivateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
providerKeys$(userId: UserId) {
|
|
||||||
return this.userPrivateKey$(userId).pipe(
|
|
||||||
switchMap((userPrivateKey) => {
|
|
||||||
if (userPrivateKey == null) {
|
|
||||||
return of(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.providerKeysHelper$(userId, userPrivateKey);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper for decrypting provider keys that requires a user id and that users decrypted private key
|
* A helper for decrypting provider keys that requires a user id and that users decrypted private key
|
||||||
* this is helpful for when you may have already grabbed the user private key and don't want to redo
|
* this is helpful for when you may have already grabbed the user private key and don't want to redo
|
||||||
|
|||||||
Reference in New Issue
Block a user