From 021d275c437f4f4ee689dfcd5695c69a62bca08c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 30 Jul 2025 23:49:50 +0200 Subject: [PATCH 1/8] [PM-24079] Switch `EncryptedString` to SDK type (#15796) * Update usages of sdk to type-safe SDK type * Update sdk version * Update to "toSdk" --- .../crypto/models/enc-string.ts | 26 +++++++++----- .../services/sdk/default-sdk.service.ts | 3 +- .../src/vault/models/domain/attachment.ts | 4 +-- libs/common/src/vault/models/domain/card.ts | 12 +++---- .../src/vault/models/domain/cipher.spec.ts | 35 +++++++++--------- libs/common/src/vault/models/domain/cipher.ts | 6 ++-- .../vault/models/domain/fido2-credential.ts | 24 ++++++------- libs/common/src/vault/models/domain/field.ts | 4 +-- .../src/vault/models/domain/identity.ts | 36 +++++++++---------- .../src/vault/models/domain/login-uri.ts | 4 +-- libs/common/src/vault/models/domain/login.ts | 6 ++-- .../src/vault/models/domain/password.ts | 2 +- .../common/src/vault/models/domain/ssh-key.ts | 6 ++-- .../src/vault/models/view/attachment.view.ts | 2 +- .../src/vault/models/view/cipher.view.ts | 2 +- ...symmetric-key-regeneration.service.spec.ts | 4 +-- package-lock.json | 8 ++--- package.json | 2 +- 18 files changed, 99 insertions(+), 87 deletions(-) diff --git a/libs/common/src/key-management/crypto/models/enc-string.ts b/libs/common/src/key-management/crypto/models/enc-string.ts index f215398d92a..b979698dfb0 100644 --- a/libs/common/src/key-management/crypto/models/enc-string.ts +++ b/libs/common/src/key-management/crypto/models/enc-string.ts @@ -1,6 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Jsonify, Opaque } from "type-fest"; +import { Jsonify } from "type-fest"; + +import { EncString as SdkEncString } from "@bitwarden/sdk-internal"; import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../platform/enums"; import { Encrypted } from "../../../platform/interfaces/encrypted"; @@ -10,7 +12,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr export const DECRYPT_ERROR = "[error: cannot decrypt]"; export class EncString implements Encrypted { - encryptedString?: EncryptedString; + encryptedString?: SdkEncString; encryptionType?: EncryptionType; decryptedValue?: string; data?: string; @@ -42,7 +44,11 @@ export class EncString implements Encrypted { return this.data == null ? null : Utils.fromB64ToArray(this.data); } - toJSON() { + toSdk(): SdkEncString { + return this.encryptedString; + } + + toJSON(): string { return this.encryptedString as string; } @@ -56,14 +62,14 @@ export class EncString implements Encrypted { private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) { if (iv != null) { - this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString; + this.encryptedString = (encType + "." + iv + "|" + data) as SdkEncString; } else { - this.encryptedString = (encType + "." + data) as EncryptedString; + this.encryptedString = (encType + "." + data) as SdkEncString; } // mac if (mac != null) { - this.encryptedString = (this.encryptedString + "|" + mac) as EncryptedString; + this.encryptedString = (this.encryptedString + "|" + mac) as SdkEncString; } this.encryptionType = encType; @@ -73,7 +79,7 @@ export class EncString implements Encrypted { } private initFromEncryptedString(encryptedString: string) { - this.encryptedString = encryptedString as EncryptedString; + this.encryptedString = encryptedString as SdkEncString; if (!this.encryptedString) { return; } @@ -191,4 +197,8 @@ export class EncString implements Encrypted { } } -export type EncryptedString = Opaque; +/** + * Temporary type mapping until consumers are moved over. + * @deprecated - Use SdkEncString directly + */ +export type EncryptedString = SdkEncString; diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index c12fbe2dbb2..d8780b0f1f4 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -22,6 +22,7 @@ import { ClientSettings, DeviceType as SdkDeviceType, TokenProvider, + UnsignedSharedKey, } from "@bitwarden/sdk-internal"; import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; @@ -237,7 +238,7 @@ export class DefaultSdkService implements SdkService { organizationKeys: new Map( Object.entries(orgKeys ?? {}) .filter(([_, v]) => v.type === "organization") - .map(([k, v]) => [k, v.key]), + .map(([k, v]) => [k, v.key as UnsignedSharedKey]), ), }); } diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 638f354c4b8..ec32e28d85d 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -128,8 +128,8 @@ export class Attachment extends Domain { url: this.url, size: this.size, sizeName: this.sizeName, - fileName: this.fileName?.toJSON(), - key: this.key?.toJSON(), + fileName: this.fileName?.toSdk(), + key: this.key?.toSdk(), }; } diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 688053ae93c..89cc361b454 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -95,12 +95,12 @@ export class Card extends Domain { */ toSdkCard(): SdkCard { return { - cardholderName: this.cardholderName?.toJSON(), - brand: this.brand?.toJSON(), - number: this.number?.toJSON(), - expMonth: this.expMonth?.toJSON(), - expYear: this.expYear?.toJSON(), - code: this.code?.toJSON(), + cardholderName: this.cardholderName?.toSdk(), + brand: this.brand?.toSdk(), + number: this.number?.toSdk(), + expMonth: this.expMonth?.toSdk(), + expYear: this.expYear?.toSdk(), + code: this.code?.toSdk(), }; } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 60fff8b510e..008324f9aec 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -11,6 +11,7 @@ import { CipherRepromptType as SdkCipherRepromptType, LoginLinkedIdType, Cipher as SdkCipher, + EncString as SdkEncString, } from "@bitwarden/sdk-internal"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; @@ -1010,22 +1011,22 @@ describe("Cipher DTO", () => { organizationId: "orgId", folderId: "folderId", collectionIds: [], - key: "EncryptedString", - name: "EncryptedString", - notes: "EncryptedString", + key: "EncryptedString" as SdkEncString, + name: "EncryptedString" as SdkEncString, + notes: "EncryptedString" as SdkEncString, type: SdkCipherType.Login, login: { - username: "EncryptedString", - password: "EncryptedString", + username: "EncryptedString" as SdkEncString, + password: "EncryptedString" as SdkEncString, passwordRevisionDate: "2022-01-31T12:00:00.000Z", uris: [ { - uri: "EncryptedString", - uriChecksum: "EncryptedString", + uri: "EncryptedString" as SdkEncString, + uriChecksum: "EncryptedString" as SdkEncString, match: UriMatchType.Domain, }, ], - totp: "EncryptedString", + totp: "EncryptedString" as SdkEncString, autofillOnPageLoad: false, fido2Credentials: undefined, }, @@ -1049,35 +1050,35 @@ describe("Cipher DTO", () => { url: "url", size: "1100", sizeName: "1.1 KB", - fileName: "file", - key: "EncKey", + fileName: "file" as SdkEncString, + key: "EncKey" as SdkEncString, }, { id: "a2", url: "url", size: "1100", sizeName: "1.1 KB", - fileName: "file", - key: "EncKey", + fileName: "file" as SdkEncString, + key: "EncKey" as SdkEncString, }, ], fields: [ { - name: "EncryptedString", - value: "EncryptedString", + name: "EncryptedString" as SdkEncString, + value: "EncryptedString" as SdkEncString, type: FieldType.Linked, linkedId: LoginLinkedIdType.Username, }, { - name: "EncryptedString", - value: "EncryptedString", + name: "EncryptedString" as SdkEncString, + value: "EncryptedString" as SdkEncString, type: FieldType.Linked, linkedId: LoginLinkedIdType.Password, }, ], passwordHistory: [ { - password: "EncryptedString", + password: "EncryptedString" as SdkEncString, lastUsedDate: "2022-01-31T12:00:00.000Z", }, ], diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 2a13cb06d71..f884dc32cce 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -348,9 +348,9 @@ export class Cipher extends Domain implements Decryptable { organizationId: this.organizationId ?? undefined, folderId: this.folderId ?? undefined, collectionIds: this.collectionIds ?? [], - key: this.key?.toJSON(), - name: this.name.toJSON(), - notes: this.notes?.toJSON(), + key: this.key?.toSdk(), + name: this.name.toSdk(), + notes: this.notes?.toSdk(), type: this.type, favorite: this.favorite ?? false, organizationUseTotp: this.organizationUseTotp ?? false, diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index 5dbf55b44fc..a74afc2336d 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -158,18 +158,18 @@ export class Fido2Credential extends Domain { */ toSdkFido2Credential(): SdkFido2Credential { return { - credentialId: this.credentialId?.toJSON(), - keyType: this.keyType.toJSON(), - keyAlgorithm: this.keyAlgorithm.toJSON(), - keyCurve: this.keyCurve.toJSON(), - keyValue: this.keyValue.toJSON(), - rpId: this.rpId.toJSON(), - userHandle: this.userHandle?.toJSON(), - userName: this.userName?.toJSON(), - counter: this.counter.toJSON(), - rpName: this.rpName?.toJSON(), - userDisplayName: this.userDisplayName?.toJSON(), - discoverable: this.discoverable?.toJSON(), + credentialId: this.credentialId?.toSdk(), + keyType: this.keyType.toSdk(), + keyAlgorithm: this.keyAlgorithm.toSdk(), + keyCurve: this.keyCurve.toSdk(), + keyValue: this.keyValue.toSdk(), + rpId: this.rpId.toSdk(), + userHandle: this.userHandle?.toSdk(), + userName: this.userName?.toSdk(), + counter: this.counter.toSdk(), + rpName: this.rpName?.toSdk(), + userDisplayName: this.userDisplayName?.toSdk(), + discoverable: this.discoverable?.toSdk(), creationDate: this.creationDate.toISOString(), }; } diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index 53756e21046..f652a2820d4 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -83,8 +83,8 @@ export class Field extends Domain { */ toSdkField(): SdkField { return { - name: this.name?.toJSON(), - value: this.value?.toJSON(), + name: this.name?.toSdk(), + value: this.value?.toSdk(), type: this.type, // Safe type cast: client and SDK LinkedIdType enums have identical values linkedId: this.linkedId as unknown as SdkLinkedIdType, diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index 16e68c72551..f0d5b3123ab 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -175,24 +175,24 @@ export class Identity extends Domain { */ toSdkIdentity(): SdkIdentity { return { - title: this.title?.toJSON(), - firstName: this.firstName?.toJSON(), - middleName: this.middleName?.toJSON(), - lastName: this.lastName?.toJSON(), - address1: this.address1?.toJSON(), - address2: this.address2?.toJSON(), - address3: this.address3?.toJSON(), - city: this.city?.toJSON(), - state: this.state?.toJSON(), - postalCode: this.postalCode?.toJSON(), - country: this.country?.toJSON(), - company: this.company?.toJSON(), - email: this.email?.toJSON(), - phone: this.phone?.toJSON(), - ssn: this.ssn?.toJSON(), - username: this.username?.toJSON(), - passportNumber: this.passportNumber?.toJSON(), - licenseNumber: this.licenseNumber?.toJSON(), + title: this.title?.toSdk(), + firstName: this.firstName?.toSdk(), + middleName: this.middleName?.toSdk(), + lastName: this.lastName?.toSdk(), + address1: this.address1?.toSdk(), + address2: this.address2?.toSdk(), + address3: this.address3?.toSdk(), + city: this.city?.toSdk(), + state: this.state?.toSdk(), + postalCode: this.postalCode?.toSdk(), + country: this.country?.toSdk(), + company: this.company?.toSdk(), + email: this.email?.toSdk(), + phone: this.phone?.toSdk(), + ssn: this.ssn?.toSdk(), + username: this.username?.toSdk(), + passportNumber: this.passportNumber?.toSdk(), + licenseNumber: this.licenseNumber?.toSdk(), }; } diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 9cfa4951dd8..973e25c8ff1 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -97,8 +97,8 @@ export class LoginUri extends Domain { */ toSdkLoginUri(): SdkLoginUri { return { - uri: this.uri?.toJSON(), - uriChecksum: this.uriChecksum?.toJSON(), + uri: this.uri?.toSdk(), + uriChecksum: this.uriChecksum?.toSdk(), match: this.match, }; } diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index 93af2269185..723478b10a8 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -155,10 +155,10 @@ export class Login extends Domain { toSdkLogin(): SdkLogin { return { uris: this.uris?.map((u) => u.toSdkLoginUri()), - username: this.username?.toJSON(), - password: this.password?.toJSON(), + username: this.username?.toSdk(), + password: this.password?.toSdk(), passwordRevisionDate: this.passwordRevisionDate?.toISOString(), - totp: this.totp?.toJSON(), + totp: this.totp?.toSdk(), autofillOnPageLoad: this.autofillOnPageLoad ?? undefined, fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()), }; diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index b8a30099454..ca594075e0b 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -67,7 +67,7 @@ export class Password extends Domain { */ toSdkPasswordHistory(): PasswordHistory { return { - password: this.password.toJSON(), + password: this.password.toSdk(), lastUsedDate: this.lastUsedDate.toISOString(), }; } diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index 0c8abf76e44..ab1685955a3 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -80,9 +80,9 @@ export class SshKey extends Domain { */ toSdkSshKey(): SdkSshKey { return { - privateKey: this.privateKey.toJSON(), - publicKey: this.publicKey.toJSON(), - fingerprint: this.keyFingerprint.toJSON(), + privateKey: this.privateKey.toSdk(), + publicKey: this.publicKey.toSdk(), + fingerprint: this.keyFingerprint.toSdk(), }; } diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 4c1033efd08..1c796c8f275 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -69,7 +69,7 @@ export class AttachmentView implements View { size: this.size, sizeName: this.sizeName, fileName: this.fileName, - key: this.encryptedKey?.toJSON(), + key: this.encryptedKey?.toSdk(), // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete decryptedKey: this.key ? this.key.toBase64() : null, }; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 0c41e49c3ab..c91d6e21ca2 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -331,7 +331,7 @@ export class CipherView implements View, InitializerMetadata { creationDate: (this.creationDate ?? new Date()).toISOString(), deletedDate: this.deletedDate?.toISOString(), reprompt: this.reprompt ?? CipherRepromptType.None, - key: this.key?.toJSON(), + key: this.key?.toSdk(), // Cipher type specific properties are set in the switch statement below // CipherView initializes each with default constructors (undefined values) // The SDK does not expect those undefined values and will throw exceptions diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts index b23d6e8f3bd..e57ab74de6b 100644 --- a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts @@ -15,7 +15,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal"; +import { VerifyAsymmetricKeysResponse, EncString as SdkEncString } from "@bitwarden/sdk-internal"; import { KeyService } from "../../abstractions/key.service"; import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service"; @@ -28,7 +28,7 @@ function setupVerificationResponse( ) { const mockKeyPairResponse = { userPublicKey: "userPublicKey", - userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey", + userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey" as SdkEncString, }; sdkService.client.crypto diff --git a/package-lock.json b/package-lock.json index de71ab1ab4f..1d7226786b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.237", + "@bitwarden/sdk-internal": "0.2.0-main.239", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4622,9 +4622,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.237", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.237.tgz", - "integrity": "sha512-1psCagsmUo2QeIw/xFW/OCfSInl6Gu+LYldbdLuv1z26FurrgmAv8BejDaPRx006BRn0z0hn6TlZtteaZS762w==", + "version": "0.2.0-main.239", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.239.tgz", + "integrity": "sha512-nX1aaoXGWdZVJKLnlewxw3kibgIjukVj/1JD0Dl1OevAVM694MnD/LQTRbyme8rvUxnUhxFjQi6NpwbUJdJPCA==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index b40ccd0a68b..6b078861f75 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.237", + "@bitwarden/sdk-internal": "0.2.0-main.239", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From 9c8188875a93d25f7493e32b07c63a39b32d9225 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 31 Jul 2025 08:00:07 -0500 Subject: [PATCH 2/8] Updates the internal sdk to the latest version. (#15860) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d7226786b3..fe5e5e51388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.239", + "@bitwarden/sdk-internal": "0.2.0-main.242", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4622,9 +4622,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.239", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.239.tgz", - "integrity": "sha512-nX1aaoXGWdZVJKLnlewxw3kibgIjukVj/1JD0Dl1OevAVM694MnD/LQTRbyme8rvUxnUhxFjQi6NpwbUJdJPCA==", + "version": "0.2.0-main.242", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.242.tgz", + "integrity": "sha512-LFPNAAq9ORVGdvcB3PBhlM3GQZUMf3MhIuYbZxmhAG5SVlvem+sbaolgK3Fnf/8ajVx1IDMNEhfgQkA4mU9uAg==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 6b078861f75..c62b651027b 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.239", + "@bitwarden/sdk-internal": "0.2.0-main.242", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From 4f9b2b618f031221536c14e28528e4d8ff44afc1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:09:14 -0400 Subject: [PATCH 3/8] [PM-24280] Remove account service from state (#15828) * Introduce ActiveUserAccessor * Use ActiveUserAccessor over AccountService * Updates tests and testing utils to support ActiveUserAccessor * Update all injection points * Fix types test * Use ternary instead --- .../browser/src/background/main.background.ts | 3 +- .../service-container/service-container.ts | 3 +- apps/desktop/src/main.ts | 3 +- .../src/services/jslib-services.module.ts | 9 ++- libs/common/spec/fake-state-provider.ts | 55 +++++++++++++++---- libs/common/spec/fake-state.ts | 16 ++---- .../services/default-active-user.accessor.ts | 19 +++++++ .../platform/state/active-user.accessor.ts | 11 ++++ ...default-active-user-state.provider.spec.ts | 37 ------------- .../default-active-user-state.provider.ts | 9 ++- .../default-active-user-state.spec.ts | 18 ++---- .../default-state.provider.spec.ts | 34 ++++-------- .../specific-state.provider.spec.ts | 4 +- libs/common/src/platform/state/index.ts | 1 + 14 files changed, 118 insertions(+), 104 deletions(-) create mode 100644 libs/common/src/auth/services/default-active-user.accessor.ts create mode 100644 libs/common/src/platform/state/active-user.accessor.ts delete mode 100644 libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 00de1873462..7e2d31ba43d 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -43,6 +43,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; +import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; @@ -591,7 +592,7 @@ export default class MainBackground { this.singleUserStateProvider, ); this.activeUserStateProvider = new DefaultActiveUserStateProvider( - this.accountService, + new DefaultActiveUserAccessor(this.accountService), this.singleUserStateProvider, ); this.derivedStateProvider = new InlineDerivedStateProvider(); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 3ea471dac5a..a5237453f1a 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -44,6 +44,7 @@ import { } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; +import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -377,7 +378,7 @@ export class ServiceContainer { ); this.activeUserStateProvider = new DefaultActiveUserStateProvider( - this.accountService, + new DefaultActiveUserAccessor(this.accountService), this.singleUserStateProvider, ); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 9b5aa0b31e9..67fbf457a77 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -9,6 +9,7 @@ import { Subject, firstValueFrom } from "rxjs"; import { SsoUrlService } from "@bitwarden/auth/common"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; +import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor"; import { ClientType } from "@bitwarden/common/enums"; import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -170,7 +171,7 @@ export class Main { ); const activeUserStateProvider = new DefaultActiveUserStateProvider( - accountService, + new DefaultActiveUserAccessor(accountService), singleUserStateProvider, ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index cdc16ba1299..113a8ab526f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -110,6 +110,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; +import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation"; @@ -232,6 +233,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { + ActiveUserAccessor, ActiveUserStateProvider, DerivedStateProvider, GlobalStateProvider, @@ -1271,10 +1273,15 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultGlobalStateProvider, deps: [StorageServiceProvider, LogService], }), + safeProvider({ + provide: ActiveUserAccessor, + useClass: DefaultActiveUserAccessor, + deps: [AccountServiceAbstraction], + }), safeProvider({ provide: ActiveUserStateProvider, useClass: DefaultActiveUserStateProvider, - deps: [AccountServiceAbstraction, SingleUserStateProvider], + deps: [ActiveUserAccessor, SingleUserStateProvider], }), safeProvider({ provide: SingleUserStateProvider, diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index 9f72ccada55..d6660402056 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { mock } from "jest-mock-extended"; -import { Observable, map, of, switchMap, take } from "rxjs"; +import { BehaviorSubject, map, Observable, of, switchMap, take } from "rxjs"; import { GlobalState, @@ -16,11 +16,11 @@ import { DeriveDefinition, DerivedStateProvider, UserKeyDefinition, + ActiveUserAccessor, } from "../src/platform/state"; import { UserId } from "../src/types/guid"; import { DerivedStateDependencies } from "../src/types/state"; -import { FakeAccountService } from "./fake-account-service"; import { FakeActiveUserState, FakeDerivedState, @@ -28,6 +28,35 @@ import { FakeSingleUserState, } from "./fake-state"; +export interface MinimalAccountService { + activeUserId: UserId | null; + activeAccount$: Observable<{ id: UserId } | null>; +} + +export class FakeActiveUserAccessor implements MinimalAccountService, ActiveUserAccessor { + private _subject: BehaviorSubject; + + constructor(startingUser: UserId | null) { + this._subject = new BehaviorSubject(startingUser); + this.activeAccount$ = this._subject + .asObservable() + .pipe(map((id) => (id != null ? { id } : null))); + this.activeUserId$ = this._subject.asObservable(); + } + + get activeUserId(): UserId { + return this._subject.value; + } + + activeUserId$: Observable; + + activeAccount$: Observable<{ id: UserId }>; + + switch(user: UserId | null) { + this._subject.next(user); + } +} + export class FakeGlobalStateProvider implements GlobalStateProvider { mock = mock(); establishedMocks: Map> = new Map(); @@ -138,18 +167,18 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider { } export class FakeActiveUserStateProvider implements ActiveUserStateProvider { - activeUserId$: Observable; + activeUserId$: Observable; states: Map> = new Map(); constructor( - public accountService: FakeAccountService, + public accountServiceAccessor: MinimalAccountService, readonly updateSyncCallback?: ( key: UserKeyDefinition, userId: UserId, newValue: unknown, ) => Promise, ) { - this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id)); + this.activeUserId$ = accountServiceAccessor.activeAccount$.pipe(map((a) => a?.id)); } get(userKeyDefinition: UserKeyDefinition): ActiveUserState { @@ -182,9 +211,13 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider { } private buildFakeState(userKeyDefinition: UserKeyDefinition, initialValue?: T) { - const state = new FakeActiveUserState(this.accountService, initialValue, async (...args) => { - await this.updateSyncCallback?.(userKeyDefinition, ...args); - }); + const state = new FakeActiveUserState( + this.accountServiceAccessor, + initialValue, + async (...args) => { + await this.updateSyncCallback?.(userKeyDefinition, ...args); + }, + ); state.keyDefinition = userKeyDefinition; return state; } @@ -256,14 +289,14 @@ export class FakeStateProvider implements StateProvider { return this.derived.get(parentState$, deriveDefinition, dependencies); } - constructor(public accountService: FakeAccountService) {} + constructor(private activeAccountAccessor: MinimalAccountService) {} private distributeSingleUserUpdate( key: UserKeyDefinition, userId: UserId, newState: unknown, ) { - if (this.activeUser.accountService.activeUserId === userId) { + if (this.activeUser.accountServiceAccessor.activeUserId === userId) { const state = this.activeUser.getFake(key, { allowInit: false }); state?.nextState(newState, { syncValue: false }); } @@ -284,7 +317,7 @@ export class FakeStateProvider implements StateProvider { this.distributeSingleUserUpdate.bind(this), ); activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider( - this.accountService, + this.activeAccountAccessor, this.distributeActiveUserUpdate.bind(this), ); derived: FakeDerivedStateProvider = new FakeDerivedStateProvider(); diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts index d4fbd108475..38019a38682 100644 --- a/libs/common/spec/fake-state.ts +++ b/libs/common/spec/fake-state.ts @@ -18,7 +18,7 @@ import { CombinedState, activeMarker } from "../src/platform/state/user-state"; import { UserId } from "../src/types/guid"; import { DerivedStateDependencies } from "../src/types/state"; -import { FakeAccountService } from "./fake-account-service"; +import { MinimalAccountService } from "./fake-state-provider"; const DEFAULT_TEST_OPTIONS: StateUpdateOptions = { shouldUpdate: () => true, @@ -177,7 +177,7 @@ export class FakeActiveUserState implements ActiveUserState { combinedState$: Observable>; constructor( - private accountService: FakeAccountService, + private activeAccountAccessor: MinimalAccountService, initialValue?: T, updateSyncCallback?: (userId: UserId, newValue: T) => Promise, ) { @@ -194,14 +194,10 @@ export class FakeActiveUserState implements ActiveUserState { this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state)); } - get userId() { - return this.accountService.activeUserId; - } - nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) { this.stateSubject.next({ syncValue, - combinedState: [this.userId, state], + combinedState: [this.activeAccountAccessor.activeUserId, state], }); } @@ -216,12 +212,12 @@ export class FakeActiveUserState implements ActiveUserState { ? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout))) : null; if (!options.shouldUpdate(current, combinedDependencies)) { - return [this.userId, current]; + return [this.activeAccountAccessor.activeUserId, current]; } const newState = configureState(current, combinedDependencies); this.nextState(newState); - this.nextMock([this.userId, newState]); - return [this.userId, newState]; + this.nextMock([this.activeAccountAccessor.activeUserId, newState]); + return [this.activeAccountAccessor.activeUserId, newState]; } /** Tracks update values resolved by `FakeState.update` */ diff --git a/libs/common/src/auth/services/default-active-user.accessor.ts b/libs/common/src/auth/services/default-active-user.accessor.ts new file mode 100644 index 00000000000..05775df9457 --- /dev/null +++ b/libs/common/src/auth/services/default-active-user.accessor.ts @@ -0,0 +1,19 @@ +import { map, Observable } from "rxjs"; + +import { UserId } from "@bitwarden/user-core"; + +import { ActiveUserAccessor } from "../../platform/state"; +import { AccountService } from "../abstractions/account.service"; + +/** + * Implementation for Platform so they can avoid a direct dependency on AccountService. Not for general consumption. + */ +export class DefaultActiveUserAccessor implements ActiveUserAccessor { + constructor(private readonly accountService: AccountService) { + this.activeUserId$ = this.accountService.activeAccount$.pipe( + map((a) => (a != null ? a.id : null)), + ); + } + + activeUserId$: Observable; +} diff --git a/libs/common/src/platform/state/active-user.accessor.ts b/libs/common/src/platform/state/active-user.accessor.ts new file mode 100644 index 00000000000..8ee2d53a93f --- /dev/null +++ b/libs/common/src/platform/state/active-user.accessor.ts @@ -0,0 +1,11 @@ +import { Observable } from "rxjs"; + +import { UserId } from "@bitwarden/user-core"; + +export abstract class ActiveUserAccessor { + /** + * Returns a stream of the current active user for the application. The stream either emits the user id for that account + * or returns null if there is no current active user. + */ + abstract activeUserId$: Observable; +} diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts deleted file mode 100644 index 681963f8233..00000000000 --- a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { mockAccountServiceWith, trackEmissions } from "../../../../spec"; -import { UserId } from "../../../types/guid"; -import { SingleUserStateProvider } from "../user-state.provider"; - -import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider"; - -describe("DefaultActiveUserStateProvider", () => { - const singleUserStateProvider = mock(); - const userId = "userId" as UserId; - const accountInfo = { - id: userId, - name: "name", - email: "email", - emailVerified: false, - }; - const accountService = mockAccountServiceWith(userId, accountInfo); - let sut: DefaultActiveUserStateProvider; - - beforeEach(() => { - sut = new DefaultActiveUserStateProvider(accountService, singleUserStateProvider); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should track the active User id from account service", () => { - const emissions = trackEmissions(sut.activeUserId$); - - accountService.activeAccountSubject.next(undefined); - accountService.activeAccountSubject.next(accountInfo); - - expect(emissions).toEqual([userId, undefined, userId]); - }); -}); diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts index 91e6f37c418..d24d2f8df72 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, distinctUntilChanged, map } from "rxjs"; +import { Observable, distinctUntilChanged } from "rxjs"; -import { AccountService } from "../../../auth/abstractions/account.service"; import { UserId } from "../../../types/guid"; +import { ActiveUserAccessor } from "../active-user.accessor"; import { UserKeyDefinition } from "../user-key-definition"; import { ActiveUserState } from "../user-state"; import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider"; @@ -14,11 +14,10 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { activeUserId$: Observable; constructor( - private readonly accountService: AccountService, + private readonly activeAccountAccessor: ActiveUserAccessor, private readonly singleUserStateProvider: SingleUserStateProvider, ) { - this.activeUserId$ = this.accountService.activeAccount$.pipe( - map((account) => account?.id), + this.activeUserId$ = this.activeAccountAccessor.activeUserId$.pipe( // To avoid going to storage when we don't need to, only get updates when there is a true change. distinctUntilChanged((a, b) => (a == null || b == null ? a == b : a === b)), // Treat null and undefined as equal ); diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts index 1cb1453a509..f6673e66c1d 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts @@ -3,14 +3,13 @@ * @jest-environment ../shared/test.environment.ts */ import { any, mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, map, of, timeout } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs"; import { Jsonify } from "type-fest"; import { StorageServiceProvider } from "@bitwarden/storage-core"; import { awaitAsync, trackEmissions } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; -import { Account } from "../../../auth/abstractions/account.service"; import { UserId } from "../../../types/guid"; import { LogService } from "../../abstractions/log.service"; import { StateDefinition } from "../state-definition"; @@ -48,7 +47,7 @@ describe("DefaultActiveUserState", () => { const storageServiceProvider = mock(); const stateEventRegistrarService = mock(); const logService = mock(); - let activeAccountSubject: BehaviorSubject; + let activeAccountSubject: BehaviorSubject; let singleUserStateProvider: DefaultSingleUserStateProvider; @@ -64,11 +63,11 @@ describe("DefaultActiveUserState", () => { logService, ); - activeAccountSubject = new BehaviorSubject(null); + activeAccountSubject = new BehaviorSubject(null); userState = new DefaultActiveUserState( testKeyDefinition, - activeAccountSubject.asObservable().pipe(map((a) => a?.id)), + activeAccountSubject.asObservable(), singleUserStateProvider, ); }); @@ -83,12 +82,7 @@ describe("DefaultActiveUserState", () => { const changeActiveUser = async (id: string) => { const userId = makeUserId(id); - activeAccountSubject.next({ - id: userId, - email: `test${id}@example.com`, - emailVerified: false, - name: `Test User ${id}`, - }); + activeAccountSubject.next(userId); await awaitAsync(); }; @@ -588,7 +582,7 @@ describe("DefaultActiveUserState", () => { }); it("does not await updates if the active user changes", async () => { - const initialUserId = (await firstValueFrom(activeAccountSubject)).id; + const initialUserId = activeAccountSubject.value; expect(initialUserId).toBe(userId); trackEmissions(userState.state$); await awaitAsync(); // storage updates are behind a promise diff --git a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts index b3190bd532e..442cec66d90 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts @@ -4,16 +4,16 @@ */ import { Observable, of } from "rxjs"; +import { UserId } from "@bitwarden/user-core"; + import { awaitAsync, trackEmissions } from "../../../../spec"; -import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; import { + FakeActiveUserAccessor, FakeActiveUserStateProvider, FakeDerivedStateProvider, FakeGlobalStateProvider, FakeSingleUserStateProvider, } from "../../../../spec/fake-state-provider"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { UserId } from "../../../types/guid"; import { DeriveDefinition } from "../derive-definition"; import { KeyDefinition } from "../key-definition"; import { StateDefinition } from "../state-definition"; @@ -27,12 +27,12 @@ describe("DefaultStateProvider", () => { let singleUserStateProvider: FakeSingleUserStateProvider; let globalStateProvider: FakeGlobalStateProvider; let derivedStateProvider: FakeDerivedStateProvider; - let accountService: FakeAccountService; + let activeAccountAccessor: FakeActiveUserAccessor; const userId = "fakeUserId" as UserId; beforeEach(() => { - accountService = mockAccountServiceWith(userId); - activeUserStateProvider = new FakeActiveUserStateProvider(accountService); + activeAccountAccessor = new FakeActiveUserAccessor(userId); + activeUserStateProvider = new FakeActiveUserStateProvider(activeAccountAccessor); singleUserStateProvider = new FakeSingleUserStateProvider(); globalStateProvider = new FakeGlobalStateProvider(); derivedStateProvider = new FakeDerivedStateProvider(); @@ -70,12 +70,6 @@ describe("DefaultStateProvider", () => { userId?: UserId, ) => Observable, ) => { - const accountInfo = { - email: "email", - emailVerified: false, - name: "name", - status: AuthenticationStatus.LoggedOut, - }; const keyDefinition = new UserKeyDefinition( new StateDefinition("test", "disk"), "test", @@ -97,7 +91,7 @@ describe("DefaultStateProvider", () => { }); it("should follow the current active user if no userId is provided", async () => { - accountService.activeAccountSubject.next({ id: userId, ...accountInfo }); + activeAccountAccessor.switch(userId); const state = singleUserStateProvider.getFake(userId, keyDefinition); state.nextState("value"); const emissions = trackEmissions(methodUnderTest(keyDefinition)); @@ -113,7 +107,7 @@ describe("DefaultStateProvider", () => { state.nextState("value"); const emissions = trackEmissions(methodUnderTest(keyDefinition)); - accountService.activeAccountSubject.next({ id: "newUserId" as UserId, ...accountInfo }); + activeAccountAccessor.switch("newUserId" as UserId); const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition)); state.nextState("value2"); state.nextState("value3"); @@ -125,12 +119,6 @@ describe("DefaultStateProvider", () => { ); describe("getUserState$", () => { - const accountInfo = { - email: "email", - emailVerified: false, - name: "name", - status: AuthenticationStatus.LoggedOut, - }; const keyDefinition = new UserKeyDefinition( new StateDefinition("test", "disk"), "test", @@ -141,7 +129,7 @@ describe("DefaultStateProvider", () => { ); it("should not emit any values until a truthy user id is supplied", async () => { - accountService.activeAccountSubject.next(null); + activeAccountAccessor.switch(null); const state = singleUserStateProvider.getFake(userId, keyDefinition); state.nextState("value"); @@ -151,7 +139,7 @@ describe("DefaultStateProvider", () => { expect(emissions).toHaveLength(0); - accountService.activeAccountSubject.next({ id: userId, ...accountInfo }); + activeAccountAccessor.switch(userId); await awaitAsync(); @@ -170,7 +158,7 @@ describe("DefaultStateProvider", () => { ); it("should emit default value if no userId supplied and first active user id emission in falsy", async () => { - accountService.activeAccountSubject.next(null); + activeAccountAccessor.switch(null); const emissions = trackEmissions( sut.getUserStateOrDefault$(keyDefinition, { diff --git a/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts b/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts index 6674c2577d7..701274eca31 100644 --- a/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended"; import { StorageServiceProvider } from "@bitwarden/storage-core"; -import { mockAccountServiceWith } from "../../../../spec/fake-account-service"; +import { FakeActiveUserAccessor } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { UserId } from "../../../types/guid"; import { LogService } from "../../abstractions/log.service"; @@ -39,7 +39,7 @@ describe("Specific State Providers", () => { stateEventRegistrarService, logService, ); - activeSut = new DefaultActiveUserStateProvider(mockAccountServiceWith(null), singleSut); + activeSut = new DefaultActiveUserStateProvider(new FakeActiveUserAccessor(null), singleSut); globalSut = new DefaultGlobalStateProvider(storageServiceProvider, logService); }); diff --git a/libs/common/src/platform/state/index.ts b/libs/common/src/platform/state/index.ts index 367beefb495..6dc14090503 100644 --- a/libs/common/src/platform/state/index.ts +++ b/libs/common/src/platform/state/index.ts @@ -10,5 +10,6 @@ export { KeyDefinition, KeyDefinitionOptions } from "./key-definition"; export { StateUpdateOptions } from "./state-update-options"; export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition"; export { StateEventRunnerService } from "./state-event-runner.service"; +export { ActiveUserAccessor } from "./active-user.accessor"; export * from "./state-definitions"; From 18bce185f03675ebbc1f53aa0936837f904329bb Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 31 Jul 2025 08:19:20 -0500 Subject: [PATCH 4/8] Fix TaxService.previewTaxAmountForOrganizationTrial return type (#15848) --- .../trial-initiation/trial-billing-step.component.ts | 3 ++- .../src/billing/abstractions/tax.service.abstraction.ts | 3 +-- libs/common/src/billing/services/tax.service.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index fda7faeeb25..e13fac41f75 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -304,6 +304,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy { this.fetchingTaxAmount = true; if (!this.taxInfoComponent.validate()) { + this.fetchingTaxAmount = false; return 0; } @@ -326,7 +327,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy { const response = await this.taxService.previewTaxAmountForOrganizationTrial(request); this.fetchingTaxAmount = false; - return response.taxAmount; + return response; }; get price() { diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts index 73dc848c95f..c94fbcba652 100644 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/tax.service.abstraction.ts @@ -3,7 +3,6 @@ import { PreviewIndividualInvoiceRequest } from "../models/request/preview-indiv import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax"; import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; -import { PreviewTaxAmountResponse } from "../models/response/tax"; export abstract class TaxServiceAbstraction { abstract getCountries(): CountryListItem[]; @@ -20,5 +19,5 @@ export abstract class TaxServiceAbstraction { abstract previewTaxAmountForOrganizationTrial: ( request: PreviewTaxAmountForOrganizationTrialRequest, - ) => Promise; + ) => Promise; } diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts index 2632ca7083b..27966016913 100644 --- a/libs/common/src/billing/services/tax.service.ts +++ b/libs/common/src/billing/services/tax.service.ts @@ -1,5 +1,4 @@ import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; -import { PreviewTaxAmountResponse } from "@bitwarden/common/billing/models/response/tax"; import { ApiService } from "../../abstractions/api.service"; import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; @@ -306,13 +305,14 @@ export class TaxService implements TaxServiceAbstraction { async previewTaxAmountForOrganizationTrial( request: PreviewTaxAmountForOrganizationTrialRequest, - ): Promise { - return await this.apiService.send( + ): Promise { + const response = await this.apiService.send( "POST", "/tax/preview-amount/organization-trial", request, true, true, ); + return response as number; } } From 95b1ab0cb758ecbe205d200c732f23eab5bf904b Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:11:06 +0100 Subject: [PATCH 5/8] Resolve the loading issue (#15795) --- .../complete-trial-initiation.component.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html index 98fe4032b55..e74997cb9f5 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html @@ -12,6 +12,7 @@ Date: Thu, 31 Jul 2025 11:45:35 -0500 Subject: [PATCH 6/8] [PM-23826] Crowdstrike integration dialog (#15757) --- .../integrations/integrations.component.ts | 381 ++++++++++-------- .../integration-card.component.html | 2 +- .../integration-card.component.spec.ts | 35 +- .../integration-card.component.ts | 78 +++- .../integration-card.stories.ts | 58 --- .../connect-dialog-hec.component.html | 38 ++ .../connect-dialog-hec.component.spec.ts | 176 ++++++++ .../connect-dialog-hec.component.ts | 81 ++++ .../integrations/integration-dialog/index.ts | 1 + .../integration-grid.component.html | 1 + .../integration-grid.component.spec.ts | 26 ++ .../integration-grid.stories.ts | 70 ---- .../shared/components/integrations/models.ts | 1 + apps/web/src/app/core/core.module.ts | 7 + apps/web/src/locales/en/messages.json | 12 + .../bit-common/src/dirt/integrations/index.ts | 5 + .../organization-integration-response.ts | 6 +- ...ganization-integration-api.service.spec.ts | 22 +- .../organization-integration-api.service.ts | 8 +- .../integrations.component.spec.ts | 30 +- 20 files changed, 678 insertions(+), 360 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts delete mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts diff --git a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts index c0a57c82954..3ddf9c0a720 100644 --- a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts +++ b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Observable, Subject, switchMap, takeUntil } from "rxjs"; +import { Observable, Subject, switchMap, takeUntil, scheduled, asyncScheduler } from "rxjs"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { getOrganizationById, OrganizationService, @@ -33,13 +35,192 @@ import { Integration } from "../shared/components/integrations/models"; ], }) export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { - integrationsList: Integration[] = []; + // integrationsList: Integration[] = []; tabIndex: number; organization$: Observable; isEventBasedIntegrationsEnabled: boolean = false; private destroy$ = new Subject(); + // initialize the integrations list with default integrations + integrationsList: Integration[] = [ + { + name: "AD FS", + linkURL: "https://bitwarden.com/help/saml-adfs/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.SSO, + }, + { + name: "Auth0", + linkURL: "https://bitwarden.com/help/saml-auth0/", + image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "AWS", + linkURL: "https://bitwarden.com/help/saml-aws/", + image: "../../../../../../../images/integrations/aws-color.svg", + imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/saml-azure/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Duo", + linkURL: "https://bitwarden.com/help/saml-duo/", + image: "../../../../../../../images/integrations/logo-duo-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Google", + linkURL: "https://bitwarden.com/help/saml-google/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/saml-jumpcloud/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "KeyCloak", + linkURL: "https://bitwarden.com/help/saml-keycloak/", + image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", + type: IntegrationType.SSO, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/saml-okta/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/saml-onelogin/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "PingFederate", + linkURL: "https://bitwarden.com/help/saml-pingfederate/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-scim-integration/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "Ping Identity", + linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Active Directory", + linkURL: "https://bitwarden.com/help/ldap-directory/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.BWDC, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Google Workspace", + linkURL: "https://bitwarden.com/help/workspace-directory/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-directory/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-directory/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "Splunk", + linkURL: "https://bitwarden.com/help/splunk-siem/", + image: "../../../../../../../images/integrations/logo-splunk-black.svg", + imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Sentinel", + linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", + image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Rapid7", + linkURL: "https://bitwarden.com/help/rapid7-siem/", + image: "../../../../../../../images/integrations/logo-rapid7-black.svg", + imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Elastic", + linkURL: "https://bitwarden.com/help/elastic-siem/", + image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Panther", + linkURL: "https://bitwarden.com/help/panther-siem/", + image: "../../../../../../../images/integrations/logo-panther-round-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Intune", + linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", + image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", + type: IntegrationType.DEVICE, + }, + ]; + ngOnInit(): void { + const orgId = this.route.snapshot.params.organizationId; + this.organization$ = this.route.params.pipe( switchMap((params) => this.accountService.activeAccount$.pipe( @@ -51,6 +232,25 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { ), ), ); + + scheduled(this.orgIntegrationApiService.getOrganizationIntegrations(orgId), asyncScheduler) + .pipe(takeUntil(this.destroy$)) + .subscribe((integrations) => { + // Update the integrations list with the fetched integrations + if (integrations && integrations.length > 0) { + integrations.forEach((integration) => { + const configJson = JSON.parse(integration.configuration || "{}"); + const serviceName = configJson.service ?? ""; + const existingIntegration = this.integrationsList.find((i) => i.name === serviceName); + + if (existingIntegration) { + // if a configuration exists, then it is connected + existingIntegration.isConnected = !!integration.configuration; + existingIntegration.configuration = integration.configuration || ""; + } + }); + } + }); } constructor( @@ -58,6 +258,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private accountService: AccountService, private configService: ConfigService, + private orgIntegrationApiService: OrganizationIntegrationApiService, ) { this.configService .getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations) @@ -66,182 +267,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.isEventBasedIntegrationsEnabled = isEnabled; }); - this.integrationsList = [ - { - name: "AD FS", - linkURL: "https://bitwarden.com/help/saml-adfs/", - image: "../../../../../../../images/integrations/azure-active-directory.svg", - type: IntegrationType.SSO, - }, - { - name: "Auth0", - linkURL: "https://bitwarden.com/help/saml-auth0/", - image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "AWS", - linkURL: "https://bitwarden.com/help/saml-aws/", - image: "../../../../../../../images/integrations/aws-color.svg", - imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/saml-azure/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Duo", - linkURL: "https://bitwarden.com/help/saml-duo/", - image: "../../../../../../../images/integrations/logo-duo-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Google", - linkURL: "https://bitwarden.com/help/saml-google/", - image: "../../../../../../../images/integrations/logo-google-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "JumpCloud", - linkURL: "https://bitwarden.com/help/saml-jumpcloud/", - image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "KeyCloak", - linkURL: "https://bitwarden.com/help/saml-keycloak/", - image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", - type: IntegrationType.SSO, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/saml-okta/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/saml-onelogin/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "PingFederate", - linkURL: "https://bitwarden.com/help/saml-pingfederate/", - image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.SCIM, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/okta-scim-integration/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "JumpCloud", - linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", - image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "Ping Identity", - linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", - image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", - type: IntegrationType.SCIM, - }, - { - name: "Active Directory", - linkURL: "https://bitwarden.com/help/ldap-directory/", - image: "../../../../../../../images/integrations/azure-active-directory.svg", - type: IntegrationType.BWDC, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/microsoft-entra-id/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.BWDC, - }, - { - name: "Google Workspace", - linkURL: "https://bitwarden.com/help/workspace-directory/", - image: "../../../../../../../images/integrations/logo-google-badge-color.svg", - type: IntegrationType.BWDC, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/okta-directory/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.BWDC, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/onelogin-directory/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.BWDC, - }, - { - name: "Splunk", - linkURL: "https://bitwarden.com/help/splunk-siem/", - image: "../../../../../../../images/integrations/logo-splunk-black.svg", - imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", - type: IntegrationType.EVENT, - }, - { - name: "Microsoft Sentinel", - linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", - image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Rapid7", - linkURL: "https://bitwarden.com/help/rapid7-siem/", - image: "../../../../../../../images/integrations/logo-rapid7-black.svg", - imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", - type: IntegrationType.EVENT, - }, - { - name: "Elastic", - linkURL: "https://bitwarden.com/help/elastic-siem/", - image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Panther", - linkURL: "https://bitwarden.com/help/panther-siem/", - image: "../../../../../../../images/integrations/logo-panther-round-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Microsoft Intune", - linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", - image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", - type: IntegrationType.DEVICE, - }, - ]; - if (this.isEventBasedIntegrationsEnabled) { this.integrationsList.push({ name: "Crowdstrike", diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html index 2c0db1cf933..e5687c71ed9 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html @@ -33,7 +33,7 @@

{{ description }}

@if (canSetupConnection) { - } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts index 16b7eb142e8..382d245b235 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts @@ -1,12 +1,15 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -// FIXME: remove `src` and fix import +import { ToastService } from "@bitwarden/components"; // eslint-disable-next-line no-restricted-imports import { SharedModule } from "@bitwarden/components/src/shared"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -17,6 +20,8 @@ describe("IntegrationCardComponent", () => { let component: IntegrationCardComponent; let fixture: ComponentFixture; const mockI18nService = mock(); + const activatedRoute = mock(); + const mockOrgIntegrationApiService = mock(); const systemTheme$ = new BehaviorSubject(ThemeType.Light); const usersPreferenceTheme$ = new BehaviorSubject(ThemeType.Light); @@ -24,26 +29,22 @@ describe("IntegrationCardComponent", () => { beforeEach(async () => { // reset system theme systemTheme$.next(ThemeType.Light); + activatedRoute.snapshot = { + paramMap: { + get: jest.fn().mockReturnValue("test-organization-id"), + }, + } as any; await TestBed.configureTestingModule({ imports: [IntegrationCardComponent, SharedModule], providers: [ - { - provide: ThemeStateService, - useValue: { selectedTheme$: usersPreferenceTheme$ }, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: systemTheme$, - }, - { - provide: I18nPipe, - useValue: mock(), - }, - { - provide: I18nService, - useValue: mockI18nService, - }, + { provide: ThemeStateService, useValue: { selectedTheme$: usersPreferenceTheme$ } }, + { provide: SYSTEM_THEME_OBSERVABLE, useValue: systemTheme$ }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService }, + { provide: ToastService, useValue: mock() }, ], }).compileComponents(); }); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts index 4188579bef9..1d95d3182b2 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts @@ -9,13 +9,26 @@ import { OnDestroy, ViewChild, } from "@angular/core"; -import { Observable, Subject, combineLatest, takeUntil } from "rxjs"; +import { ActivatedRoute } from "@angular/router"; +import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +// eslint-disable-next-line no-restricted-imports +import { + OrganizationIntegrationType, + OrganizationIntegrationRequest, + OrganizationIntegrationResponse, + OrganizationIntegrationApiService, +} from "@bitwarden/bit-common/dirt/integrations/index"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../../../../shared/shared.module"; +import { openHecConnectDialog } from "../integration-dialog/index"; +import { Integration } from "../models"; @Component({ selector: "app-integration-card", @@ -30,6 +43,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { @Input() image: string; @Input() imageDarkMode?: string; @Input() linkURL: string; + @Input() integrationSettings: Integration; /** Adds relevant `rel` attribute to external links */ @Input() externalURL?: boolean; @@ -49,6 +63,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private themeStateService: ThemeStateService, @Inject(SYSTEM_THEME_OBSERVABLE) private systemTheme$: Observable, + private dialogService: DialogService, + private activatedRoute: ActivatedRoute, + private apiService: OrganizationIntegrationApiService, + private toastService: ToastService, + private i18nService: I18nService, ) {} ngAfterViewInit() { @@ -101,9 +120,58 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { return this.isConnected !== undefined; } - setupConnection(app: string) { - // This method can be used to handle the connection logic for the integration - // For example, it could open a modal or redirect to a setup page - this.isConnected = !this.isConnected; // Toggle connection state for demonstration + async setupConnection() { + // invoke the dialog to connect the integration + const dialog = openHecConnectDialog(this.dialogService, { + data: { + settings: this.integrationSettings, + }, + }); + + const result = await lastValueFrom(dialog.closed); + + // the dialog was cancelled + if (!result || !result.success) { + return; + } + + // save the integration + try { + const dbResponse = await this.saveHecIntegration(result.configuration); + this.isConnected = !!dbResponse.id; + } catch { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("failedToSaveIntegration"), + }); + return; + } + } + + async saveHecIntegration(configuration: string): Promise { + const organizationId = this.activatedRoute.snapshot.paramMap.get( + "organizationId", + ) as OrganizationId; + + const request = new OrganizationIntegrationRequest( + OrganizationIntegrationType.Hec, + configuration, + ); + + const integrations = await this.apiService.getOrganizationIntegrations(organizationId); + const existingIntegration = integrations.find( + (i) => i.type === OrganizationIntegrationType.Hec, + ); + + if (existingIntegration) { + return await this.apiService.updateOrganizationIntegration( + organizationId, + existingIntegration.id, + request, + ); + } else { + return await this.apiService.createOrganizationIntegration(organizationId, request); + } } } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts deleted file mode 100644 index 256bfd3d827..00000000000 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { importProvidersFrom } from "@angular/core"; -import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; -import { of } from "rxjs"; - -import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; -import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; - -import { PreloadedEnglishI18nModule } from "../../../../../../core/tests"; - -import { IntegrationCardComponent } from "./integration-card.component"; - -class MockThemeService implements Partial {} - -export default { - title: "Web/Integration Layout/Integration Card", - component: IntegrationCardComponent, - decorators: [ - applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], - }), - moduleMetadata({ - providers: [ - { - provide: ThemeStateService, - useClass: MockThemeService, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeTypes.Light), - }, - ], - }), - ], - args: { - integrations: [], - }, -} as Meta; - -type Story = StoryObj; - -export const Default: Story = { - render: (args) => ({ - props: args, - template: /*html*/ ` - - `, - }), - args: { - name: "Bitwarden", - image: "/integrations/bitwarden-vertical-blue.svg", - linkURL: "https://bitwarden.com", - }, -}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html new file mode 100644 index 00000000000..7f28317dd67 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html @@ -0,0 +1,38 @@ +
+ + + {{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }} + +
+ @if (loading) { + + + + } + @if (!loading) { + + + {{ "url" | i18n }} + + + + {{ "bearerToken" | i18n }} + + + + {{ "index" | i18n }} + + + + } +
+ + + + +
+
diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts new file mode 100644 index 00000000000..9be854545aa --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts @@ -0,0 +1,176 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { Integration } from "../../models"; + +import { + ConnectHecDialogComponent, + HecConnectDialogParams, + HecConnectDialogResult, + openHecConnectDialog, +} from "./connect-dialog-hec.component"; + +beforeAll(() => { + // Mock element.animate for jsdom + // the animate function is not available in jsdom, so we provide a mock implementation + // This is necessary for tests that rely on animations + // This mock does not perform any actual animations, it just provides a structure that allows tests + // to run without throwing errors related to missing animate function + if (!HTMLElement.prototype.animate) { + HTMLElement.prototype.animate = function () { + return { + play: () => {}, + pause: () => {}, + finish: () => {}, + cancel: () => {}, + reverse: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + onfinish: null, + oncancel: null, + startTime: 0, + currentTime: 0, + playbackRate: 1, + playState: "idle", + replaceState: "active", + effect: null, + finished: Promise.resolve(), + id: "", + remove: () => {}, + timeline: null, + ready: Promise.resolve(), + } as unknown as Animation; + }; + } +}); + +describe("ConnectDialogHecComponent", () => { + let component: ConnectHecDialogComponent; + let fixture: ComponentFixture; + let dialogRefMock = mock>(); + const mockI18nService = mock(); + + const integrationMock: Integration = { + name: "Test Integration", + image: "test-image.png", + linkURL: "https://example.com", + imageDarkMode: "test-image-dark.png", + newBadgeExpiration: "2024-12-31", + description: "Test Description", + isConnected: false, + canSetupConnection: true, + type: IntegrationType.EVENT, + } as Integration; + const connectInfo: HecConnectDialogParams = { settings: integrationMock }; + + beforeEach(async () => { + dialogRefMock = mock>(); + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule], + providers: [ + FormBuilder, + { provide: DIALOG_DATA, useValue: connectInfo }, + { provide: DialogRef, useValue: dialogRefMock }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConnectHecDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + mockI18nService.t.mockImplementation((key) => key); + }); + + it("should create the component", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize form with empty values", () => { + expect(component.formGroup.value).toEqual({ + url: "", + bearerToken: "", + index: "", + service: "Test Integration", + }); + }); + + it("should have required validators for all fields", () => { + component.formGroup.setValue({ url: "", bearerToken: "", index: "", service: "" }); + expect(component.formGroup.valid).toBeFalsy(); + + component.formGroup.setValue({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeTruthy(); + }); + + it("should invalidate url if not matching pattern", () => { + component.formGroup.setValue({ + url: "ftp://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeFalsy(); + + component.formGroup.setValue({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeTruthy(); + }); + + it("should call dialogRef.close with correct result on submit", async () => { + component.formGroup.setValue({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + + await component.submit(); + + expect(dialogRefMock.close).toHaveBeenCalledWith({ + integrationSettings: integrationMock, + configuration: JSON.stringify({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }), + success: true, + error: null, + }); + }); +}); + +describe("openCrowdstrikeConnectDialog", () => { + it("should call dialogService.open with correct params", () => { + const dialogServiceMock = mock(); + const config: DialogConfig> = { + data: { settings: { name: "Test" } as Integration }, + } as any; + + openHecConnectDialog(dialogServiceMock, config); + + expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectHecDialogComponent, config); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts new file mode 100644 index 00000000000..c0af17db8d7 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts @@ -0,0 +1,81 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { Integration } from "../../models"; + +export type HecConnectDialogParams = { + settings: Integration; +}; + +export interface HecConnectDialogResult { + integrationSettings: Integration; + configuration: string; + success: boolean; + error: string | null; +} + +@Component({ + templateUrl: "./connect-dialog-hec.component.html", + imports: [SharedModule], +}) +export class ConnectHecDialogComponent implements OnInit { + loading = false; + formGroup = this.formBuilder.group({ + url: ["", [Validators.required, Validators.pattern("https?://.+")]], + bearerToken: ["", Validators.required], + index: ["", Validators.required], + service: ["", Validators.required], + }); + + constructor( + @Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams, + protected formBuilder: FormBuilder, + private dialogRef: DialogRef, + ) {} + + ngOnInit(): void { + const settings = this.getSettingsAsJson(this.connectInfo.settings.configuration ?? ""); + + if (settings) { + this.formGroup.patchValue({ + url: settings?.url || "", + bearerToken: settings?.bearerToken || "", + index: settings?.index || "", + service: this.connectInfo.settings.name, + }); + } + } + + getSettingsAsJson(configuration: string) { + try { + return JSON.parse(configuration); + } catch { + return {}; + } + } + + submit = async (): Promise => { + const formJson = this.formGroup.getRawValue(); + + const result: HecConnectDialogResult = { + integrationSettings: this.connectInfo.settings, + configuration: JSON.stringify(formJson), + success: true, + error: null, + }; + + this.dialogRef.close(result); + + return; + }; +} + +export function openHecConnectDialog( + dialogService: DialogService, + config: DialogConfig>, +) { + return dialogService.open(ConnectHecDialogComponent, config); +} diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts new file mode 100644 index 00000000000..8c4891b9aa8 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts @@ -0,0 +1 @@ +export * from "./connect-dialog/connect-dialog-hec.component"; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html index b4eaff993f0..661c57b47fc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html @@ -16,6 +16,7 @@ [description]="integration.description | i18n" [isConnected]="integration.isConnected" [canSetupConnection]="integration.canSetupConnection" + [integrationSettings]="integration" > diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts index 04866f4627b..01a512ac38c 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts @@ -1,14 +1,20 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services"; import { IntegrationType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeTypes } from "@bitwarden/common/platform/enums"; +// eslint-disable-next-line import/order import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; // FIXME: remove `src` and fix import + +import { ToastService } from "@bitwarden/components"; // eslint-disable-next-line no-restricted-imports import { SharedModule } from "@bitwarden/components/src/shared"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -21,6 +27,8 @@ import { IntegrationGridComponent } from "./integration-grid.component"; describe("IntegrationGridComponent", () => { let component: IntegrationGridComponent; let fixture: ComponentFixture; + const mockActivatedRoute = mock(); + const mockOrgIntegrationApiService = mock(); const integrations: Integration[] = [ { name: "Integration 1", @@ -37,6 +45,12 @@ describe("IntegrationGridComponent", () => { ]; beforeEach(() => { + mockActivatedRoute.snapshot = { + paramMap: { + get: jest.fn().mockReturnValue("test-organization-id"), + }, + } as any; + TestBed.configureTestingModule({ imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], providers: [ @@ -56,6 +70,18 @@ describe("IntegrationGridComponent", () => { provide: I18nService, useValue: mock({ t: (key, p1) => key + " " + p1 }), }, + { + provide: ActivatedRoute, + useValue: mockActivatedRoute, + }, + { + provide: OrganizationIntegrationApiService, + useValue: mockOrgIntegrationApiService, + }, + { + provide: ToastService, + useValue: mock(), + }, ], }); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts deleted file mode 100644 index b6580af2881..00000000000 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { importProvidersFrom } from "@angular/core"; -import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; -import { of } from "rxjs"; - -import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; -import { IntegrationType } from "@bitwarden/common/enums"; -import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; - -import { PreloadedEnglishI18nModule } from "../../../../../../core/tests"; -import { IntegrationCardComponent } from "../integration-card/integration-card.component"; -import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; - -class MockThemeService implements Partial {} - -export default { - title: "Web/Integration Layout/Integration Grid", - component: IntegrationGridComponent, - decorators: [ - applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], - }), - moduleMetadata({ - imports: [IntegrationCardComponent], - providers: [ - { - provide: ThemeStateService, - useClass: MockThemeService, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeTypes.Dark), - }, - ], - }), - ], -} as Meta; - -type Story = StoryObj; - -export const Default: Story = { - render: (args) => ({ - props: args, - template: /*html*/ ` - - `, - }), - args: { - integrations: [ - { - name: "Card 1", - linkURL: "https://bitwarden.com", - image: "/integrations/bitwarden-vertical-blue.svg", - type: IntegrationType.SSO, - }, - { - name: "Card 2", - linkURL: "https://bitwarden.com", - image: "/integrations/bitwarden-vertical-blue.svg", - type: IntegrationType.SDK, - }, - { - name: "Card 3", - linkURL: "https://bitwarden.com", - image: "/integrations/bitwarden-vertical-blue.svg", - type: IntegrationType.SCIM, - }, - ], - }, -}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts index a231523b578..b3d24ffb3b0 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts @@ -20,4 +20,5 @@ export type Integration = { description?: string; isConnected?: boolean; canSetupConnection?: boolean; + configuration?: string; }; diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 92f9eaaee03..a222b668043 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -41,6 +41,8 @@ import { InternalUserDecryptionOptionsServiceAbstraction, LoginEmailService, } from "@bitwarden/auth/common"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; @@ -392,6 +394,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultDeviceManagementComponentService, deps: [], }), + safeProvider({ + provide: OrganizationIntegrationApiService, + useClass: OrganizationIntegrationApiService, + deps: [ApiService], + }), ]; @NgModule({ diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 88a26c6f594..2a75bf51900 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9481,6 +9481,9 @@ "crowdstrikeEventIntegrationDesc": { "message": "Send event data to your Logscale instance" }, + "failedToSaveIntegration": { + "message": "Failed to save integration. Please try again later." + }, "deviceIdMissing": { "message": "Device ID is missing" }, @@ -9562,6 +9565,15 @@ "createNewClientToManageAsProvider": { "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, + "url": { + "message": "URL" + }, + "bearerToken": { + "message": "Bearer Token" + }, + "index": { + "message": "Index" + }, "selectAPlan": { "message": "Select a plan" }, diff --git a/bitwarden_license/bit-common/src/dirt/integrations/index.ts b/bitwarden_license/bit-common/src/dirt/integrations/index.ts index b2221a94a89..d2c1d173e3c 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/index.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/index.ts @@ -1 +1,6 @@ export * from "./services"; +export * from "./models/organization-integration-type"; +export * from "./models/organization-integration-request"; +export * from "./models/organization-integration-response"; +export * from "./models/organization-integration-configuration-request"; +export * from "./models/organization-integration-configuration-response"; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts index 00880ea4740..d4a836df4c3 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts @@ -5,11 +5,13 @@ import { OrganizationIntegrationType } from "./organization-integration-type"; export class OrganizationIntegrationResponse extends BaseResponse { id: OrganizationIntegrationId; - organizationIntegrationType: OrganizationIntegrationType; + type: OrganizationIntegrationType; + configuration: string; constructor(response: any) { super(response); this.id = this.getResponseProperty("Id"); - this.organizationIntegrationType = this.getResponseProperty("Type"); + this.type = this.getResponseProperty("Type"); + this.configuration = this.getResponseProperty("Configuration"); } } diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts index bf3e16ed430..10ea87486b4 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts @@ -11,17 +11,17 @@ import { OrganizationIntegrationApiService } from "./organization-integration-ap export const mockIntegrationResponse: any = { id: "1" as OrganizationIntegrationId, - organizationIntegrationType: OrganizationIntegrationType.Hec, + type: OrganizationIntegrationType.Hec, }; export const mockIntegrationResponses: any[] = [ { id: "1" as OrganizationIntegrationId, - OrganizationIntegrationType: OrganizationIntegrationType.Hec, + type: OrganizationIntegrationType.Hec, }, { id: "2" as OrganizationIntegrationId, - OrganizationIntegrationType: OrganizationIntegrationType.Webhook, + type: OrganizationIntegrationType.Webhook, }, ]; @@ -46,7 +46,7 @@ describe("OrganizationIntegrationApiService", () => { expect(result).toEqual(mockIntegrationResponses); expect(apiService.send).toHaveBeenCalledWith( "GET", - `organizations/${orgId}/integrations`, + `/organizations/${orgId}/integrations`, null, true, true, @@ -63,12 +63,10 @@ describe("OrganizationIntegrationApiService", () => { apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); const result = await service.createOrganizationIntegration(orgId, request); - expect(result.organizationIntegrationType).toEqual( - mockIntegrationResponse.organizationIntegrationType, - ); + expect(result.type).toEqual(mockIntegrationResponse.type); expect(apiService.send).toHaveBeenCalledWith( "POST", - `organizations/${orgId.toString()}/integrations`, + `/organizations/${orgId.toString()}/integrations`, request, true, true, @@ -86,12 +84,10 @@ describe("OrganizationIntegrationApiService", () => { apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); const result = await service.updateOrganizationIntegration(orgId, integrationId, request); - expect(result.organizationIntegrationType).toEqual( - mockIntegrationResponse.organizationIntegrationType, - ); + expect(result.type).toEqual(mockIntegrationResponse.type); expect(apiService.send).toHaveBeenCalledWith( "PUT", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, request, true, true, @@ -106,7 +102,7 @@ describe("OrganizationIntegrationApiService", () => { expect(apiService.send).toHaveBeenCalledWith( "DELETE", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, null, true, false, diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts index 5cf8efefb05..2c2266940e0 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts @@ -15,7 +15,7 @@ export class OrganizationIntegrationApiService { ): Promise { const response = await this.apiService.send( "GET", - `organizations/${orgId}/integrations`, + `/organizations/${orgId}/integrations`, null, true, true, @@ -29,7 +29,7 @@ export class OrganizationIntegrationApiService { ): Promise { const response = await this.apiService.send( "POST", - `organizations/${orgId}/integrations`, + `/organizations/${orgId}/integrations`, request, true, true, @@ -44,7 +44,7 @@ export class OrganizationIntegrationApiService { ): Promise { const response = await this.apiService.send( "PUT", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, request, true, true, @@ -58,7 +58,7 @@ export class OrganizationIntegrationApiService { ): Promise { await this.apiService.send( "DELETE", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, null, true, false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index b563591f32f..be4b5725ecc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -8,9 +9,12 @@ import {} from "@bitwarden/web-vault/app/shared"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { ToastService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component"; import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component"; @@ -33,23 +37,25 @@ class MockNewMenuComponent {} describe("IntegrationsComponent", () => { let fixture: ComponentFixture; + const mockOrgIntegrationApiService = mock(); + const activatedRouteMock = { + snapshot: { paramMap: { get: jest.fn() } }, + }; + const mockI18nService = mock(); + beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent], imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent], providers: [ - { - provide: I18nService, - useValue: mock(), - }, - { - provide: ThemeStateService, - useValue: mock(), - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeType.Light), - }, + { provide: I18nService, useValue: mock() }, + { provide: ThemeStateService, useValue: mock() }, + { provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService }, + { provide: ToastService, useValue: mock() }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, ], }).compileComponents(); fixture = TestBed.createComponent(IntegrationsComponent); From 6179bb4d138b15bef8c4b6bfb097b9d49959c931 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:56:14 -0400 Subject: [PATCH 7/8] [deps] Autofill: Update concurrently to v9.2.0 (#15495) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe5e5e51388..3c0ea1e6387 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "base64-loader": "1.0.0", "browserslist": "4.23.2", "chromatic": "13.1.2", - "concurrently": "9.1.2", + "concurrently": "9.2.0", "copy-webpack-plugin": "13.0.0", "cross-env": "7.0.3", "css-loader": "7.1.2", @@ -17206,9 +17206,9 @@ } }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c62b651027b..39f2732e33b 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "base64-loader": "1.0.0", "browserslist": "4.23.2", "chromatic": "13.1.2", - "concurrently": "9.1.2", + "concurrently": "9.2.0", "copy-webpack-plugin": "13.0.0", "cross-env": "7.0.3", "css-loader": "7.1.2", From be29a43a59948ae04dea36349de4ce4171d28278 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:52:30 -0400 Subject: [PATCH 8/8] [BRE-1022] Replacing SPs with managed identity access (#15853) --- .github/workflows/deploy-web.yml | 61 ++++++++++---------------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index e21f7ae1e79..d3788dc77b9 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -69,7 +69,6 @@ jobs: azure_login_client_key_name: ${{ steps.config.outputs.azure_login_client_key_name }} azure_login_subscription_id_key_name: ${{ steps.config.outputs.azure_login_subscription_id_key_name }} retrieve_secrets_keyvault: ${{ steps.config.outputs.retrieve_secrets_keyvault }} - sync_utility: ${{ steps.config.outputs.sync_utility }} sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }} slack_channel_name: ${{ steps.config.outputs.slack_channel_name }} steps: @@ -127,8 +126,6 @@ jobs: echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT ;; esac - # Set the sync utility to use for deployment to the environment (az-sync or azcopy) - echo "sync_utility=azcopy" >> $GITHUB_OUTPUT - name: Environment Protection env: @@ -337,32 +334,6 @@ jobs: description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' ref: ${{ needs.artifact-check.outputs.artifact_build_commit }} - - name: Login to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets[needs.setup.outputs.azure_login_subscription_id_key_name] }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets[needs.setup.outputs.azure_login_client_key_name] }} - - - name: Retrieve Storage Account connection string for az sync - if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} - id: retrieve-secrets-az-sync - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }} - secrets: "sa-bitwarden-web-vault-dev-key-temp" - - - name: Retrieve Storage Account name and SPN credentials for azcopy - if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} - id: retrieve-secrets-azcopy - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }} - secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' if: ${{ inputs.build-web-run-id }} uses: bitwarden/gh-actions/download-artifacts@main @@ -389,28 +360,32 @@ jobs: working-directory: apps/web run: unzip ${{ env._ENVIRONMENT_ARTIFACT }} - - name: Sync to Azure Storage Account using az storage blob sync - if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} - working-directory: apps/web - run: | - az storage blob sync \ - --source "./build" \ - --container '$web' \ - --connection-string "${{ steps.retrieve-secrets-az-sync.outputs.sa-bitwarden-web-vault-dev-key-temp }}" \ - --delete-destination=${{ inputs.force-delete-destination }} + - name: Login to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets[needs.setup.outputs.azure_login_subscription_id_key_name] }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets[needs.setup.outputs.azure_login_client_key_name] }} + + - name: Retrieve Storage Account name + id: retrieve-secrets-azcopy + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }} + secrets: "sa-bitwarden-web-vault-name" - name: Sync to Azure Storage Account using azcopy - if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} working-directory: apps/web env: - AZCOPY_AUTO_LOGIN_TYPE: SPN - AZCOPY_SPA_APPLICATION_ID: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-appid }} - AZCOPY_SPA_CLIENT_SECRET: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-password }} - AZCOPY_TENANT_ID: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-tenant }} + AZCOPY_AUTO_LOGIN_TYPE: AZCLI + AZCOPY_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} run: | azcopy sync ./build 'https://${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }}.blob.core.windows.net/$web/' \ --delete-destination=${{ inputs.force-delete-destination }} --compare-hash="MD5" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Debug sync logs if: ${{ inputs.debug }} run: cat /home/runner/.azcopy/*.log