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 78b57b8c08f..60905b8add2 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
@@ -18,7 +18,7 @@ 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 { HecConnectDialogResultStatus, openHecConnectDialog } from "../integration-dialog/index";
import { HecConfiguration, HecConfigurationTemplate, Integration } from "../models";
import { OrganizationIntegrationService } from "../services/organization-integration.service";
@@ -141,6 +141,19 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
const integration = new HecConfiguration(result.url, result.bearerToken, result.service);
const configurationTemplate = new HecConfigurationTemplate(result.index, result.service);
+ if (result.success === HecConnectDialogResultStatus.Edited) {
+ await this.saveIntegration(integration, configurationTemplate);
+ }
+
+ if (result.success === HecConnectDialogResultStatus.Delete) {
+ await this.deleteIntegration(integration, configurationTemplate);
+ }
+ }
+
+ async saveIntegration(
+ integration: HecConfiguration,
+ configurationTemplate: HecConfigurationTemplate,
+ ) {
// save the integration
try {
await this.organizationIntegrationService.saveHec(
@@ -158,4 +171,25 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
return;
}
}
+
+ async deleteIntegration(
+ integration: HecConfiguration,
+ configurationTemplate: HecConfigurationTemplate,
+ ) {
+ // delete the integration
+ try {
+ await this.organizationIntegrationService.deleteHec(
+ this.organizationId,
+ integration,
+ configurationTemplate,
+ );
+ } catch {
+ this.toastService.showToast({
+ variant: "error",
+ title: "",
+ message: this.i18nService.t("failedToDeleteIntegration"),
+ });
+ return;
+ }
+ }
}
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
index bd72c3d77a6..9f9c2ac44e4 100644
--- 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
@@ -37,6 +37,18 @@
+
+ @if (canDelete()) {
+
+
+
+ }
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
index d0481ebce79..f9403c0f116 100644
--- 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
@@ -16,6 +16,7 @@ import {
HecConnectDialogParams,
HecConnectDialogResult,
openHecConnectDialog,
+ HecConnectDialogResultStatus,
} from "./connect-dialog-hec.component";
beforeAll(() => {
@@ -57,6 +58,7 @@ describe("ConnectDialogHecComponent", () => {
let component: ConnectHecDialogComponent;
let fixture: ComponentFixture;
let dialogRefMock = mock>();
+ const dialogService = mock();
const mockI18nService = mock();
const integrationMock: Integration = {
@@ -72,13 +74,6 @@ describe("ConnectDialogHecComponent", () => {
} as Integration;
const connectInfo: HecConnectDialogParams = {
settings: integrationMock,
- configuration: {
- uri: "",
- scheme: "https",
- token: "",
- service: "mock-service",
- }, // Provide appropriate mock configuration if needed
- template: null, // Provide appropriate mock template if needed
};
beforeEach(async () => {
@@ -92,8 +87,18 @@ describe("ConnectDialogHecComponent", () => {
{ provide: DialogRef, useValue: dialogRefMock },
{ provide: I18nPipe, useValue: mock() },
{ provide: I18nService, useValue: mockI18nService },
+ { provide: DialogService, useValue: dialogService },
],
}).compileComponents();
+
+ TestBed.overrideComponent(ConnectHecDialogComponent, {
+ add: {
+ providers: [{ provide: DialogService, useValue: dialogService }],
+ },
+ remove: {
+ providers: [DialogService],
+ },
+ });
});
beforeEach(() => {
@@ -163,8 +168,48 @@ describe("ConnectDialogHecComponent", () => {
bearerToken: "token",
index: "1",
service: "Test Service",
- success: true,
- error: null,
+ success: HecConnectDialogResultStatus.Edited,
+ });
+ });
+
+ describe("ConnectHecDialogComponent.delete", () => {
+ it("should call dialogService.openSimpleDialog and close dialog with delete result if confirmed", async () => {
+ // Arrange
+ const confirmed = true;
+ dialogService.openSimpleDialog.mockResolvedValue(confirmed);
+ component.formGroup.setValue({
+ url: "https://test.com",
+ bearerToken: "token",
+ index: "1",
+ service: "Test Service",
+ });
+
+ // Act
+ await component.delete();
+
+ // Assert
+ expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({
+ title: { key: "deleteItem" },
+ content: { key: "deleteItemConfirmation" },
+ type: "warning",
+ });
+ expect(dialogRefMock.close).toHaveBeenCalledWith({
+ integrationSettings: integrationMock,
+ url: "https://test.com",
+ bearerToken: "token",
+ index: "1",
+ service: "Test Service",
+ success: HecConnectDialogResultStatus.Delete,
+ });
+ });
+
+ it("should not close dialog if not confirmed", async () => {
+ dialogService.openSimpleDialog.mockResolvedValue(false);
+
+ await component.delete();
+
+ expect(dialogService.openSimpleDialog).toHaveBeenCalled();
+ expect(dialogRefMock.close).not.toHaveBeenCalled();
});
});
});
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 3593660aaaa..d397bfa83ba 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
@@ -16,10 +16,17 @@ export interface HecConnectDialogResult {
bearerToken: string;
index: string;
service: string;
- success: boolean;
- error: string | null;
+ success: HecConnectDialogResultStatusType | null;
}
+export const HecConnectDialogResultStatus = {
+ Edited: "edit",
+ Delete: "delete",
+} as const;
+
+export type HecConnectDialogResultStatusType =
+ (typeof HecConnectDialogResultStatus)[keyof typeof HecConnectDialogResultStatus];
+
@Component({
templateUrl: "./connect-dialog-hec.component.html",
imports: [SharedModule],
@@ -37,6 +44,7 @@ export class ConnectHecDialogComponent implements OnInit {
@Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams,
protected formBuilder: FormBuilder,
private dialogRef: DialogRef,
+ private dialogService: DialogService,
) {}
ngOnInit(): void {
@@ -52,6 +60,10 @@ export class ConnectHecDialogComponent implements OnInit {
return !!this.connectInfo.settings.HecConfiguration;
}
+ canDelete(): boolean {
+ return !!this.connectInfo.settings.HecConfiguration;
+ }
+
getSettingsAsJson(configuration: string) {
try {
return JSON.parse(configuration);
@@ -61,22 +73,43 @@ export class ConnectHecDialogComponent implements OnInit {
}
submit = async (): Promise => {
- const formJson = this.formGroup.getRawValue();
-
- const result: HecConnectDialogResult = {
- integrationSettings: this.connectInfo.settings,
- url: formJson.url || "",
- bearerToken: formJson.bearerToken || "",
- index: formJson.index || "",
- service: formJson.service || "",
- success: true,
- error: null,
- };
+ const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Edited);
this.dialogRef.close(result);
return;
};
+
+ delete = async (): Promise => {
+ const confirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "deleteItem" },
+ content: {
+ key: "deleteItemConfirmation",
+ },
+ type: "warning",
+ });
+
+ if (confirmed) {
+ // Perform the deletion logic here
+ const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Delete);
+ this.dialogRef.close(result);
+ }
+ };
+
+ private getHecConnectDialogResult(
+ status: HecConnectDialogResultStatusType,
+ ): HecConnectDialogResult {
+ const formJson = this.formGroup.getRawValue();
+
+ return {
+ integrationSettings: this.connectInfo.settings,
+ url: formJson.url || "",
+ bearerToken: formJson.bearerToken || "",
+ index: formJson.index || "",
+ service: formJson.service || "",
+ success: status,
+ };
+ }
}
export function openHecConnectDialog(
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 d74f0c241e2..ce6a82d3222 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
@@ -5,7 +5,9 @@ import { mock } from "jest-mock-extended";
import {
OrganizationIntegrationApiService,
OrganizationIntegrationConfigurationApiService,
+ OrganizationIntegrationConfigurationResponse,
} from "@bitwarden/bit-common/dirt/integrations";
+import { EventType } from "@bitwarden/common/enums";
import { OrganizationIntegrationService } from "./organization-integration.service";
@@ -139,4 +141,202 @@ describe("OrganizationIntegrationService", () => {
expect(result).toBeNull();
});
});
+
+ describe("deleteHecIntegrationConfiguration", () => {
+ const organizationId = "org-123" as any;
+ const integrationId = "int-1" as any;
+ const serviceName = "splunk";
+
+ let mockConfig: any;
+
+ beforeEach(() => {
+ mockConfig = { id: "conf-1", template: '{"service":"splunk"}' };
+
+ jest.resetAllMocks();
+
+ // Set up integrationConfigurations$ with a matching config/template
+ service["integrationConfigurations$"].next([
+ {
+ integrationId,
+ configurationResponses: [mockConfig],
+ },
+ ]);
+
+ jest.spyOn(service, "convertToJson").mockImplementation((json: string) => {
+ try {
+ return JSON.parse(json);
+ } catch {
+ return null;
+ }
+ });
+ });
+
+ it("should delete configuration when config and template exist", async () => {
+ mockOrgIntegrationConfigurationApiService.deleteOrganizationIntegrationConfiguration.mockResolvedValue(
+ undefined,
+ );
+
+ await service.deleteHecIntegrationConfiguration(organizationId, integrationId, serviceName);
+
+ expect(
+ mockOrgIntegrationConfigurationApiService.deleteOrganizationIntegrationConfiguration,
+ ).toHaveBeenCalledWith(organizationId, integrationId, mockConfig.id);
+
+ // The configurationResponses should be filtered (removed)
+ const updatedConfigs = service["integrationConfigurations$"].value.find(
+ (c) => c.integrationId === integrationId,
+ );
+ expect(updatedConfigs?.configurationResponses).toEqual([]);
+ });
+
+ it("should do nothing if config or template is missing", async () => {
+ // Set up with no matching config/template
+ service["integrationConfigurations$"].next([
+ {
+ integrationId,
+ configurationResponses: [
+ {
+ id: "conf-2",
+ eventType: EventType.Cipher_AttachmentCreated,
+ configuration: "",
+ template: '{"service":"other"}',
+ } as OrganizationIntegrationConfigurationResponse,
+ ],
+ },
+ ]);
+
+ await service.deleteHecIntegrationConfiguration(organizationId, integrationId, serviceName);
+
+ expect(
+ mockOrgIntegrationConfigurationApiService.deleteOrganizationIntegrationConfiguration,
+ ).not.toHaveBeenCalled();
+
+ // configurationResponses should remain unchanged
+ const configs = service["integrationConfigurations$"].value.find(
+ (c) => c.integrationId === integrationId,
+ );
+ expect(configs?.configurationResponses.length).toBe(1);
+ });
+
+ it("should do nothing if integrationConfigurations is empty", async () => {
+ service["integrationConfigurations$"].next([]);
+
+ await service.deleteHecIntegrationConfiguration(organizationId, integrationId, serviceName);
+
+ expect(
+ mockOrgIntegrationConfigurationApiService.deleteOrganizationIntegrationConfiguration,
+ ).not.toHaveBeenCalled();
+ expect(service["integrationConfigurations$"].value).toEqual([]);
+ });
+ });
+
+ describe("deleteHecIntegration", () => {
+ const organizationId = "org-123" as any;
+ const integrationId = "int-1" as any;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ service["integrations$"].next([
+ { id: integrationId, type: "Hec", configuration: "{}", service: "splunk" } as any,
+ { id: "int-2", type: "Other", configuration: "{}" } as any,
+ ]);
+ });
+
+ it("should call deleteOrganizationIntegration and remove integration from store", async () => {
+ mockOrgIntegrationApiService.deleteOrganizationIntegration.mockResolvedValue(undefined);
+
+ await service.deleteHecIntegration(organizationId, integrationId);
+
+ expect(mockOrgIntegrationApiService.deleteOrganizationIntegration).toHaveBeenCalledWith(
+ organizationId,
+ integrationId,
+ );
+ const remainingIntegrations = service["integrations$"].value;
+ expect(remainingIntegrations).toEqual([{ id: "int-2", type: "Other", configuration: "{}" }]);
+ });
+
+ it("should not throw if integration does not exist", async () => {
+ mockOrgIntegrationApiService.deleteOrganizationIntegration.mockResolvedValue(undefined);
+ service["integrations$"].next([{ id: "int-2", type: "Other", configuration: "{}" } as any]);
+
+ await expect(
+ service.deleteHecIntegration(organizationId, integrationId),
+ ).resolves.not.toThrow();
+ expect(service["integrations$"].value).toEqual([
+ { id: "int-2", type: "Other", configuration: "{}" },
+ ]);
+ });
+
+ it("should handle empty integrations list", async () => {
+ mockOrgIntegrationApiService.deleteOrganizationIntegration.mockResolvedValue(undefined);
+ service["integrations$"].next([]);
+
+ await expect(
+ service.deleteHecIntegration(organizationId, integrationId),
+ ).resolves.not.toThrow();
+ expect(service["integrations$"].value).toEqual([]);
+ });
+ });
+
+ describe("deleteHec", () => {
+ const organizationId = "org-123" as any;
+ const hecConfiguration = { service: "splunk", toString: () => '{"service":"splunk"}' } as any;
+ const hecConfigurationTemplate = {
+ service: "splunk",
+ toString: () => '{"service":"splunk"}',
+ } as any;
+ const integrationId = "int-1";
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Set up integrations$ with a matching Hec integration
+ service["integrations$"].next([
+ { id: integrationId, type: "Hec", configuration: '{"service":"splunk"}' } as any,
+ ]);
+ // Spy on getIntegration to return the expected integration
+ jest.spyOn(service as any, "getIntegration").mockReturnValue({
+ id: integrationId,
+ type: "Hec",
+ configuration: '{"service":"splunk"}',
+ });
+ });
+
+ it("should delete HEC integration configuration and then integration", async () => {
+ const deleteConfigSpy = jest
+ .spyOn(service, "deleteHecIntegrationConfiguration")
+ .mockResolvedValue(undefined);
+ const deleteIntegrationSpy = jest
+ .spyOn(service, "deleteHecIntegration")
+ .mockResolvedValue(undefined);
+
+ await service.deleteHec(organizationId, hecConfiguration, hecConfigurationTemplate);
+
+ expect(deleteConfigSpy).toHaveBeenCalledWith(
+ organizationId,
+ integrationId,
+ hecConfigurationTemplate.service,
+ );
+ expect(deleteIntegrationSpy).toHaveBeenCalledWith(organizationId, integrationId);
+ });
+
+ it("should handle errors thrown by deleteHecIntegrationConfiguration", async () => {
+ jest
+ .spyOn(service, "deleteHecIntegrationConfiguration")
+ .mockRejectedValue(new Error("Config error"));
+ jest.spyOn(service, "deleteHecIntegration").mockResolvedValue(undefined);
+
+ await expect(
+ service.deleteHec(organizationId, hecConfiguration, hecConfigurationTemplate),
+ ).rejects.toThrow("Config error");
+ });
+
+ it("should handle errors thrown by deleteHecIntegration", async () => {
+ jest.spyOn(service, "deleteHecIntegrationConfiguration").mockResolvedValue(undefined);
+ jest.spyOn(service, "deleteHecIntegration").mockRejectedValue(new Error("Integration error"));
+
+ await expect(
+ service.deleteHec(organizationId, hecConfiguration, hecConfigurationTemplate),
+ ).rejects.toThrow("Integration error");
+ });
+ });
});
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 91eaf105e2d..25bd9a94ba4 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
@@ -68,6 +68,15 @@ export class OrganizationIntegrationService {
.subscribe(([integrations, configurations]) => {
const existingIntegrations = [...this.masterIntegrationList$.value];
+ // reset - to original values
+ existingIntegrations.forEach((integration) => {
+ integration.isConnected = false;
+ integration.configuration = "";
+ integration.template = "";
+ integration.HecConfiguration = null;
+ integration.HecConfigurationTemplate = null;
+ });
+
// Update the integrations list with the fetched integrations
if (integrations && integrations.length > 0) {
integrations.forEach((integration) => {
@@ -81,7 +90,7 @@ export class OrganizationIntegrationService {
existingIntegration.configuration = integration.configuration || "";
existingIntegration.HecConfiguration = hecConfigJson;
- const template = this.getIntegrationConfiguration(
+ const { template } = this.getIntegrationConfiguration(
integration.id,
serviceName,
configurations,
@@ -199,9 +208,7 @@ export class OrganizationIntegrationService {
);
// find the existing integration
- const existingIntegration = this.integrations$.value.find(
- (i) => i.type === OrganizationIntegrationType.Hec,
- );
+ const existingIntegration = this.getIntegration(OrganizationIntegrationType.Hec);
if (existingIntegration) {
// existing integration record found, invoke update API endpoint
@@ -261,20 +268,12 @@ export class OrganizationIntegrationService {
// check if we have an existing configuration for this integration
const integrationConfigurations = this.integrationConfigurations$.value;
- // 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(
- (config) =>
- config.template &&
- this.convertToJson(config.template)?.service === service,
- )
- : null;
+ const { config } = this.getIntegrationConfiguration(
+ integrationId,
+ service,
+ integrationConfigurations,
+ );
+ const existingConfiguration = config;
if (existingConfiguration) {
// existing configuration found, invoke update API endpoint
@@ -328,13 +327,95 @@ export class OrganizationIntegrationService {
}
}
+ async deleteHec(
+ organizationId: OrganizationId,
+ integration: HecConfiguration,
+ template: HecConfigurationTemplate,
+ ) {
+ // find the existing integration
+ const existingIntegration = this.getIntegration(OrganizationIntegrationType.Hec);
+
+ if (existingIntegration === null) {
+ // couldn't find existing integration for Hec and Service name
+ return;
+ }
+
+ // drop HEC Integration Configuration first
+ await this.deleteHecIntegrationConfiguration(
+ organizationId,
+ existingIntegration.id,
+ template.service,
+ );
+
+ // drop HEC Integration
+ await this.deleteHecIntegration(organizationId, existingIntegration.id);
+ }
+
+ async deleteHecIntegration(
+ organizationId: OrganizationId,
+ integrationId: OrganizationIntegrationId,
+ ) {
+ // drop HEC Integration
+ await this.integrationApiService.deleteOrganizationIntegration(organizationId, integrationId);
+
+ const updatedIntegrations = this.integrations$.value.filter((i) => i.id !== integrationId);
+
+ this.integrations$.next(updatedIntegrations);
+ }
+
+ async deleteHecIntegrationConfiguration(
+ organizationId: OrganizationId,
+ integrationId: OrganizationIntegrationId,
+ service: string,
+ ) {
+ const integrationConfigurations = this.integrationConfigurations$.value;
+
+ const { config, template } = this.getIntegrationConfiguration(
+ integrationId,
+ service,
+ integrationConfigurations,
+ );
+
+ if (!config || !template) {
+ return;
+ }
+
+ // drop HEC Integration Configuration first
+ await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
+ organizationId,
+ integrationId,
+ config.id,
+ );
+
+ // remove the configuration from the local store
+ integrationConfigurations.forEach((integrationConfig) => {
+ if (integrationConfig.integrationId === integrationId) {
+ integrationConfig.configurationResponses = integrationConfig.configurationResponses.filter(
+ (config) => config.id !== config.id,
+ );
+ }
+ });
+
+ this.integrationConfigurations$.next(integrationConfigurations);
+ }
+
+ private getIntegration(
+ integrationType: OrganizationIntegrationType,
+ ): OrganizationIntegrationResponse | null {
+ const integrations = this.integrations$.value;
+ return integrations.find((i) => i.type === integrationType) ?? null;
+ }
+
private getIntegrationConfiguration(
integrationId: OrganizationIntegrationId,
service: string,
integrationConfigurations: OrganizationIntegrationConfigurationResponseWithIntegrationId[],
- ): HecConfigurationTemplate | null {
+ ): {
+ config: OrganizationIntegrationConfigurationResponse | null;
+ template: HecConfigurationTemplate | null;
+ } {
if (integrationConfigurations.length === 0) {
- return null;
+ return { config: null, template: null };
}
const integrationConfigs = integrationConfigurations.find(
@@ -342,17 +423,17 @@ export class OrganizationIntegrationService {
);
if (!integrationConfigs) {
- return null;
+ return { config: null, template: null };
}
for (const config of integrationConfigs.configurationResponses) {
const template = this.convertToJson(config.template || "");
if (template && template.service === service) {
- return template;
+ return { config, template };
}
}
- return null;
+ return { config: null, template: null };
}
convertToJson(jsonString?: string): T | null {