mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 19:23:19 +00:00
PM-23824 updated PR to move as much code to the service and remove code from components
This commit is contained in:
@@ -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<Organization>;
|
||||
isEventBasedIntegrationsEnabled: boolean = false;
|
||||
@@ -222,7 +221,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit(): Promise<void> {
|
||||
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<HecConfiguration>(
|
||||
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,
|
||||
|
||||
@@ -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<void> = new Subject();
|
||||
@ViewChild("imageEle") imageEle: ElementRef<HTMLImageElement>;
|
||||
@ViewChild("imageEle") imageEle!: ElementRef<HTMLImageElement>;
|
||||
|
||||
@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<HecConfiguration>(
|
||||
this.integrationSettings.configuration,
|
||||
),
|
||||
template: this.organizationIntegrationService.convertToJson<HecConfigurationTemplate>(
|
||||
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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}';
|
||||
|
||||
@@ -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<OrganizationIntegrationResponse[]>([]);
|
||||
integrations$ = this.integrations.asObservable();
|
||||
|
||||
private integrationConfigurations = new BehaviorSubject<
|
||||
private destroy$ = new Subject<void>();
|
||||
private organizationId$ = new BehaviorSubject<OrganizationId | null>(null);
|
||||
private integrations$ = new BehaviorSubject<OrganizationIntegrationResponse[]>([]);
|
||||
private integrationConfigurations$ = new BehaviorSubject<
|
||||
OrganizationIntegrationConfigurationResponseWithIntegrationId[]
|
||||
>([]);
|
||||
integrationConfigurations$ = this.integrationConfigurations.asObservable();
|
||||
|
||||
private masterIntegrationList$ = new BehaviorSubject<Integration[]>([]);
|
||||
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<HecConfiguration>(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<void>[] = [];
|
||||
|
||||
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<void>[] = [];
|
||||
|
||||
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<T>(jsonString: string): T | null {
|
||||
convertToJson<T>(jsonString?: string): T | null {
|
||||
try {
|
||||
return JSON.parse(jsonString) as T;
|
||||
return JSON.parse(jsonString || "") as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user