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 91e219da6f3..3785399b6e5 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,10 +2,10 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Observable, Subject, switchMap, takeUntil, combineLatest } from "rxjs"; +import { Observable, Subject, switchMap, takeUntil } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { HecConfiguration } from "@bitwarden/bit-common/dirt/integrations"; +import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/integrations"; import { getOrganizationById, OrganizationService, @@ -36,7 +36,6 @@ import { OrganizationIntegrationService } from "../shared/components/integration ], }) export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { - // integrationsList: Integration[] = []; tabIndex: number; organization$: Observable; isEventBasedIntegrationsEnabled: boolean = false; @@ -222,7 +221,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { const orgId = this.route.snapshot.params.organizationId; - await this.organizationIntegrationService.getIntegrationsAndConfigurations(orgId); + await this.organizationIntegrationService.setOrganizationId(orgId, this.integrationsList); this.organization$ = this.route.params.pipe( switchMap((params) => @@ -236,41 +235,10 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { ), ); - combineLatest([ - this.organizationIntegrationService.integrations$, - this.organizationIntegrationService.integrationConfigurations$, - ]) + this.organizationIntegrationService.integrationList$ .pipe(takeUntil(this.destroy$)) - .subscribe(([integrations, integrationConfigurations]) => { - const existingIntegrations = [...this.integrationsList]; - - // Update the integrations list with the fetched integrations - if (integrations && integrations.length > 0) { - integrations.forEach((integration) => { - const configJson = this.organizationIntegrationService.convertToJson( - integration.configuration, - ); - const serviceName = configJson?.service ?? ""; - const existingIntegration = existingIntegrations.find((i) => i.name === serviceName); - - if (existingIntegration) { - // update integrations - existingIntegration.isConnected = !!integration.configuration; - existingIntegration.configuration = integration.configuration || ""; - - const template = this.organizationIntegrationService.getIntegrationConfiguration( - integration.id, - serviceName, - integrationConfigurations, - ); - - existingIntegration.template = JSON.stringify(template || {}); - } - }); - } - - // update the integrations list - this.integrationsList = existingIntegrations; + .subscribe((integrations) => { + this.integrationsList = integrations; }); } @@ -290,7 +258,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { if (this.isEventBasedIntegrationsEnabled) { this.integrationsList.push({ - name: "Crowdstrike", + name: OrganizationIntegrationServiceType.CrowdStrike, linkURL: "", image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg", type: IntegrationType.EVENT, 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 c193a72bcfe..78b57b8c08f 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 @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AfterViewInit, Component, @@ -13,11 +11,6 @@ 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 { - HecConfiguration, - HecConfigurationTemplate, -} 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"; @@ -26,7 +19,7 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../../../../shared/shared.module"; import { openHecConnectDialog } from "../integration-dialog/index"; -import { Integration } from "../models"; +import { HecConfiguration, HecConfigurationTemplate, Integration } from "../models"; import { OrganizationIntegrationService } from "../services/organization-integration.service"; @Component({ @@ -36,13 +29,13 @@ import { OrganizationIntegrationService } from "../services/organization-integra }) export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private destroyed$: Subject = new Subject(); - @ViewChild("imageEle") imageEle: ElementRef; + @ViewChild("imageEle") imageEle!: ElementRef; - @Input() name: string; - @Input() image: string; - @Input() imageDarkMode?: string; - @Input() linkURL: string; - @Input() integrationSettings: Integration; + @Input() name: string = ""; + @Input() image: string = ""; + @Input() imageDarkMode: string = ""; + @Input() linkURL: string = ""; + @Input() integrationSettings!: Integration; /** Adds relevant `rel` attribute to external links */ @Input() externalURL?: boolean; @@ -69,13 +62,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private organizationIntegrationService: OrganizationIntegrationService, private toastService: ToastService, private i18nService: I18nService, - ) {} - - ngAfterViewInit() { + ) { this.organizationId = this.activatedRoute.snapshot.paramMap.get( "organizationId", ) as OrganizationId; + } + ngAfterViewInit() { combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$]) .pipe(takeUntil(this.destroyed$)) .subscribe(([theme, systemTheme]) => { @@ -134,12 +127,6 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { const dialog = openHecConnectDialog(this.dialogService, { data: { settings: this.integrationSettings, - configuration: this.organizationIntegrationService.convertToJson( - this.integrationSettings.configuration, - ), - template: this.organizationIntegrationService.convertToJson( - this.integrationSettings.template, - ), }, }); @@ -156,26 +143,16 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { // save the integration try { - const dbResponse = await this.organizationIntegrationService.saveHec( + await this.organizationIntegrationService.saveHec( this.organizationId, this.integrationSettings.name, integration, configurationTemplate, ); - - if (!!dbResponse.integration && !!dbResponse.configuration) { - this.isConnected = true; - this.integrationSettings.configuration = dbResponse.integration.configuration; - this.integrationSettings.template = dbResponse.configuration.template; - } - } catch (err) { - this.isConnected = false; - // eslint-disable-next-line no-console - console.error("Failed to save integration", err); - + } catch { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("failedToSaveIntegration"), }); return; 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 index 8acb58c1ef4..3593660aaaa 100644 --- 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 @@ -1,11 +1,6 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -// eslint-disable-next-line no-restricted-imports -import { - HecConfiguration, - HecConfigurationTemplate, -} from "@bitwarden/bit-common/dirt/integrations"; import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; @@ -13,8 +8,6 @@ import { Integration } from "../../models"; export type HecConnectDialogParams = { settings: Integration; - configuration: HecConfiguration | null; - template: HecConfigurationTemplate | null; }; export interface HecConnectDialogResult { @@ -48,15 +41,15 @@ export class ConnectHecDialogComponent implements OnInit { ngOnInit(): void { this.formGroup.patchValue({ - url: this.connectInfo.configuration?.uri || "", - bearerToken: this.connectInfo.configuration?.token || "", - index: this.connectInfo.template?.index || "", + url: this.connectInfo.settings.HecConfiguration?.uri || "", + bearerToken: this.connectInfo.settings.HecConfiguration?.token || "", + index: this.connectInfo.settings.HecConfigurationTemplate?.index || "", service: this.connectInfo.settings.name, }); } isUpdateAvailable(): boolean { - return !!this.connectInfo.configuration; + return !!this.connectInfo.settings.HecConfiguration; } getSettingsAsJson(configuration: string) { 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 b781beb8466..567ed205cdc 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 @@ -1,3 +1,5 @@ +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/integrations"; import { IntegrationType } from "@bitwarden/common/enums"; /** Integration or SDK */ @@ -22,4 +24,48 @@ export type Integration = { canSetupConnection?: boolean; configuration?: string; template?: string; + + HecConfiguration?: HecConfiguration | null; + HecConfigurationTemplate?: HecConfigurationTemplate | null; }; + +/* + * Represents the configuration for a HEC (HTTP Event Collector) integration. + * Configuration model that is required by OrganizationIntegration. + */ +export class HecConfiguration { + uri: string; + scheme = "Bearer"; + token: string; + service: OrganizationIntegrationServiceType; + + constructor(uri: string, token: string, service: string) { + this.uri = uri; + this.token = token; + this.service = service as OrganizationIntegrationServiceType; + } + + toString(): string { + return JSON.stringify(this); + } +} + +/** + * Represents the configuration template for a HEC (HTTP Event Collector) integration. + * from OrganizationIntegrationConfiguration + */ +export class HecConfigurationTemplate { + event = "#EventMessage"; + source = "Bitwarden"; + index: string; + service: OrganizationIntegrationServiceType; + + constructor(index: string, service: string) { + this.index = index; + this.service = service as OrganizationIntegrationServiceType; + } + + toString(): string { + return JSON.stringify(this); + } +} diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.spec.ts index d1b866d043f..d74f0c241e2 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.spec.ts @@ -32,92 +32,6 @@ describe("OrganizationIntegrationService", () => { expect(service).toBeTruthy(); }); - describe("getIntegrationsAndConfigurations", () => { - const orgId = "org-123" as any; - - beforeEach(() => { - mockOrgIntegrationApiService.getOrganizationIntegrations.mockReset(); - mockOrgIntegrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReset(); - service["integrations"].next([]); - service["integrationConfigurations"].next([]); - }); - - it("should fetch integrations and their configurations and update observables", async () => { - const integration1 = { id: "int-1", type: "type1" } as any; - const integration2 = { id: "int-2", type: "type2" } as any; - const config1 = [{ id: "conf-1", template: "{}" }] as any; - const config2 = [{ id: "conf-2", template: "{}" }] as any; - - mockOrgIntegrationApiService.getOrganizationIntegrations.mockResolvedValue([ - integration1, - integration2, - ]); - mockOrgIntegrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockImplementation( - (_, integrationId) => { - if (integrationId === "int-1") { - return Promise.resolve(config1); - } - if (integrationId === "int-2") { - return Promise.resolve(config2); - } - return Promise.resolve([]); - }, - ); - - const result = await service.getIntegrationsAndConfigurations(orgId); - - expect(mockOrgIntegrationApiService.getOrganizationIntegrations).toHaveBeenCalledWith(orgId); - expect( - mockOrgIntegrationConfigurationApiService.getOrganizationIntegrationConfigurations, - ).toHaveBeenCalledWith(orgId, "int-1"); - expect( - mockOrgIntegrationConfigurationApiService.getOrganizationIntegrationConfigurations, - ).toHaveBeenCalledWith(orgId, "int-2"); - - expect(result.integrations).toEqual([integration1, integration2]); - expect(result.configurations.length).toBe(2); - expect(result.configurations[0].integrationId).toBe("int-1"); - expect(result.configurations[1].integrationId).toBe("int-2"); - - // Check observables updated - service.integrations$.subscribe((integrations) => { - expect(integrations).toEqual([integration1, integration2]); - }); - service.integrationConfigurations$.subscribe((configs) => { - expect(configs.length).toBe(2); - }); - }); - - it("should handle no integrations", async () => { - mockOrgIntegrationApiService.getOrganizationIntegrations.mockResolvedValue([]); - const result = await service.getIntegrationsAndConfigurations(orgId); - - expect(result.integrations).toEqual([]); - expect(result.configurations).toEqual([]); - service.integrations$.subscribe((integrations) => { - expect(integrations).toEqual([]); - }); - service.integrationConfigurations$.subscribe((configs) => { - expect(configs).toEqual([]); - }); - }); - - it("should handle integrations with no configurations", async () => { - const integration = { id: "int-1", type: "type1" } as any; - mockOrgIntegrationApiService.getOrganizationIntegrations.mockResolvedValue([integration]); - mockOrgIntegrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockResolvedValue( - [], - ); - - const result = await service.getIntegrationsAndConfigurations(orgId); - - expect(result.integrations).toEqual([integration]); - expect(result.configurations.length).toBe(1); - expect(result.configurations[0].integrationId).toBe("int-1"); - expect(result.configurations[0].configurationResponses).toEqual([]); - }); - }); - describe("saveHec", () => { const organizationId = "org-123" as any; const serviceName = "splunk"; @@ -127,8 +41,6 @@ describe("OrganizationIntegrationService", () => { beforeEach(() => { mockOrgIntegrationApiService.getOrganizationIntegrations.mockReset(); mockOrgIntegrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReset(); - service["integrations"].next([]); - service["integrationConfigurations"].next([]); jest.clearAllMocks(); }); @@ -185,123 +97,6 @@ describe("OrganizationIntegrationService", () => { }); }); - describe("getIntegrationConfiguration", () => { - const integrationId = "int-1" as any; - const serviceName = "splunk"; - const otherServiceName = "datadog"; - - it("should return null if integrationConfigurations is empty", () => { - const result = service.getIntegrationConfiguration(integrationId, serviceName, []); - expect(result).toBeNull(); - }); - - it("should return null if no integrationConfigs found for integrationId", () => { - const integrationConfigurations = [ - { - integrationId: "int-2", - configurationResponses: [{ id: "conf-1", template: '{"service":"splunk"}' }], - }, - ] as any; - const result = service.getIntegrationConfiguration( - integrationId, - serviceName, - integrationConfigurations, - ); - expect(result).toBeNull(); - }); - - it("should return null if no configurationResponses match the service", () => { - const sampleIntegrationConfigurations = [ - { - integrationId, - configurationResponses: [{ id: "conf-1", template: '{"service":"splunk"}' }], - }, - ] as any; - const result = service.getIntegrationConfiguration( - integrationId, - otherServiceName, - sampleIntegrationConfigurations, - ); - expect(result).toBeNull(); - }); - - it("should return the configuration template if service matches", () => { - const template = { service: "splunk", token: "abc" }; - const integrationConfigurations = [ - { - integrationId, - configurationResponses: [ - { id: "conf-1", template: JSON.stringify(template) }, - { id: "conf-2", template: '{"service":"datadog"}' }, - ], - }, - ] as any; - const result = service.getIntegrationConfiguration( - integrationId, - serviceName, - integrationConfigurations, - ); - expect(result).toEqual(template); - }); - - it("should return the first matching configuration if multiple match", () => { - const template1 = { service: "splunk", token: "abc" }; - const template2 = { service: "splunk", token: "def" }; - const integrationConfigurations = [ - { - integrationId, - configurationResponses: [ - { id: "conf-1", template: JSON.stringify(template1) }, - { id: "conf-2", template: JSON.stringify(template2) }, - ], - }, - ] as any; - const result = service.getIntegrationConfiguration( - integrationId, - serviceName, - integrationConfigurations, - ); - expect(result).toEqual(template1); - }); - - it("should skip invalid JSON templates", () => { - const template = { service: "splunk", token: "abc" }; - const integrationConfigurations = [ - { - integrationId, - configurationResponses: [ - { id: "conf-1", template: "invalid-json" }, - { id: "conf-2", template: JSON.stringify(template) }, - ], - }, - ] as any; - const result = service.getIntegrationConfiguration( - integrationId, - serviceName, - integrationConfigurations, - ); - expect(result).toEqual(template); - }); - - it("should return null if all templates are invalid JSON", () => { - const integrationConfigurations = [ - { - integrationId, - configurationResponses: [ - { id: "conf-1", template: "invalid-json" }, - { id: "conf-2", template: "" }, - ], - }, - ] as any; - const result = service.getIntegrationConfiguration( - integrationId, - serviceName, - integrationConfigurations, - ); - expect(result).toBeNull(); - }); - }); - describe("convertToJson", () => { it("should parse valid JSON string and return object", () => { const jsonString = '{"foo":"bar","num":42}'; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.ts index 3526371e5a1..91eaf105e2d 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/services/organization-integration.service.ts @@ -1,5 +1,13 @@ import { Injectable } from "@angular/core"; -import { BehaviorSubject } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + firstValueFrom, + Subject, + switchMap, + takeUntil, + zip, +} from "rxjs"; // eslint-disable-next-line no-restricted-imports import { @@ -7,82 +15,152 @@ import { OrganizationIntegrationConfigurationApiService, OrganizationIntegrationConfigurationRequest, OrganizationIntegrationConfigurationResponse, - HecConfiguration, OrganizationIntegrationRequest, OrganizationIntegrationResponse, OrganizationIntegrationType, - HecConfigurationTemplate, OrganizationIntegrationConfigurationResponseWithIntegrationId, } from "@bitwarden/bit-common/dirt/integrations"; import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid"; +import { HecConfiguration, HecConfigurationTemplate, Integration } from "../models"; + @Injectable({ providedIn: "root", }) export class OrganizationIntegrationService { - private integrations = new BehaviorSubject([]); - integrations$ = this.integrations.asObservable(); - - private integrationConfigurations = new BehaviorSubject< + private destroy$ = new Subject(); + private organizationId$ = new BehaviorSubject(null); + private integrations$ = new BehaviorSubject([]); + private integrationConfigurations$ = new BehaviorSubject< OrganizationIntegrationConfigurationResponseWithIntegrationId[] >([]); - integrationConfigurations$ = this.integrationConfigurations.asObservable(); + + private masterIntegrationList$ = new BehaviorSubject([]); + integrationList$ = this.masterIntegrationList$.asObservable(); + + // retrieve integrations and configurations from the DB + private fetch$ = this.organizationId$ + .pipe( + switchMap(async (orgId) => { + if (orgId) { + const data$ = await this.getIntegrationsAndConfigurations(orgId); + return await firstValueFrom(data$); + } else { + return { + integrations: this.integrations$.value, + configurations: this.integrationConfigurations$.value, + }; + } + }), + takeUntil(this.destroy$), + ) + .subscribe({ + next: ({ integrations, configurations }) => { + // update the integrations + this.integrations$.next(integrations); + this.integrationConfigurations$.next(configurations); + }, + }); + + // Update the master Integration list - if any of the integrations or configurations change + private mapping$ = combineLatest([this.integrations$, this.integrationConfigurations$]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([integrations, configurations]) => { + const existingIntegrations = [...this.masterIntegrationList$.value]; + + // Update the integrations list with the fetched integrations + if (integrations && integrations.length > 0) { + integrations.forEach((integration) => { + const hecConfigJson = this.convertToJson(integration.configuration); + const serviceName = hecConfigJson?.service ?? ""; + const existingIntegration = existingIntegrations.find((i) => i.name === serviceName); + + if (existingIntegration) { + // update integrations + existingIntegration.isConnected = !!integration.configuration; + existingIntegration.configuration = integration.configuration || ""; + existingIntegration.HecConfiguration = hecConfigJson; + + const template = this.getIntegrationConfiguration( + integration.id, + serviceName, + configurations, + ); + + existingIntegration.HecConfigurationTemplate = template; + existingIntegration.template = JSON.stringify(template || {}); + } + }); + } + + // update the integrations list + this.masterIntegrationList$.next(existingIntegrations); + }); constructor( private integrationApiService: OrganizationIntegrationApiService, private integrationConfigurationApiService: OrganizationIntegrationConfigurationApiService, ) {} - /* - * Fetches the integrations and their configurations for a specific organization. - * @param orgId The ID of the organization. - * Invoke this method to retrieve the integrations into observable. - */ - async getIntegrationsAndConfigurations(orgId: OrganizationId) { - const promises: Promise[] = []; - - const integrations = await this.integrationApiService.getOrganizationIntegrations(orgId); - const integrationConfigurations: OrganizationIntegrationConfigurationResponseWithIntegrationId[] = - []; - - integrations.forEach((integration) => { - const promise = this.integrationConfigurationApiService - .getOrganizationIntegrationConfigurations(orgId, integration.id) - .then((configs) => { - const mappedConfigurations = - new OrganizationIntegrationConfigurationResponseWithIntegrationId( - integration.id, - configs, - ); - integrationConfigurations.push(mappedConfigurations); - }); - promises.push(promise); - }); - - await Promise.all(promises); - - this.integrations.next(integrations); - this.integrationConfigurations.next(integrationConfigurations); - - return { - integrations: integrations, - configurations: integrationConfigurations, - }; + setOrganizationId(orgId: OrganizationId, integrationList: Integration[]) { + this.organizationId$.next(orgId); + this.masterIntegrationList$.next(integrationList); } + private async getIntegrationsAndConfigurations(orgId: OrganizationId) { + const results$ = zip(this.integrationApiService.getOrganizationIntegrations(orgId)).pipe( + switchMap(([integrations]) => { + const integrationConfigurations: OrganizationIntegrationConfigurationResponseWithIntegrationId[] = + []; + const promises: Promise[] = []; + + integrations.forEach((integration) => { + const promise = this.integrationConfigurationApiService + .getOrganizationIntegrationConfigurations(orgId, integration.id) + .then((configs) => { + const mappedConfigurations = + new OrganizationIntegrationConfigurationResponseWithIntegrationId( + integration.id, + configs, + ); + integrationConfigurations.push(mappedConfigurations); + }); + promises.push(promise); + }); + + return Promise.all(promises).then(() => { + return { integrations, configurations: integrationConfigurations }; + }); + }), + ); + + return results$; + } + + /* + * Saves the HEC integration configuration for a specific organization. + * @param organizationId The ID of the organization. + * @param service The name of the service. + * @param hecConfiguration The HEC integration configuration. + * @returns The saved or updated integration response. + */ async saveHec( organizationId: OrganizationId, service: string, hecConfiguration: HecConfiguration, hecConfigurationTemplate: HecConfigurationTemplate, - ) { + ): Promise<{ + integration: OrganizationIntegrationResponse; + configuration: OrganizationIntegrationConfigurationResponse; + }> { + // save the Hec Integration record const integrationResponse = await this.saveHecIntegration(organizationId, hecConfiguration); if (!integrationResponse.id) { throw new Error("Failed to save HEC integration"); } - // Save the configuration for the HEC integration + // Save the configuration for the HEC integration record const configurationResponse = await this.saveHecIntegrationConfiguration( organizationId, integrationResponse.id, @@ -121,7 +199,7 @@ export class OrganizationIntegrationService { ); // find the existing integration - const existingIntegration = this.integrations.value.find( + const existingIntegration = this.integrations$.value.find( (i) => i.type === OrganizationIntegrationType.Hec, ); @@ -134,14 +212,14 @@ export class OrganizationIntegrationService { ); // update our observable with the updated integration - const updatedIntegrations = this.integrations.value.map((integration) => { + const updatedIntegrations = this.integrations$.value.map((integration) => { if (integration.id === existingIntegration.id) { return updatedIntegration; } return integration; }); - this.integrations.next(updatedIntegrations); + this.integrations$.next(updatedIntegrations); return updatedIntegration; } else { @@ -152,7 +230,7 @@ export class OrganizationIntegrationService { ); // add this to our integrations observable - this.integrations.next([...this.integrations.value, newIntegration]); + this.integrations$.next([...this.integrations$.value, newIntegration]); return newIntegration; } } @@ -180,14 +258,15 @@ export class OrganizationIntegrationService { configurationTemplate.toString(), ); - // check if we have an existing configuration for this integration - in case of new records - const integrationConfigurations = this.integrationConfigurations.value; + // check if we have an existing configuration for this integration + const integrationConfigurations = this.integrationConfigurations$.value; - // find the existing configuration + // find the existing configuration by integrationId const existingConfigurations = integrationConfigurations .filter((config) => config.integrationId === integrationId) .flatMap((config) => config.configurationResponses || []); + // find the configuration by service const existingConfiguration = existingConfigurations.length > 0 ? existingConfigurations.find( @@ -218,7 +297,7 @@ export class OrganizationIntegrationService { } }); - this.integrationConfigurations.next(integrationConfigurations); + this.integrationConfigurations$.next(integrationConfigurations); return updatedConfiguration; } else { @@ -234,16 +313,22 @@ export class OrganizationIntegrationService { const integrationConfig = integrationConfigurations.find( (config) => config.integrationId === integrationId, ); + if (integrationConfig) { integrationConfig.configurationResponses.push(newConfiguration); + } else { + integrationConfigurations.push({ + integrationId, + configurationResponses: [newConfiguration], + }); } - this.integrationConfigurations.next(integrationConfigurations); + this.integrationConfigurations$.next(integrationConfigurations); return newConfiguration; } } - getIntegrationConfiguration( + private getIntegrationConfiguration( integrationId: OrganizationIntegrationId, service: string, integrationConfigurations: OrganizationIntegrationConfigurationResponseWithIntegrationId[], @@ -270,9 +355,9 @@ export class OrganizationIntegrationService { return null; } - convertToJson(jsonString: string): T | null { + convertToJson(jsonString?: string): T | null { try { - return JSON.parse(jsonString) as T; + return JSON.parse(jsonString || "") as T; } catch { return null; } diff --git a/bitwarden_license/bit-common/src/dirt/integrations/index.ts b/bitwarden_license/bit-common/src/dirt/integrations/index.ts index d2c1d173e3c..8f60d44c11c 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/index.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/index.ts @@ -4,3 +4,4 @@ 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"; +export * from "./models/organization-integration-service-type"; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts index 289bb36dcad..95f7d180dae 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts @@ -9,40 +9,3 @@ export class OrganizationIntegrationRequest { this.configuration = configuration; } } - -/* - * Represents the configuration for a HEC (HTTP Event Collector) integration. - * Configuration model that is required by OrganizationIntegration. - */ -export class HecConfiguration { - uri: string; - scheme = "Bearer"; - token: string; - service: string; - - constructor(uri: string, token: string, service: string) { - this.uri = uri; - this.token = token; - this.service = service; - } - - toString(): string { - return JSON.stringify(this); - } -} - -export class HecConfigurationTemplate { - event = "#EventMessage"; - source = "Bitwarden"; - index: string; - service: string; - - constructor(index: string, service: string) { - this.index = index; - this.service = service; - } - - toString(): string { - return JSON.stringify(this); - } -}