mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
Merge branch 'main' into autofill/pm-5189-fix-issues-present-with-inline-menu-rendering-in-iframes
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
|
|
||||||
|
|
||||||
import { BrowserStateService } from "./abstractions/browser-state.service";
|
import { BrowserStateService } from "./abstractions/browser-state.service";
|
||||||
|
|
||||||
@browserSession
|
|
||||||
export class DefaultBrowserStateService
|
export class DefaultBrowserStateService
|
||||||
extends BaseStateService<GlobalState, Account>
|
extends BaseStateService<GlobalState, Account>
|
||||||
implements BrowserStateService
|
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;
|
protected accountDeserializer = Account.fromJSON;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -218,8 +218,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
await this.vaultTimeoutService.lock(message.userId);
|
await this.vaultTimeoutService.lock(message.userId);
|
||||||
break;
|
break;
|
||||||
case "lockAllVaults": {
|
case "lockAllVaults": {
|
||||||
const currentUser = await this.stateService.getUserId();
|
const currentUser = await firstValueFrom(
|
||||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
this.accountService.activeAccount$.pipe(map((a) => a.id)),
|
||||||
|
);
|
||||||
|
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||||
await this.vaultTimeoutService.lock(currentUser);
|
await this.vaultTimeoutService.lock(currentUser);
|
||||||
for (const account of Object.keys(accounts)) {
|
for (const account of Object.keys(accounts)) {
|
||||||
if (account === currentUser) {
|
if (account === currentUser) {
|
||||||
@@ -690,7 +692,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkForSystemTimeout(timeout: number): Promise<void> {
|
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) {
|
for (const userId in accounts) {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EncryptedMessageHandlerService,
|
provide: EncryptedMessageHandlerService,
|
||||||
deps: [
|
deps: [
|
||||||
StateServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
CipherServiceAbstraction,
|
CipherServiceAbstraction,
|
||||||
PolicyServiceAbstraction,
|
PolicyServiceAbstraction,
|
||||||
|
|||||||
@@ -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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
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 {
|
export class EncryptedMessageHandlerService {
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private accountService: AccountService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
@@ -62,7 +63,9 @@ export class EncryptedMessageHandlerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkUserStatus(userId: string): Promise<string> {
|
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) {
|
if (userId !== activeUserId) {
|
||||||
return "not-active-user";
|
return "not-active-user";
|
||||||
@@ -77,17 +80,19 @@ export class EncryptedMessageHandlerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
|
private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
|
||||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||||
const activeUserId = await this.stateService.getUserId();
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
if (!accounts || !Object.keys(accounts)) {
|
if (!accounts || !Object.keys(accounts)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
Object.keys(accounts).map(async (userId) => {
|
Object.keys(accounts).map(async (userId: UserId) => {
|
||||||
const authStatus = await this.authService.getAuthStatus(userId);
|
const authStatus = await this.authService.getAuthStatus(userId);
|
||||||
const email = await this.stateService.getEmail({ userId });
|
const email = accounts[userId].email;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -107,7 +112,9 @@ export class EncryptedMessageHandlerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ciphersResponse: CipherResponse[] = [];
|
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);
|
const authStatus = await this.authService.getAuthStatus(activeUserId);
|
||||||
|
|
||||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, NgZone } from "@angular/core";
|
import { Injectable, NgZone } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
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 { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@@ -41,6 +42,7 @@ export class NativeMessagingService {
|
|||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private nativeMessageHandler: NativeMessageHandlerService,
|
private nativeMessageHandler: NativeMessageHandlerService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private accountService: AccountService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -51,9 +53,7 @@ export class NativeMessagingService {
|
|||||||
private async messageHandler(msg: LegacyMessageWrapper | Message) {
|
private async messageHandler(msg: LegacyMessageWrapper | Message) {
|
||||||
const outerMessage = msg as Message;
|
const outerMessage = msg as Message;
|
||||||
if (outerMessage.version) {
|
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.
|
await this.nativeMessageHandler.handleMessage(outerMessage);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.nativeMessageHandler.handleMessage(outerMessage);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export class NativeMessagingService {
|
|||||||
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey);
|
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey);
|
||||||
|
|
||||||
// Validate the UserId to ensure we are logged into the same account.
|
// 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);
|
const userIds = Object.keys(accounts);
|
||||||
if (!userIds.includes(rawMessage.userId)) {
|
if (!userIds.includes(rawMessage.userId)) {
|
||||||
ipc.platform.nativeMessaging.sendMessage({
|
ipc.platform.nativeMessaging.sendMessage({
|
||||||
@@ -81,7 +81,7 @@ export class NativeMessagingService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fingerprint = await this.cryptoService.getFingerprint(
|
const fingerprint = await this.cryptoService.getFingerprint(
|
||||||
await this.stateService.getUserId(),
|
rawMessage.userId,
|
||||||
remotePublicKey,
|
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.
|
await this.secureCommunication(remotePublicKey, appId);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.secureCommunication(remotePublicKey, appId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,9 +142,7 @@ export class NativeMessagingService {
|
|||||||
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||||
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
||||||
if (!(await biometricUnlockPromise)) {
|
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.
|
await this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
|
||||||
|
|
||||||
return this.ngZone.run(() =>
|
return this.ngZone.run(() =>
|
||||||
this.dialogService.openSimpleDialog({
|
this.dialogService.openSimpleDialog({
|
||||||
@@ -172,9 +168,7 @@ export class NativeMessagingService {
|
|||||||
// we send the master key still for backwards compatibility
|
// we send the master key still for backwards compatibility
|
||||||
// with older browser extensions
|
// with older browser extensions
|
||||||
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
|
// 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.
|
await this.send(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.send(
|
|
||||||
{
|
{
|
||||||
command: "biometricUnlock",
|
command: "biometricUnlock",
|
||||||
response: "unlocked",
|
response: "unlocked",
|
||||||
@@ -184,14 +178,10 @@ export class NativeMessagingService {
|
|||||||
appId,
|
appId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
} as EmergencyAccessTakeoverResponse);
|
} as EmergencyAccessTakeoverResponse);
|
||||||
|
|
||||||
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
||||||
|
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
||||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
||||||
|
|
||||||
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
||||||
@@ -197,6 +198,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
kdf: KdfType.PBKDF2_SHA256,
|
kdf: KdfType.PBKDF2_SHA256,
|
||||||
kdfIterations: 500,
|
kdfIterations: 500,
|
||||||
} as EmergencyAccessTakeoverResponse);
|
} as EmergencyAccessTakeoverResponse);
|
||||||
|
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
emergencyAccessService.takeover(mockId, mockEmail, mockName),
|
emergencyAccessService.takeover(mockId, mockEmail, mockName),
|
||||||
@@ -204,6 +206,21 @@ describe("EmergencyAccessService", () => {
|
|||||||
|
|
||||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
|
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", () => {
|
describe("getRotatedKeys", () => {
|
||||||
|
|||||||
@@ -209,7 +209,16 @@ export class EmergencyAccessService {
|
|||||||
async getViewOnlyCiphers(id: string): Promise<CipherView[]> {
|
async getViewOnlyCiphers(id: string): Promise<CipherView[]> {
|
||||||
const response = await this.emergencyAccessApiService.postEmergencyAccessView(id);
|
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 grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||||
|
|
||||||
const ciphers = await this.encryptService.decryptItems(
|
const ciphers = await this.encryptService.decryptItems(
|
||||||
@@ -229,7 +238,16 @@ export class EmergencyAccessService {
|
|||||||
async takeover(id: string, masterPassword: string, email: string) {
|
async takeover(id: string, masterPassword: string, email: string) {
|
||||||
const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id);
|
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) {
|
if (grantorKeyBuffer == null) {
|
||||||
throw new Error("Failed to decrypt grantor key");
|
throw new Error("Failed to decrypt grantor key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { combineLatest, firstValueFrom, from } from "rxjs";
|
import { firstValueFrom, from, map } from "rxjs";
|
||||||
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
|
import { switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
@@ -40,10 +40,6 @@ export class ClientsComponent extends BaseClientsComponent {
|
|||||||
manageOrganizations = false;
|
manageOrganizations = false;
|
||||||
showAddExisting = false;
|
showAddExisting = false;
|
||||||
|
|
||||||
protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.EnableConsolidatedBilling,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
@@ -75,15 +71,10 @@ export class ClientsComponent extends BaseClientsComponent {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params) => {
|
switchMap((params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
return combineLatest([
|
return this.providerService.get$(this.providerId).pipe(
|
||||||
this.providerService.get(this.providerId),
|
canAccessBilling(this.configService),
|
||||||
this.consolidatedBillingEnabled$,
|
map((canAccessBilling) => {
|
||||||
]).pipe(
|
if (canAccessBilling) {
|
||||||
concatMap(([provider, consolidatedBillingEnabled]) => {
|
|
||||||
if (
|
|
||||||
consolidatedBillingEnabled &&
|
|
||||||
provider.providerStatus === ProviderStatusType.Billable
|
|
||||||
) {
|
|
||||||
return from(
|
return from(
|
||||||
this.router.navigate(["../manage-client-organizations"], {
|
this.router.navigate(["../manage-client-organizations"], {
|
||||||
relativeTo: this.activatedRoute,
|
relativeTo: this.activatedRoute,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<bit-layout variant="secondary">
|
<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">
|
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block" [appA11yTitle]="'providerPortal' | i18n">
|
||||||
<bit-icon [icon]="logo"></bit-icon>
|
<bit-icon [icon]="logo"></bit-icon>
|
||||||
</a>
|
</a>
|
||||||
@@ -7,14 +7,14 @@
|
|||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
icon="bwi-bank"
|
icon="bwi-bank"
|
||||||
[text]="'clients' | i18n"
|
[text]="'clients' | i18n"
|
||||||
[route]="
|
[route]="(canAccessBilling$ | async) ? 'manage-client-organizations' : 'clients'"
|
||||||
(enableConsolidatedBilling$ | async) &&
|
|
||||||
provider.providerStatus === ProviderStatusType.Billable
|
|
||||||
? 'manage-client-organizations'
|
|
||||||
: 'clients'
|
|
||||||
"
|
|
||||||
></bit-nav-item>
|
></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
|
<bit-nav-item
|
||||||
[text]="'people' | i18n"
|
[text]="'people' | i18n"
|
||||||
route="manage/people"
|
route="manage/people"
|
||||||
@@ -26,13 +26,20 @@
|
|||||||
*ngIf="provider.useEvents"
|
*ngIf="provider.useEvents"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
</bit-nav-group>
|
</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
|
<bit-nav-item
|
||||||
icon="bwi-cogs"
|
icon="bwi-cogs"
|
||||||
[text]="'settings' | i18n"
|
[text]="'settings' | i18n"
|
||||||
route="settings"
|
route="settings"
|
||||||
*ngIf="showSettingsTab"
|
*ngIf="showSettingsTab(provider)"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
|
|
||||||
<app-toggle-width></app-toggle-width>
|
<app-toggle-width></app-toggle-width>
|
||||||
</nav>
|
</nav>
|
||||||
<app-payment-method-warnings
|
<app-payment-method-warnings
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
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 { 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||||
@@ -28,21 +30,17 @@ import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-wi
|
|||||||
ToggleWidthComponent,
|
ToggleWidthComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||||
export class ProvidersLayoutComponent {
|
|
||||||
protected readonly logo = ProviderPortalLogo;
|
protected readonly logo = ProviderPortalLogo;
|
||||||
|
|
||||||
provider: Provider;
|
private destroy$ = new Subject<void>();
|
||||||
private providerId: string;
|
protected provider$: Observable<Provider>;
|
||||||
|
protected canAccessBilling$: Observable<boolean>;
|
||||||
|
|
||||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
);
|
);
|
||||||
|
|
||||||
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.EnableConsolidatedBilling,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
@@ -51,37 +49,30 @@ export class ProvidersLayoutComponent {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
document.body.classList.remove("layout_frontend");
|
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.provider$ = this.route.params.pipe(
|
||||||
this.providerId = params.providerId;
|
switchMap((params) => this.providerService.get$(params.providerId)),
|
||||||
await this.load();
|
takeUntil(this.destroy$),
|
||||||
});
|
);
|
||||||
|
|
||||||
|
this.canAccessBilling$ = this.provider$.pipe(
|
||||||
|
filter((provider) => !!provider),
|
||||||
|
canAccessBilling(this.configService),
|
||||||
|
startWith(false),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
ngOnDestroy() {
|
||||||
this.provider = await this.providerService.get(this.providerId);
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
get showMenuBar() {
|
showManageTab(provider: Provider) {
|
||||||
return this.showManageTab || this.showSettingsTab;
|
return provider.canManageUsers || provider.canAccessEventLogs;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showManageTab() {
|
showSettingsTab(provider: Provider) {
|
||||||
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
|
||||||
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-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 { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
@@ -68,6 +69,7 @@ const routes: Routes = [
|
|||||||
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
||||||
{
|
{
|
||||||
path: "manage-client-organizations",
|
path: "manage-client-organizations",
|
||||||
|
canActivate: [hasConsolidatedBilling],
|
||||||
component: ManageClientOrganizationsComponent,
|
component: ManageClientOrganizationsComponent,
|
||||||
data: { titleId: "clients" },
|
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",
|
path: "settings",
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
|
|||||||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||||
|
|
||||||
|
import { ProviderSubscriptionComponent } from "../../billing/providers";
|
||||||
import {
|
import {
|
||||||
CreateClientOrganizationComponent,
|
CreateClientOrganizationComponent,
|
||||||
ManageClientOrganizationSubscriptionComponent,
|
ManageClientOrganizationSubscriptionComponent,
|
||||||
@@ -62,6 +63,7 @@ import { SetupComponent } from "./setup/setup.component";
|
|||||||
CreateClientOrganizationComponent,
|
CreateClientOrganizationComponent,
|
||||||
ManageClientOrganizationsComponent,
|
ManageClientOrganizationsComponent,
|
||||||
ManageClientOrganizationSubscriptionComponent,
|
ManageClientOrganizationSubscriptionComponent,
|
||||||
|
ProviderSubscriptionComponent,
|
||||||
],
|
],
|
||||||
providers: [WebProviderService, ProviderPermissionsGuard],
|
providers: [WebProviderService, ProviderPermissionsGuard],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs";
|
import { firstValueFrom, from, lastValueFrom, map } from "rxjs";
|
||||||
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
|
import { switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.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 { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
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 { 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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
@@ -36,10 +36,6 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||||||
loading = true;
|
loading = true;
|
||||||
manageOrganizations = false;
|
manageOrganizations = false;
|
||||||
|
|
||||||
private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.EnableConsolidatedBilling,
|
|
||||||
);
|
|
||||||
|
|
||||||
protected plans: PlanResponse[];
|
protected plans: PlanResponse[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -72,23 +68,16 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params) => {
|
switchMap((params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
return combineLatest([
|
return this.providerService.get$(this.providerId).pipe(
|
||||||
this.providerService.get(this.providerId),
|
canAccessBilling(this.configService),
|
||||||
this.consolidatedBillingEnabled$,
|
map((canAccessBilling) => {
|
||||||
]).pipe(
|
if (!canAccessBilling) {
|
||||||
concatMap(([provider, consolidatedBillingEnabled]) => {
|
|
||||||
if (
|
|
||||||
!consolidatedBillingEnabled ||
|
|
||||||
provider.providerStatus !== ProviderStatusType.Billable
|
|
||||||
) {
|
|
||||||
return from(
|
return from(
|
||||||
this.router.navigate(["../clients"], {
|
this.router.navigate(["../clients"], {
|
||||||
relativeTo: this.activatedRoute,
|
relativeTo: this.activatedRoute,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.provider = provider;
|
|
||||||
this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin;
|
|
||||||
return from(this.load());
|
return from(this.load());
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -104,6 +93,10 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
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.clients = (await this.apiService.getProviderClients(this.providerId)).data;
|
||||||
|
|
||||||
this.dataSource.data = this.clients;
|
this.dataSource.data = this.clients;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./guards/has-consolidated-billing.guard";
|
||||||
|
export * from "./provider-subscription.component";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<app-header></app-header>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-provider-subscription",
|
||||||
|
templateUrl: "./provider-subscription.component.html",
|
||||||
|
})
|
||||||
|
export class ProviderSubscriptionComponent {}
|
||||||
@@ -117,13 +117,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
return super.logInTwoFactor(twoFactor);
|
return super.logInTwoFactor(twoFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||||
if (
|
if (
|
||||||
authRequestCredentials.decryptedMasterKey &&
|
authRequestCredentials.decryptedMasterKey &&
|
||||||
authRequestCredentials.decryptedMasterKeyHash
|
authRequestCredentials.decryptedMasterKeyHash
|
||||||
) {
|
) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(
|
await this.masterPasswordService.setMasterKey(
|
||||||
authRequestCredentials.decryptedMasterKey,
|
authRequestCredentials.decryptedMasterKey,
|
||||||
userId,
|
userId,
|
||||||
@@ -147,15 +146,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
if (authRequestCredentials.decryptedUserKey) {
|
if (authRequestCredentials.decryptedUserKey) {
|
||||||
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
||||||
} else {
|
} else {
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey(userId);
|
||||||
|
|
||||||
// Establish trust if required after setting user key
|
// Establish trust if required after setting user key
|
||||||
await this.deviceTrustService.trustDeviceIfRequired(userId);
|
await this.deviceTrustService.trustDeviceIfRequired(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ describe("LoginStrategy", () => {
|
|||||||
const result = await passwordLoginStrategy.logIn(credentials);
|
const result = await passwordLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
userId: userId,
|
||||||
forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
|
forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
|
||||||
resetMasterPassword: true,
|
resetMasterPassword: true,
|
||||||
twoFactorProviders: null,
|
twoFactorProviders: null,
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ export abstract class LoginStrategy {
|
|||||||
|
|
||||||
// Must come before setting keys, user key needs email to update additional keys
|
// Must come before setting keys, user key needs email to update additional keys
|
||||||
const userId = await this.saveAccountInformation(response);
|
const userId = await this.saveAccountInformation(response);
|
||||||
|
result.userId = userId;
|
||||||
|
|
||||||
if (response.twoFactorToken != null) {
|
if (response.twoFactorToken != null) {
|
||||||
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
// 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.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setMasterKey(response);
|
await this.setMasterKey(response, userId);
|
||||||
await this.setUserKey(response, userId);
|
await this.setUserKey(response, userId);
|
||||||
await this.setPrivateKey(response);
|
await this.setPrivateKey(response);
|
||||||
|
|
||||||
@@ -259,7 +260,7 @@ export abstract class LoginStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The keys comes from different sources depending on the login strategy
|
// 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 setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,11 @@ describe("PasswordLoginStrategy", () => {
|
|||||||
localHashedPassword,
|
localHashedPassword,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
tokenResponse.key,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
const [authResult, identityResponse] = await this.startLogIn();
|
const [authResult, identityResponse] = await this.startLogIn();
|
||||||
|
|
||||||
|
if (identityResponse instanceof IdentityCaptchaResponse) {
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
const masterPasswordPolicyOptions =
|
const masterPasswordPolicyOptions =
|
||||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||||
|
|
||||||
@@ -157,23 +161,23 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
credentials,
|
credentials,
|
||||||
masterPasswordPolicyOptions,
|
masterPasswordPolicyOptions,
|
||||||
);
|
);
|
||||||
|
if (meetsRequirements) {
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!meetsRequirements) {
|
if (identityResponse instanceof IdentityTwoFactorResponse) {
|
||||||
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
|
// Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope
|
||||||
// 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.next({
|
...this.cache.value,
|
||||||
...this.cache.value,
|
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
// Authentication was successful, save the force update password options with the state service
|
||||||
// Authentication was successful, save the force update password options with the state service
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
ForceSetPasswordReason.WeakMasterPassword,
|
||||||
await this.masterPasswordService.setForceSetPasswordReason(
|
authResult.userId, // userId is only available on successful login
|
||||||
ForceSetPasswordReason.WeakMasterPassword,
|
);
|
||||||
userId,
|
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
||||||
);
|
|
||||||
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return authResult;
|
return authResult;
|
||||||
@@ -196,17 +200,18 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
!result.requiresCaptcha &&
|
!result.requiresCaptcha &&
|
||||||
forcePasswordResetReason != ForceSetPasswordReason.None
|
forcePasswordResetReason != ForceSetPasswordReason.None
|
||||||
) {
|
) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId);
|
forcePasswordResetReason,
|
||||||
|
result.userId,
|
||||||
|
);
|
||||||
result.forcePasswordReset = forcePasswordResetReason;
|
result.forcePasswordReset = forcePasswordResetReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
const { masterKey, localMasterKeyHash } = this.cache.value;
|
const { masterKey, localMasterKeyHash } = this.cache.value;
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||||
}
|
}
|
||||||
@@ -219,12 +224,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
if (this.encryptionKeyMigrationRequired(response)) {
|
if (this.encryptionKeyMigrationRequired(response)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
|
||||||
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(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(
|
private getMasterPasswordPolicyOptionsFromResponse(
|
||||||
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse,
|
response: IdentityTokenResponse | IdentityTwoFactorResponse,
|
||||||
): MasterPasswordPolicyOptions {
|
): MasterPasswordPolicyOptions {
|
||||||
if (response == null || response instanceof IdentityCaptchaResponse) {
|
if (response == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
||||||
|
|||||||
@@ -163,7 +163,10 @@ describe("SsoLoginStrategy", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
|
tokenResponse.key,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Trusted Device Decryption", () => {
|
describe("Trusted Device Decryption", () => {
|
||||||
@@ -417,7 +420,7 @@ describe("SsoLoginStrategy", () => {
|
|||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
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 () => {
|
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(
|
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
ssoOrgId,
|
ssoOrgId,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -468,7 +472,7 @@ describe("SsoLoginStrategy", () => {
|
|||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
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 () => {
|
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(
|
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
ssoOrgId,
|
ssoOrgId,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.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 { 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 { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
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));
|
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
async logIn(credentials: SsoLoginCredentials) {
|
async logIn(credentials: SsoLoginCredentials): Promise<AuthResult> {
|
||||||
const data = new SsoLoginStrategyData();
|
const data = new SsoLoginStrategyData();
|
||||||
data.orgId = credentials.orgId;
|
data.orgId = credentials.orgId;
|
||||||
|
|
||||||
@@ -147,10 +148,9 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
// Auth guard currently handles redirects for this.
|
// Auth guard currently handles redirects for this.
|
||||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setForceSetPasswordReason(
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
ssoAuthResult.forcePasswordReset,
|
ssoAuthResult.forcePasswordReset,
|
||||||
userId,
|
ssoAuthResult.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
return ssoAuthResult;
|
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.
|
// 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.
|
// First, check to make sure that we should do so based on the token response.
|
||||||
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
|
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
|
||||||
@@ -175,10 +175,11 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
this.cache.value.orgId,
|
this.cache.value.orgId,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
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) {
|
if (masterKeyEncryptedUserKey) {
|
||||||
// set the master key encrypted user key if it exists
|
// 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;
|
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||||
@@ -251,7 +252,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
this.getKeyConnectorUrl(tokenResponse) != null
|
this.getKeyConnectorUrl(tokenResponse) != null
|
||||||
) {
|
) {
|
||||||
// Key connector enabled for user
|
// 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
|
// 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> {
|
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
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
|
// There is a scenario in which the master key is not set here. That will occur if the user
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
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 () => {
|
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
||||||
@@ -195,6 +195,6 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
await apiLogInStrategy.logIn(credentials);
|
await apiLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
const env = await firstValueFrom(this.environmentService.environment$);
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const keyConnectorUrl = env.getKeyConnectorUrl();
|
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);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||||
|
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(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> {
|
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
||||||
const userId = await super.saveAccountInformation(tokenResponse);
|
const userId = await super.saveAccountInformation(tokenResponse);
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,10 @@ describe("WebAuthnLoginStrategy", () => {
|
|||||||
// Assert
|
// Assert
|
||||||
// Master key encrypted user key should be set
|
// Master key encrypted user key should be set
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(cryptoService.decryptToBytes).toHaveBeenCalledWith(
|
expect(cryptoService.decryptToBytes).toHaveBeenCalledWith(
|
||||||
@@ -220,7 +223,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||||||
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
|
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
|
||||||
mockPrfPrivateKey,
|
mockPrfPrivateKey,
|
||||||
);
|
);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
|
||||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
|
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
|
||||||
|
|
||||||
// Master key and private key should not be set
|
// Master key and private key should not be set
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||||||
throw new Error("2FA not supported yet for WebAuthn Login.");
|
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();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
if (masterKeyEncryptedUserKey) {
|
if (masterKeyEncryptedUserKey) {
|
||||||
// set the master key encrypted user key if it exists
|
// 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;
|
const userDecryptionOptions = idTokenResponse?.userDecryptionOptions;
|
||||||
@@ -134,7 +134,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey);
|
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { ProviderData } from "../models/data/provider.data";
|
import { ProviderData } from "../models/data/provider.data";
|
||||||
import { Provider } from "../models/domain/provider";
|
import { Provider } from "../models/domain/provider";
|
||||||
|
|
||||||
export abstract class ProviderService {
|
export abstract class ProviderService {
|
||||||
|
get$: (id: string) => Observable<Provider>;
|
||||||
get: (id: string) => Promise<Provider>;
|
get: (id: string) => Promise<Provider>;
|
||||||
getAll: () => Promise<Provider[]>;
|
getAll: () => Promise<Provider[]>;
|
||||||
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;
|
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
|
|||||||
constructor(private key: string) {}
|
constructor(private key: string) {}
|
||||||
|
|
||||||
async decrypt(cryptoService: CryptoService) {
|
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;
|
return new SymmetricCryptoKey(decValue) as OrgKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
@@ -86,6 +88,7 @@ describe("ProviderService", () => {
|
|||||||
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
||||||
fakeUserState = fakeStateProvider.singleUser.getFake(fakeUserId, PROVIDERS);
|
fakeUserState = fakeStateProvider.singleUser.getFake(fakeUserId, PROVIDERS);
|
||||||
fakeActiveUserState = fakeStateProvider.activeUser.getFake(PROVIDERS);
|
fakeActiveUserState = fakeStateProvider.activeUser.getFake(PROVIDERS);
|
||||||
|
|
||||||
providerService = new ProviderService(fakeStateProvider);
|
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()", () => {
|
describe("get()", () => {
|
||||||
it("Returns a single provider from state that matches the specified id", async () => {
|
it("Returns a single provider from state that matches the specified id", async () => {
|
||||||
const mockData = buildMockProviders(5);
|
const mockData = buildMockProviders(5);
|
||||||
|
|||||||
@@ -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 { UserId } from "../../types/guid";
|
||||||
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
|
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
|
||||||
import { ProviderData } from "../models/data/provider.data";
|
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> {
|
async get(id: string): Promise<Provider> {
|
||||||
return await firstValueFrom(this.providers$().pipe(mapToSingleProvider(id)));
|
return await firstValueFrom(this.providers$().pipe(mapToSingleProvider(id)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Organization } from "../../admin-console/models/domain/organization";
|
import { Organization } from "../../admin-console/models/domain/organization";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||||
|
|
||||||
export abstract class KeyConnectorService {
|
export abstract class KeyConnectorService {
|
||||||
setMasterKeyFromUrl: (url?: string) => Promise<void>;
|
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
|
||||||
getManagingOrganization: () => Promise<Organization>;
|
getManagingOrganization: () => Promise<Organization>;
|
||||||
getUsesKeyConnector: () => Promise<boolean>;
|
getUsesKeyConnector: () => Promise<boolean>;
|
||||||
migrateUser: () => Promise<void>;
|
migrateUser: () => Promise<void>;
|
||||||
@@ -10,6 +11,7 @@ export abstract class KeyConnectorService {
|
|||||||
convertNewSsoUserToKeyConnector: (
|
convertNewSsoUserToKeyConnector: (
|
||||||
tokenResponse: IdentityTokenResponse,
|
tokenResponse: IdentityTokenResponse,
|
||||||
orgId: string,
|
orgId: string,
|
||||||
|
userId: UserId,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
||||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
||||||
|
|
||||||
import { ForceSetPasswordReason } from "./force-set-password-reason";
|
import { ForceSetPasswordReason } from "./force-set-password-reason";
|
||||||
|
|
||||||
export class AuthResult {
|
export class AuthResult {
|
||||||
|
userId: UserId;
|
||||||
captchaSiteKey = "";
|
captchaSiteKey = "";
|
||||||
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ describe("KeyConnectorService", () => {
|
|||||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await keyConnectorService.setMasterKeyFromUrl(url);
|
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||||
@@ -235,7 +235,7 @@ describe("KeyConnectorService", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Act
|
// Act
|
||||||
await keyConnectorService.setMasterKeyFromUrl(url);
|
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||||
} catch {
|
} catch {
|
||||||
// Assert
|
// Assert
|
||||||
expect(logService.error).toHaveBeenCalledWith(error);
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
StateProvider,
|
StateProvider,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { MasterKey } from "../../types/key";
|
import { MasterKey } from "../../types/key";
|
||||||
import { AccountService } from "../abstractions/account.service";
|
import { AccountService } from "../abstractions/account.service";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.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
|
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
||||||
async setMasterKeyFromUrl(url: string) {
|
async setMasterKeyFromUrl(url: string, userId: UserId) {
|
||||||
try {
|
try {
|
||||||
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
||||||
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
||||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.handleKeyConnectorError(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)
|
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
const {
|
const {
|
||||||
kdf,
|
kdf,
|
||||||
@@ -145,12 +149,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
kdfConfig,
|
kdfConfig,
|
||||||
);
|
);
|
||||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
|
|
||||||
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey[0]);
|
await this.cryptoService.setUserKey(userKey[0], userId);
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
|
||||||
|
|
||||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -311,15 +311,17 @@ export abstract class CryptoService {
|
|||||||
* @param data The data to encrypt
|
* @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
|
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
|
||||||
* @returns The encrypted data
|
* @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.
|
* Decrypts a value using RSA.
|
||||||
* @param encValue The encrypted value to decrypt
|
* @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
|
* @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>;
|
abstract randomNumber(min: number, max: number): Promise<number>;
|
||||||
/**
|
/**
|
||||||
* Generates a new cipher key
|
* Generates a new cipher key
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Observable } from "rxjs";
|
|
||||||
|
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||||
@@ -24,8 +22,6 @@ export type InitOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export abstract class StateService<T extends Account = Account> {
|
export abstract class StateService<T extends Account = Account> {
|
||||||
accounts$: Observable<{ [userId: string]: T }>;
|
|
||||||
|
|
||||||
addAccount: (account: T) => Promise<void>;
|
addAccount: (account: T) => Promise<void>;
|
||||||
clearDecryptedData: (userId: UserId) => Promise<void>;
|
clearDecryptedData: (userId: UserId) => Promise<void>;
|
||||||
clean: (options?: StorageOptions) => Promise<void>;
|
clean: (options?: StorageOptions) => Promise<void>;
|
||||||
|
|||||||
@@ -621,19 +621,20 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
|
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) {
|
if (publicKey == null) {
|
||||||
publicKey = await this.getPublicKey();
|
throw new Error("'publicKey' is a required parameter and must be non-null");
|
||||||
}
|
|
||||||
if (publicKey == null) {
|
|
||||||
throw new Error("Public key unavailable.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
|
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(".");
|
const headerPieces = encValue.split(".");
|
||||||
let encType: EncryptionType = null;
|
let encType: EncryptionType = null;
|
||||||
let encPieces: string[];
|
let encPieces: string[];
|
||||||
@@ -665,10 +666,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = Utils.fromB64ToArray(encPieces[0]);
|
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";
|
let alg: "sha1" | "sha256" = "sha1";
|
||||||
switch (encType) {
|
switch (encType) {
|
||||||
@@ -771,6 +768,13 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: EncString;
|
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 userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
|
||||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||||
await this.setUserKey(userKey);
|
await this.setUserKey(userKey);
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ describe("derived decrypted org keys", () => {
|
|||||||
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
|
"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
|
// 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-1"].key);
|
||||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
|
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
|
import { firstValueFrom, map } from "rxjs";
|
||||||
import { Jsonify, JsonValue } from "type-fest";
|
import { Jsonify, JsonValue } from "type-fest";
|
||||||
|
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
@@ -52,9 +52,6 @@ export class StateService<
|
|||||||
TAccount extends Account = Account,
|
TAccount extends Account = Account,
|
||||||
> implements StateServiceAbstraction<TAccount>
|
> implements StateServiceAbstraction<TAccount>
|
||||||
{
|
{
|
||||||
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
|
||||||
accounts$ = this.accountsSubject.asObservable();
|
|
||||||
|
|
||||||
private hasBeenInited = false;
|
private hasBeenInited = false;
|
||||||
protected isRecoveredSession = false;
|
protected isRecoveredSession = false;
|
||||||
|
|
||||||
@@ -115,8 +112,6 @@ export class StateService<
|
|||||||
state = await this.syncAccountFromDisk(authenticatedAccounts[i]);
|
state = await this.syncAccountFromDisk(authenticatedAccounts[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.pushAccounts();
|
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -153,7 +148,6 @@ export class StateService<
|
|||||||
|
|
||||||
await this.removeAccountFromDisk(options?.userId);
|
await this.removeAccountFromDisk(options?.userId);
|
||||||
await this.removeAccountFromMemory(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> {
|
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(
|
protected reconcileOptions(
|
||||||
requestedOptions: StorageOptions,
|
requestedOptions: StorageOptions,
|
||||||
defaultOptions: StorageOptions,
|
defaultOptions: StorageOptions,
|
||||||
@@ -1096,8 +1078,6 @@ export class StateService<
|
|||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.pushAccounts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
||||||
|
|||||||
Reference in New Issue
Block a user