Merge branch 'main' into auth/pm-7392/token-service-add-secure-storage-fallback

This commit is contained in:
Jared Snider 2024-05-03 14:56:26 -04:00 committed by GitHub
commit 457e7a4819
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 369 additions and 234 deletions

View File

@ -1,5 +1,3 @@
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -15,21 +13,13 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
import { Account } from "../../models/account";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import { BrowserStateService } from "./abstractions/browser-state.service";
@browserSession
export class DefaultBrowserStateService
extends BaseStateService<GlobalState, Account>
implements BrowserStateService
{
@sessionSync({
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
initializeAs: "record",
})
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
protected accountDeserializer = Account.fromJSON;
constructor(

View File

@ -219,8 +219,10 @@ export class AppComponent implements OnInit, OnDestroy {
await this.vaultTimeoutService.lock(message.userId);
break;
case "lockAllVaults": {
const currentUser = await this.stateService.getUserId();
const accounts = await firstValueFrom(this.stateService.accounts$);
const currentUser = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a.id)),
);
const accounts = await firstValueFrom(this.accountService.accounts$);
await this.vaultTimeoutService.lock(currentUser);
for (const account of Object.keys(accounts)) {
if (account === currentUser) {
@ -701,7 +703,7 @@ export class AppComponent implements OnInit, OnDestroy {
}
private async checkForSystemTimeout(timeout: number): Promise<void> {
const accounts = await firstValueFrom(this.stateService.accounts$);
const accounts = await firstValueFrom(this.accountService.accounts$);
for (const userId in accounts) {
if (userId == null) {
continue;

View File

@ -221,7 +221,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: EncryptedMessageHandlerService,
deps: [
StateServiceAbstraction,
AccountServiceAbstraction,
AuthServiceAbstraction,
CipherServiceAbstraction,
PolicyServiceAbstraction,

View File

@ -1,12 +1,13 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -28,7 +29,7 @@ import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-me
export class EncryptedMessageHandlerService {
constructor(
private stateService: StateService,
private accountService: AccountService,
private authService: AuthService,
private cipherService: CipherService,
private policyService: PolicyService,
@ -62,7 +63,9 @@ export class EncryptedMessageHandlerService {
}
private async checkUserStatus(userId: string): Promise<string> {
const activeUserId = await this.stateService.getUserId();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (userId !== activeUserId) {
return "not-active-user";
@ -77,17 +80,19 @@ export class EncryptedMessageHandlerService {
}
private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
const accounts = await firstValueFrom(this.stateService.accounts$);
const activeUserId = await this.stateService.getUserId();
const accounts = await firstValueFrom(this.accountService.accounts$);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (!accounts || !Object.keys(accounts)) {
return [];
}
return Promise.all(
Object.keys(accounts).map(async (userId) => {
Object.keys(accounts).map(async (userId: UserId) => {
const authStatus = await this.authService.getAuthStatus(userId);
const email = await this.stateService.getEmail({ userId });
const email = accounts[userId].email;
return {
id: userId,
@ -107,7 +112,9 @@ export class EncryptedMessageHandlerService {
}
const ciphersResponse: CipherResponse[] = [];
const activeUserId = await this.stateService.getUserId();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const authStatus = await this.authService.getAuthStatus(activeUserId);
if (authStatus !== AuthenticationStatus.Unlocked) {

View File

@ -1,6 +1,7 @@
import { Injectable, NgZone } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -41,6 +42,7 @@ export class NativeMessagingService {
private biometricStateService: BiometricStateService,
private nativeMessageHandler: NativeMessageHandlerService,
private dialogService: DialogService,
private accountService: AccountService,
private ngZone: NgZone,
) {}
@ -51,9 +53,7 @@ export class NativeMessagingService {
private async messageHandler(msg: LegacyMessageWrapper | Message) {
const outerMessage = msg as Message;
if (outerMessage.version) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.nativeMessageHandler.handleMessage(outerMessage);
await this.nativeMessageHandler.handleMessage(outerMessage);
return;
}
@ -64,7 +64,7 @@ export class NativeMessagingService {
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey);
// Validate the UserId to ensure we are logged into the same account.
const accounts = await firstValueFrom(this.stateService.accounts$);
const accounts = await firstValueFrom(this.accountService.accounts$);
const userIds = Object.keys(accounts);
if (!userIds.includes(rawMessage.userId)) {
ipc.platform.nativeMessaging.sendMessage({
@ -81,7 +81,7 @@ export class NativeMessagingService {
});
const fingerprint = await this.cryptoService.getFingerprint(
await this.stateService.getUserId(),
rawMessage.userId,
remotePublicKey,
);
@ -98,9 +98,7 @@ export class NativeMessagingService {
}
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.secureCommunication(remotePublicKey, appId);
await this.secureCommunication(remotePublicKey, appId);
return;
}
@ -144,9 +142,7 @@ export class NativeMessagingService {
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
if (!(await biometricUnlockPromise)) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
await this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
return this.ngZone.run(() =>
this.dialogService.openSimpleDialog({
@ -172,9 +168,7 @@ export class NativeMessagingService {
// we send the master key still for backwards compatibility
// with older browser extensions
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.send(
await this.send(
{
command: "biometricUnlock",
response: "unlocked",
@ -184,14 +178,10 @@ export class NativeMessagingService {
appId,
);
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}
} catch (e) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}
break;

View File

@ -153,6 +153,7 @@ describe("EmergencyAccessService", () => {
} as EmergencyAccessTakeoverResponse);
const mockDecryptedGrantorUserKey = new Uint8Array(64);
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
@ -197,6 +198,7 @@ describe("EmergencyAccessService", () => {
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: 500,
} as EmergencyAccessTakeoverResponse);
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
await expect(
emergencyAccessService.takeover(mockId, mockEmail, mockName),
@ -204,6 +206,21 @@ describe("EmergencyAccessService", () => {
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
});
it("should throw an error if the users private key cannot be retrieved", async () => {
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
keyEncrypted: "EncryptedKey",
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: 500,
} as EmergencyAccessTakeoverResponse);
cryptoService.getPrivateKey.mockResolvedValue(null);
await expect(emergencyAccessService.takeover(mockId, mockEmail, mockName)).rejects.toThrow(
"user does not have a private key",
);
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
});
});
describe("getRotatedKeys", () => {

View File

@ -209,7 +209,16 @@ export class EmergencyAccessService {
async getViewOnlyCiphers(id: string): Promise<CipherView[]> {
const response = await this.emergencyAccessApiService.postEmergencyAccessView(id);
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
const activeUserPrivateKey = await this.cryptoService.getPrivateKey();
if (activeUserPrivateKey == null) {
throw new Error("Active user does not have a private key, cannot get view only ciphers.");
}
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
response.keyEncrypted,
activeUserPrivateKey,
);
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
const ciphers = await this.encryptService.decryptItems(
@ -229,7 +238,16 @@ export class EmergencyAccessService {
async takeover(id: string, masterPassword: string, email: string) {
const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id);
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
const activeUserPrivateKey = await this.cryptoService.getPrivateKey();
if (activeUserPrivateKey == null) {
throw new Error("Active user does not have a private key, cannot complete a takeover.");
}
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
takeoverResponse.keyEncrypted,
activeUserPrivateKey,
);
if (grantorKeyBuffer == null) {
throw new Error("Failed to decrypt grantor key");
}

View File

@ -1,17 +1,17 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, firstValueFrom, from } from "rxjs";
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
import { firstValueFrom, from, map } from "rxjs";
import { switchMap, takeUntil } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
import { PlanType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@ -40,10 +40,6 @@ export class ClientsComponent extends BaseClientsComponent {
manageOrganizations = false;
showAddExisting = false;
protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
);
constructor(
private router: Router,
private providerService: ProviderService,
@ -75,15 +71,10 @@ export class ClientsComponent extends BaseClientsComponent {
.pipe(
switchMap((params) => {
this.providerId = params.providerId;
return combineLatest([
this.providerService.get(this.providerId),
this.consolidatedBillingEnabled$,
]).pipe(
concatMap(([provider, consolidatedBillingEnabled]) => {
if (
consolidatedBillingEnabled &&
provider.providerStatus === ProviderStatusType.Billable
) {
return this.providerService.get$(this.providerId).pipe(
canAccessBilling(this.configService),
map((canAccessBilling) => {
if (canAccessBilling) {
return from(
this.router.navigate(["../manage-client-organizations"], {
relativeTo: this.activatedRoute,

View File

@ -1,5 +1,5 @@
<bit-layout variant="secondary">
<nav slot="sidebar" *ngIf="provider">
<nav slot="sidebar" *ngIf="provider$ | async as provider">
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block" [appA11yTitle]="'providerPortal' | i18n">
<bit-icon [icon]="logo"></bit-icon>
</a>
@ -7,14 +7,14 @@
<bit-nav-item
icon="bwi-bank"
[text]="'clients' | i18n"
[route]="
(enableConsolidatedBilling$ | async) &&
provider.providerStatus === ProviderStatusType.Billable
? 'manage-client-organizations'
: 'clients'
"
[route]="(canAccessBilling$ | async) ? 'manage-client-organizations' : 'clients'"
></bit-nav-item>
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
<bit-nav-group
icon="bwi-sliders"
[text]="'manage' | i18n"
route="manage"
*ngIf="showManageTab(provider)"
>
<bit-nav-item
[text]="'people' | i18n"
route="manage/people"
@ -26,13 +26,20 @@
*ngIf="provider.useEvents"
></bit-nav-item>
</bit-nav-group>
<bit-nav-group
icon="bwi-billing"
[text]="'billing' | i18n"
route="billing"
*ngIf="canAccessBilling$ | async"
>
<bit-nav-item [text]="'subscription' | i18n" route="billing/subscription"></bit-nav-item>
</bit-nav-group>
<bit-nav-item
icon="bwi-cogs"
[text]="'settings' | i18n"
route="settings"
*ngIf="showSettingsTab"
*ngIf="showSettingsTab(provider)"
></bit-nav-item>
<app-toggle-width></app-toggle-width>
</nav>
<app-payment-method-warnings

View File

@ -1,11 +1,13 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { switchMap, Observable, Subject, filter, startWith } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
@ -28,21 +30,17 @@ import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-wi
ToggleWidthComponent,
],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ProvidersLayoutComponent {
export class ProvidersLayoutComponent implements OnInit, OnDestroy {
protected readonly logo = ProviderPortalLogo;
provider: Provider;
private providerId: string;
private destroy$ = new Subject<void>();
protected provider$: Observable<Provider>;
protected canAccessBilling$: Observable<boolean>;
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
FeatureFlag.ShowPaymentMethodWarningBanners,
);
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
);
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
@ -51,37 +49,30 @@ export class ProvidersLayoutComponent {
ngOnInit() {
document.body.classList.remove("layout_frontend");
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
});
this.provider$ = this.route.params.pipe(
switchMap((params) => this.providerService.get$(params.providerId)),
takeUntil(this.destroy$),
);
this.canAccessBilling$ = this.provider$.pipe(
filter((provider) => !!provider),
canAccessBilling(this.configService),
startWith(false),
takeUntil(this.destroy$),
);
}
async load() {
this.provider = await this.providerService.get(this.providerId);
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get showMenuBar() {
return this.showManageTab || this.showSettingsTab;
showManageTab(provider: Provider) {
return provider.canManageUsers || provider.canAccessEventLogs;
}
get showManageTab() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
showSettingsTab(provider: Provider) {
return provider.isProviderAdmin;
}
get showSettingsTab() {
return this.provider.isProviderAdmin;
}
get manageRoute(): string {
switch (true) {
case this.provider.canManageUsers:
return "manage/people";
case this.provider.canAccessEventLogs:
return "manage/events";
}
}
protected readonly ProviderStatusType = ProviderStatusType;
}

View File

@ -7,7 +7,8 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
import { ProviderSubscriptionComponent, hasConsolidatedBilling } from "../../billing/providers";
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
@ -68,6 +69,7 @@ const routes: Routes = [
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
{
path: "manage-client-organizations",
canActivate: [hasConsolidatedBilling],
component: ManageClientOrganizationsComponent,
data: { titleId: "clients" },
},
@ -99,6 +101,25 @@ const routes: Routes = [
},
],
},
{
path: "billing",
canActivate: [hasConsolidatedBilling],
data: { providerPermissions: (provider: Provider) => provider.isProviderAdmin },
children: [
{
path: "",
pathMatch: "full",
redirectTo: "subscription",
},
{
path: "subscription",
component: ProviderSubscriptionComponent,
data: {
titleId: "subscription",
},
},
],
},
{
path: "settings",
children: [

View File

@ -9,6 +9,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
import { ProviderSubscriptionComponent } from "../../billing/providers";
import {
CreateClientOrganizationComponent,
ManageClientOrganizationSubscriptionComponent,
@ -62,6 +63,7 @@ import { SetupComponent } from "./setup/setup.component";
CreateClientOrganizationComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationSubscriptionComponent,
ProviderSubscriptionComponent,
],
providers: [WebProviderService, ProviderPermissionsGuard],
})

View File

@ -1,17 +1,17 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs";
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
import { firstValueFrom, from, lastValueFrom, map } from "rxjs";
import { switchMap, takeUntil } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@ -36,10 +36,6 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
loading = true;
manageOrganizations = false;
private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
);
protected plans: PlanResponse[];
constructor(
@ -72,23 +68,16 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
.pipe(
switchMap((params) => {
this.providerId = params.providerId;
return combineLatest([
this.providerService.get(this.providerId),
this.consolidatedBillingEnabled$,
]).pipe(
concatMap(([provider, consolidatedBillingEnabled]) => {
if (
!consolidatedBillingEnabled ||
provider.providerStatus !== ProviderStatusType.Billable
) {
return this.providerService.get$(this.providerId).pipe(
canAccessBilling(this.configService),
map((canAccessBilling) => {
if (!canAccessBilling) {
return from(
this.router.navigate(["../clients"], {
relativeTo: this.activatedRoute,
}),
);
} else {
this.provider = provider;
this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin;
return from(this.load());
}
}),
@ -104,6 +93,10 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
}
async load() {
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin;
this.clients = (await this.apiService.getProviderClients(this.providerId)).data;
this.dataSource.data = this.clients;

View File

@ -0,0 +1,30 @@
import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export const hasConsolidatedBilling: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
const configService = inject(ConfigService);
const providerService = inject(ProviderService);
const provider = await firstValueFrom(providerService.get$(route.params.providerId));
const consolidatedBillingEnabled = await configService.getFeatureFlag(
FeatureFlag.EnableConsolidatedBilling,
);
if (
!consolidatedBillingEnabled ||
!provider ||
!provider.isProviderAdmin ||
provider.providerStatus !== ProviderStatusType.Billable
) {
return createUrlTreeFromSnapshot(route, ["/providers", route.params.providerId]);
}
return true;
};

View File

@ -0,0 +1,2 @@
export * from "./guards/has-consolidated-billing.guard";
export * from "./provider-subscription.component";

View File

@ -0,0 +1 @@
<app-header></app-header>

View File

@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-provider-subscription",
templateUrl: "./provider-subscription.component.html",
})
export class ProviderSubscriptionComponent {}

View File

@ -117,13 +117,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
return super.logInTwoFactor(twoFactor);
}
protected override async setMasterKey(response: IdentityTokenResponse) {
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
const authRequestCredentials = this.cache.value.authRequestCredentials;
if (
authRequestCredentials.decryptedMasterKey &&
authRequestCredentials.decryptedMasterKeyHash
) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setMasterKey(
authRequestCredentials.decryptedMasterKey,
userId,
@ -147,15 +146,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
if (authRequestCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
} else {
await this.trySetUserKeyWithMasterKey();
await this.trySetUserKeyWithMasterKey(userId);
// Establish trust if required after setting user key
await this.deviceTrustService.trustDeviceIfRequired(userId);
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);

View File

@ -244,6 +244,7 @@ describe("LoginStrategy", () => {
const result = await passwordLoginStrategy.logIn(credentials);
expect(result).toEqual({
userId: userId,
forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
resetMasterPassword: true,
twoFactorProviders: null,

View File

@ -241,6 +241,7 @@ export abstract class LoginStrategy {
// Must come before setting keys, user key needs email to update additional keys
const userId = await this.saveAccountInformation(response);
result.userId = userId;
if (response.twoFactorToken != null) {
// note: we can read email from access token b/c it was saved in saveAccountInformation
@ -249,7 +250,7 @@ export abstract class LoginStrategy {
await this.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken);
}
await this.setMasterKey(response);
await this.setMasterKey(response, userId);
await this.setUserKey(response, userId);
await this.setPrivateKey(response);
@ -259,7 +260,7 @@ export abstract class LoginStrategy {
}
// The keys comes from different sources depending on the login strategy
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;

View File

@ -173,8 +173,11 @@ describe("PasswordLoginStrategy", () => {
localHashedPassword,
userId,
);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
tokenResponse.key,
userId,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});

View File

@ -147,6 +147,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
const [authResult, identityResponse] = await this.startLogIn();
if (identityResponse instanceof IdentityCaptchaResponse) {
return authResult;
}
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
@ -157,23 +161,23 @@ export class PasswordLoginStrategy extends LoginStrategy {
credentials,
masterPasswordPolicyOptions,
);
if (meetsRequirements) {
return authResult;
}
if (!meetsRequirements) {
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
// Save the flag to this strategy for later use as the master password is about to pass out of scope
this.cache.next({
...this.cache.value,
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
});
} else {
// Authentication was successful, save the force update password options with the state service
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
}
if (identityResponse instanceof IdentityTwoFactorResponse) {
// Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope
this.cache.next({
...this.cache.value,
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
});
} else {
// Authentication was successful, save the force update password options with the state service
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword,
authResult.userId, // userId is only available on successful login
);
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
}
}
return authResult;
@ -196,17 +200,18 @@ export class PasswordLoginStrategy extends LoginStrategy {
!result.requiresCaptcha &&
forcePasswordResetReason != ForceSetPasswordReason.None
) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId);
await this.masterPasswordService.setForceSetPasswordReason(
forcePasswordResetReason,
result.userId,
);
result.forcePasswordReset = forcePasswordResetReason;
}
return result;
}
protected override async setMasterKey(response: IdentityTokenResponse) {
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
const { masterKey, localMasterKeyHash } = this.cache.value;
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
}
@ -219,12 +224,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
if (this.encryptionKeyMigrationRequired(response)) {
return;
}
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
await this.cryptoService.setUserKey(userKey, userId);
}
}
@ -239,9 +244,9 @@ export class PasswordLoginStrategy extends LoginStrategy {
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse,
response: IdentityTokenResponse | IdentityTwoFactorResponse,
): MasterPasswordPolicyOptions {
if (response == null || response instanceof IdentityCaptchaResponse) {
if (response == null) {
return null;
}
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);

View File

@ -163,7 +163,10 @@ describe("SsoLoginStrategy", () => {
// Assert
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
tokenResponse.key,
userId,
);
});
describe("Trusted Device Decryption", () => {
@ -417,7 +420,7 @@ describe("SsoLoginStrategy", () => {
await ssoLoginStrategy.logIn(credentials);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
});
it("converts new SSO user with no master password to Key Connector on first login", async () => {
@ -430,6 +433,7 @@ describe("SsoLoginStrategy", () => {
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId,
userId,
);
});
@ -468,7 +472,7 @@ describe("SsoLoginStrategy", () => {
await ssoLoginStrategy.logIn(credentials);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
});
it("converts new SSO user with no master password to Key Connector on first login", async () => {
@ -481,6 +485,7 @@ describe("SsoLoginStrategy", () => {
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId,
userId,
);
});

View File

@ -8,6 +8,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
@ -124,7 +125,7 @@ export class SsoLoginStrategy extends LoginStrategy {
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
}
async logIn(credentials: SsoLoginCredentials) {
async logIn(credentials: SsoLoginCredentials): Promise<AuthResult> {
const data = new SsoLoginStrategyData();
data.orgId = credentials.orgId;
@ -147,10 +148,9 @@ export class SsoLoginStrategy extends LoginStrategy {
// Auth guard currently handles redirects for this.
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ssoAuthResult.forcePasswordReset,
userId,
ssoAuthResult.userId,
);
}
@ -163,7 +163,7 @@ export class SsoLoginStrategy extends LoginStrategy {
return ssoAuthResult;
}
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
protected override async setMasterKey(tokenResponse: IdentityTokenResponse, userId: UserId) {
// The only way we can be setting a master key at this point is if we are using Key Connector.
// First, check to make sure that we should do so based on the token response.
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
@ -175,10 +175,11 @@ export class SsoLoginStrategy extends LoginStrategy {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
tokenResponse,
this.cache.value.orgId,
userId,
);
} else {
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
}
}
}
@ -231,7 +232,7 @@ export class SsoLoginStrategy extends LoginStrategy {
if (masterKeyEncryptedUserKey) {
// set the master key encrypted user key if it exists
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId);
}
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
@ -251,7 +252,7 @@ export class SsoLoginStrategy extends LoginStrategy {
this.getKeyConnectorUrl(tokenResponse) != null
) {
// Key connector enabled for user
await this.trySetUserKeyWithMasterKey();
await this.trySetUserKeyWithMasterKey(userId);
}
// Note: In the traditional SSO flow with MP without key connector, the lock component
@ -338,8 +339,7 @@ export class SsoLoginStrategy extends LoginStrategy {
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
// There is a scenario in which the master key is not set here. That will occur if the user

View File

@ -174,7 +174,7 @@ describe("UserApiLoginStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
});
it("decrypts and sets the user key if Key Connector is enabled", async () => {
@ -195,6 +195,6 @@ describe("UserApiLoginStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
});
});

View File

@ -93,11 +93,11 @@ export class UserApiLoginStrategy extends LoginStrategy {
return authResult;
}
protected override async setMasterKey(response: IdentityTokenResponse) {
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
if (response.apiUseKeyConnector) {
const env = await firstValueFrom(this.environmentService.environment$);
const keyConnectorUrl = env.getKeyConnectorUrl();
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
}
}
@ -108,11 +108,10 @@ export class UserApiLoginStrategy extends LoginStrategy {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (response.apiUseKeyConnector) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
await this.cryptoService.setUserKey(userKey, userId);
}
}
}
@ -123,6 +122,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
);
}
// Overridden to save client ID and secret to token service
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
const userId = await super.saveAccountInformation(tokenResponse);

View File

@ -208,7 +208,10 @@ describe("WebAuthnLoginStrategy", () => {
// Assert
// Master key encrypted user key should be set
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(idTokenResponse.key);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
idTokenResponse.key,
userId,
);
expect(cryptoService.decryptToBytes).toHaveBeenCalledTimes(1);
expect(cryptoService.decryptToBytes).toHaveBeenCalledWith(
@ -220,7 +223,7 @@ describe("WebAuthnLoginStrategy", () => {
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
mockPrfPrivateKey,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
// Master key and private key should not be set

View File

@ -98,7 +98,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
throw new Error("2FA not supported yet for WebAuthn Login.");
}
protected override async setMasterKey() {
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
return Promise.resolve();
}
@ -107,7 +107,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
if (masterKeyEncryptedUserKey) {
// set the master key encrypted user key if it exists
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId);
}
const userDecryptionOptions = idTokenResponse?.userDecryptionOptions;
@ -134,7 +134,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
);
if (userKey) {
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey);
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId);
}
}
}

View File

@ -1,8 +1,11 @@
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
import { ProviderData } from "../models/data/provider.data";
import { Provider } from "../models/domain/provider";
export abstract class ProviderService {
get$: (id: string) => Observable<Provider>;
get: (id: string) => Promise<Provider>;
getAll: () => Promise<Provider[]>;
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;

View File

@ -25,7 +25,13 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
constructor(private key: string) {}
async decrypt(cryptoService: CryptoService) {
const decValue = await cryptoService.rsaDecrypt(this.key);
const activeUserPrivateKey = await cryptoService.getPrivateKey();
if (activeUserPrivateKey == null) {
throw new Error("Active user does not have a private key, cannot decrypt organization key.");
}
const decValue = await cryptoService.rsaDecrypt(this.key, activeUserPrivateKey);
return new SymmetricCryptoKey(decValue) as OrgKey;
}

View File

@ -1,3 +1,5 @@
import { firstValueFrom } from "rxjs";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { Utils } from "../../platform/misc/utils";
@ -86,6 +88,7 @@ describe("ProviderService", () => {
fakeStateProvider = new FakeStateProvider(fakeAccountService);
fakeUserState = fakeStateProvider.singleUser.getFake(fakeUserId, PROVIDERS);
fakeActiveUserState = fakeStateProvider.activeUser.getFake(PROVIDERS);
providerService = new ProviderService(fakeStateProvider);
});
@ -106,6 +109,22 @@ describe("ProviderService", () => {
});
});
describe("get$()", () => {
it("Returns an observable of a single provider from state that matches the specified id", async () => {
const mockData = buildMockProviders(5);
fakeUserState.nextState(arrayToRecord(mockData));
const result = providerService.get$(mockData[3].id);
const provider = await firstValueFrom(result);
expect(provider).toEqual(new Provider(mockData[3]));
});
it("Returns an observable of undefined if the specified provider is not found", async () => {
const result = providerService.get$("this-provider-does-not-exist");
const provider = await firstValueFrom(result);
expect(provider).toBe(undefined);
});
});
describe("get()", () => {
it("Returns a single provider from state that matches the specified id", async () => {
const mockData = buildMockProviders(5);

View File

@ -1,6 +1,6 @@
import { Observable, map, firstValueFrom, of, switchMap, take } from "rxjs";
import { firstValueFrom, map, Observable, of, switchMap, take } from "rxjs";
import { UserKeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state";
import { PROVIDERS_DISK, StateProvider, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
import { ProviderData } from "../models/data/provider.data";
@ -38,6 +38,10 @@ export class ProviderService implements ProviderServiceAbstraction {
);
}
get$(id: string): Observable<Provider> {
return this.providers$().pipe(mapToSingleProvider(id));
}
async get(id: string): Promise<Provider> {
return await firstValueFrom(this.providers$().pipe(mapToSingleProvider(id)));
}

View File

@ -1,8 +1,9 @@
import { Organization } from "../../admin-console/models/domain/organization";
import { UserId } from "../../types/guid";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
export abstract class KeyConnectorService {
setMasterKeyFromUrl: (url?: string) => Promise<void>;
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;
@ -10,6 +11,7 @@ export abstract class KeyConnectorService {
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string,
userId: UserId,
) => Promise<void>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;

View File

@ -1,9 +1,11 @@
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { ForceSetPasswordReason } from "./force-set-password-reason";
export class AuthResult {
userId: UserId;
captchaSiteKey = "";
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
/**

View File

@ -215,7 +215,7 @@ describe("KeyConnectorService", () => {
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
// Act
await keyConnectorService.setMasterKeyFromUrl(url);
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
// Assert
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
@ -235,7 +235,7 @@ describe("KeyConnectorService", () => {
try {
// Act
await keyConnectorService.setMasterKeyFromUrl(url);
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
} catch {
// Assert
expect(logService.error).toHaveBeenCalledWith(error);

View File

@ -16,6 +16,7 @@ import {
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
import { UserId } from "../../types/guid";
import { MasterKey } from "../../types/key";
import { AccountService } from "../abstractions/account.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
@ -100,12 +101,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
}
// TODO: UserKey should be renamed to MasterKey and typed accordingly
async setMasterKeyFromUrl(url: string) {
async setMasterKeyFromUrl(url: string, userId: UserId) {
try {
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setMasterKey(masterKey, userId);
} catch (e) {
this.handleKeyConnectorError(e);
@ -123,7 +123,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
);
}
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
async convertNewSsoUserToKeyConnector(
tokenResponse: IdentityTokenResponse,
orgId: string,
userId: UserId,
) {
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
const {
kdf,
@ -145,12 +149,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
kdfConfig,
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.cryptoService.makeUserKey(masterKey);
await this.cryptoService.setUserKey(userKey[0]);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
await this.cryptoService.setUserKey(userKey[0], userId);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();

View File

@ -0,0 +1,25 @@
import { map, Observable, OperatorFunction, switchMap } from "rxjs";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
type MaybeProvider = Provider | undefined;
export const canAccessBilling = (
configService: ConfigService,
): OperatorFunction<MaybeProvider, boolean> =>
switchMap<MaybeProvider, Observable<boolean>>((provider) =>
configService
.getFeatureFlag$(FeatureFlag.EnableConsolidatedBilling)
.pipe(
map((consolidatedBillingEnabled) =>
provider
? provider.isProviderAdmin &&
provider.providerStatus === ProviderStatusType.Billable &&
consolidatedBillingEnabled
: false,
),
),
);

View File

@ -311,15 +311,17 @@ export abstract class CryptoService {
* @param data The data to encrypt
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
* @returns The encrypted data
* @throws If the given publicKey is a null-ish value.
*/
abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString>;
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
/**
* Decrypts a value using RSA.
* @param encValue The encrypted value to decrypt
* @param privateKeyValue The private key to use for decryption
* @param privateKey The private key to use for decryption
* @returns The decrypted value
* @throws If the given privateKey is a null-ish value.
*/
abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array>;
abstract rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise<Uint8Array>;
abstract randomNumber(min: number, max: number): Promise<number>;
/**
* Generates a new cipher key

View File

@ -1,5 +1,3 @@
import { Observable } from "rxjs";
import { BiometricKey } from "../../auth/types/biometric-key";
import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
@ -24,8 +22,6 @@ export type InitOptions = {
};
export abstract class StateService<T extends Account = Account> {
accounts$: Observable<{ [userId: string]: T }>;
addAccount: (account: T) => Promise<void>;
clearDecryptedData: (userId: UserId) => Promise<void>;
clean: (options?: StorageOptions) => Promise<void>;

View File

@ -621,19 +621,20 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
}
async rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString> {
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
if (publicKey == null) {
publicKey = await this.getPublicKey();
}
if (publicKey == null) {
throw new Error("Public key unavailable.");
throw new Error("'publicKey' is a required parameter and must be non-null");
}
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
}
async rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array> {
async rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise<Uint8Array> {
if (privateKey == null) {
throw new Error("'privateKey' is a required parameter and must be non-null");
}
const headerPieces = encValue.split(".");
let encType: EncryptionType = null;
let encPieces: string[];
@ -665,10 +666,6 @@ export class CryptoService implements CryptoServiceAbstraction {
}
const data = Utils.fromB64ToArray(encPieces[0]);
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
if (privateKey == null) {
throw new Error("No private key.");
}
let alg: "sha1" | "sha256" = "sha1";
switch (encType) {
@ -771,6 +768,13 @@ export class CryptoService implements CryptoServiceAbstraction {
publicKey: string;
privateKey: EncString;
}> {
// Verify user key doesn't exist
const existingUserKey = await this.getUserKey();
if (existingUserKey != null) {
this.logService.error("Tried to initialize account with existing user key.");
throw new Error("Cannot initialize account, keys already exist.");
}
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
await this.setUserKey(userKey);

View File

@ -65,6 +65,10 @@ describe("derived decrypted org keys", () => {
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
};
const userPrivateKey = makeStaticByteArray(64, 3);
cryptoService.getPrivateKey.mockResolvedValue(userPrivateKey);
// TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key);
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);

View File

@ -1,4 +1,4 @@
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import { Jsonify, JsonValue } from "type-fest";
import { AccountService } from "../../auth/abstractions/account.service";
@ -52,9 +52,6 @@ export class StateService<
TAccount extends Account = Account,
> implements StateServiceAbstraction<TAccount>
{
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
accounts$ = this.accountsSubject.asObservable();
private hasBeenInited = false;
protected isRecoveredSession = false;
@ -115,8 +112,6 @@ export class StateService<
state = await this.syncAccountFromDisk(authenticatedAccounts[i]);
}
await this.pushAccounts();
return state;
});
}
@ -153,7 +148,6 @@ export class StateService<
await this.removeAccountFromDisk(options?.userId);
await this.removeAccountFromMemory(options?.userId);
await this.pushAccounts();
}
/**
@ -856,7 +850,6 @@ export class StateService<
});
});
}
await this.pushAccounts();
}
protected async scaffoldNewAccountStorage(account: TAccount): Promise<void> {
@ -934,17 +927,6 @@ export class StateService<
);
}
protected async pushAccounts(): Promise<void> {
await this.state().then((state) => {
if (state.accounts == null || Object.keys(state.accounts).length < 1) {
this.accountsSubject.next({});
return;
}
this.accountsSubject.next(state.accounts);
});
}
protected reconcileOptions(
requestedOptions: StorageOptions,
defaultOptions: StorageOptions,
@ -1096,8 +1078,6 @@ export class StateService<
return state;
});
await this.pushAccounts();
}
protected createAccount(init: Partial<TAccount> = null): TAccount {