1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-24655] Delete an existing Integration (#16382)

This commit is contained in:
Vijay Oommen
2025-09-16 09:30:11 -05:00
committed by GitHub
parent befc756a02
commit 59396f0262
9 changed files with 252 additions and 50 deletions

View File

@@ -9670,6 +9670,9 @@
"failedToSaveIntegration": {
"message": "Failed to save integration. Please try again later."
},
"failedToDeleteIntegration": {
"message": "Failed to delete integration. Please try again later."
},
"deviceIdMissing": {
"message": "Device ID is missing"
},

View File

@@ -20,7 +20,6 @@ export type Integration = {
*/
newBadgeExpiration?: string;
description?: string;
isConnected?: boolean;
canSetupConnection?: boolean;
configuration?: string;
template?: string;

View File

@@ -156,6 +156,34 @@ export class HecOrganizationIntegrationService {
}
}
async deleteHec(
organizationId: OrganizationId,
OrganizationIntegrationId: OrganizationIntegrationId,
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
) {
if (organizationId != this.organizationId$.getValue()) {
throw new Error("Organization ID mismatch");
}
// delete the configuration first due to foreign key constraint
await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
organizationId,
OrganizationIntegrationId,
OrganizationIntegrationConfigurationId,
);
// delete the integration
await this.integrationApiService.deleteOrganizationIntegration(
organizationId,
OrganizationIntegrationId,
);
// update the local observable
const updatedIntegrations = this._integrations$
.getValue()
.filter((i) => i.id !== OrganizationIntegrationId);
this._integrations$.next(updatedIntegrations);
}
/**
* Gets a OrganizationIntegration for an OrganizationIntegrationId
* @param integrationId id of the integration

View File

@@ -13,12 +13,13 @@ import { DialogService, ToastService } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { openHecConnectDialog } from "../integration-dialog";
import { HecConnectDialogResultStatus, openHecConnectDialog } from "../integration-dialog";
import { IntegrationCardComponent } from "./integration-card.component";
jest.mock("../integration-dialog", () => ({
openHecConnectDialog: jest.fn(),
HecConnectDialogResultStatus: { Edited: "edit", Delete: "delete" },
}));
describe("IntegrationCardComponent", () => {
@@ -272,7 +273,7 @@ describe("IntegrationCardComponent", () => {
it("should call updateHec if isUpdateAvailable is true", async () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: true,
success: HecConnectDialogResultStatus.Edited,
url: "test-url",
bearerToken: "token",
index: "index",
@@ -304,7 +305,7 @@ describe("IntegrationCardComponent", () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: true,
success: HecConnectDialogResultStatus.Edited,
url: "test-url",
bearerToken: "token",
index: "index",
@@ -327,10 +328,66 @@ describe("IntegrationCardComponent", () => {
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
});
it("should show toast on error", async () => {
it("should call deleteHec when a delete is requested", async () => {
component.organizationId = "org-id" as any;
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: true,
success: HecConnectDialogResultStatus.Delete,
url: "test-url",
bearerToken: "token",
index: "index",
}),
});
mockIntegrationService.deleteHec.mockResolvedValue(undefined);
await component.setupConnection();
expect(mockIntegrationService.deleteHec).toHaveBeenCalledWith(
"org-id",
"integration-id",
"config-id",
);
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
});
it("should not call deleteHec if no existing configuration", async () => {
component.integrationSettings = {
organizationIntegration: null,
name: OrganizationIntegrationServiceType.CrowdStrike,
} as any;
component.organizationId = "org-id" as any;
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: HecConnectDialogResultStatus.Delete,
url: "test-url",
bearerToken: "token",
index: "index",
}),
});
mockIntegrationService.deleteHec.mockResolvedValue(undefined);
await component.setupConnection();
expect(mockIntegrationService.deleteHec).not.toHaveBeenCalledWith(
"org-id",
"integration-id",
"config-id",
OrganizationIntegrationServiceType.CrowdStrike,
"test-url",
"token",
"index",
);
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
});
it("should show toast on error while saving", async () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: HecConnectDialogResultStatus.Edited,
url: "test-url",
bearerToken: "token",
index: "index",
@@ -349,5 +406,28 @@ describe("IntegrationCardComponent", () => {
message: mockI18nService.t("failedToSaveIntegration"),
});
});
it("should show toast on error while deleting", async () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: HecConnectDialogResultStatus.Delete,
url: "test-url",
bearerToken: "token",
index: "index",
}),
});
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
mockIntegrationService.deleteHec.mockRejectedValue(new Error("fail"));
await component.setupConnection();
expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
message: mockI18nService.t("failedToDeleteIntegration"),
});
});
});
});

View File

@@ -21,7 +21,11 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { openHecConnectDialog } from "../integration-dialog/index";
import {
HecConnectDialogResult,
HecConnectDialogResultStatus,
openHecConnectDialog,
} from "../integration-dialog/index";
@Component({
selector: "app-integration-card",
@@ -142,32 +146,20 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
}
try {
if (this.isUpdateAvailable) {
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
const orgIntegrationConfigurationId =
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
if (result.success === HecConnectDialogResultStatus.Delete) {
await this.deleteHec();
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToDeleteIntegration"),
});
}
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
throw Error("Organization Integration ID or Configuration ID is missing");
}
await this.hecOrganizationIntegrationService.updateHec(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
this.integrationSettings.name as OrganizationIntegrationServiceType,
result.url,
result.bearerToken,
result.index,
);
} else {
await this.hecOrganizationIntegrationService.saveHec(
this.organizationId,
this.integrationSettings.name as OrganizationIntegrationServiceType,
result.url,
result.bearerToken,
result.index,
);
try {
if (result.success === HecConnectDialogResultStatus.Edited) {
await this.saveHec(result);
}
} catch {
this.toastService.showToast({
@@ -175,7 +167,55 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
title: "",
message: this.i18nService.t("failedToSaveIntegration"),
});
return;
}
}
async saveHec(result: HecConnectDialogResult) {
if (this.isUpdateAvailable) {
// retrieve org integration and configuration ids
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
const orgIntegrationConfigurationId =
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
throw Error("Organization Integration ID or Configuration ID is missing");
}
// update existing integration and configuration
await this.hecOrganizationIntegrationService.updateHec(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
this.integrationSettings.name as OrganizationIntegrationServiceType,
result.url,
result.bearerToken,
result.index,
);
} else {
// create new integration and configuration
await this.hecOrganizationIntegrationService.saveHec(
this.organizationId,
this.integrationSettings.name as OrganizationIntegrationServiceType,
result.url,
result.bearerToken,
result.index,
);
}
}
async deleteHec() {
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
const orgIntegrationConfigurationId =
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
throw Error("Organization Integration ID or Configuration ID is missing");
}
await this.hecOrganizationIntegrationService.deleteHec(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
);
}
}

View File

@@ -37,6 +37,19 @@
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
{{ "cancel" | i18n }}
</button>
@if (canDelete) {
<div class="tw-ml-auto">
<button
bitIconButton="bwi-trash"
type="button"
buttonType="danger"
label="'delete' | i18n"
[appA11yTitle]="'delete' | i18n"
[bitAction]="delete"
></button>
</div>
}
</ng-container>
</bit-dialog>
</form>

View File

@@ -14,6 +14,7 @@ import {
ConnectHecDialogComponent,
HecConnectDialogParams,
HecConnectDialogResult,
HecConnectDialogResultStatus,
openHecConnectDialog,
} from "./connect-dialog-hec.component";
@@ -65,7 +66,6 @@ describe("ConnectDialogHecComponent", () => {
imageDarkMode: "test-image-dark.png",
newBadgeExpiration: "2024-12-31",
description: "Test Description",
isConnected: false,
canSetupConnection: true,
type: IntegrationType.EVENT,
} as Integration;
@@ -155,8 +155,7 @@ describe("ConnectDialogHecComponent", () => {
bearerToken: "token",
index: "1",
service: "Test Service",
success: true,
error: null,
success: HecConnectDialogResultStatus.Edited,
});
});
});

View File

@@ -17,10 +17,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],
@@ -40,6 +47,7 @@ export class ConnectHecDialogComponent implements OnInit {
@Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams,
protected formBuilder: FormBuilder,
private dialogRef: DialogRef<HecConnectDialogResult>,
private dialogService: DialogService,
) {}
ngOnInit(): void {
@@ -62,23 +70,51 @@ export class ConnectHecDialogComponent implements OnInit {
return !!this.hecConfig;
}
submit = async (): Promise<void> => {
const formJson = this.formGroup.getRawValue();
get canDelete(): boolean {
return !!this.hecConfig;
}
const result: HecConnectDialogResult = {
integrationSettings: this.connectInfo.settings,
url: formJson.url || "",
bearerToken: formJson.bearerToken || "",
index: formJson.index || "",
service: formJson.service || "",
success: true,
error: null,
};
submit = async (): Promise<void> => {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Edited);
this.dialogRef.close(result);
return;
};
delete = async (): Promise<void> => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },
content: {
key: "deleteItemConfirmation",
},
type: "warning",
});
if (confirmed) {
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(

View File

@@ -253,7 +253,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
type: IntegrationType.EVENT,
description: "crowdstrikeEventIntegrationDesc",
isConnected: false,
canSetupConnection: true,
};
@@ -265,6 +264,11 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
this.hecOrganizationIntegrationService.integrations$
.pipe(takeUntil(this.destroy$))
.subscribe((integrations) => {
// reset all integrations to null first - in case one was deleted
this.integrationsList.forEach((i) => {
i.organizationIntegration = null;
});
integrations.map((integration) => {
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
if (item) {