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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ export type Integration = {
|
||||
*/
|
||||
newBadgeExpiration?: string;
|
||||
description?: string;
|
||||
isConnected?: boolean;
|
||||
canSetupConnection?: boolean;
|
||||
configuration?: string;
|
||||
template?: string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user