Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -9752,6 +9752,9 @@
"failedToSaveIntegration": {
"message": "Failed to save integration. Please try again later."
},
"mustBeOrgOwnerToPerformAction": {
"message": "You must be the organization owner to perform this action."
},
"failedToDeleteIntegration": {
"message": "Failed to delete integration. Please try again later."
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs";

import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import {
OrganizationId,
OrganizationIntegrationId,
Expand All @@ -20,6 +21,11 @@ import { OrganizationIntegrationType } from "../models/organization-integration-
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";

export type HecModificationFailureReason = {
mustBeOwner: boolean;
success: boolean;
};

export class HecOrganizationIntegrationService {
private organizationId$ = new BehaviorSubject<OrganizationId | null>(null);
private _integrations$ = new BehaviorSubject<OrganizationIntegration[]>([]);
Expand All @@ -34,7 +40,7 @@ export class HecOrganizationIntegrationService {
const data$ = await this.setIntegrations(orgId);
return await firstValueFrom(data$);
} else {
return this._integrations$.getValue();
return [] as OrganizationIntegration[];
}
}),
takeUntil(this.destroy$),
Expand All @@ -56,6 +62,10 @@ export class HecOrganizationIntegrationService {
* @param orgId
*/
setOrganizationIntegrations(orgId: OrganizationId) {
if (orgId == this.organizationId$.getValue()) {
return;
}
this._integrations$.next([]);
this.organizationId$.next(orgId);
}

Expand All @@ -73,31 +83,39 @@ export class HecOrganizationIntegrationService {
url: string,
bearerToken: string,
index: string,
) {
): Promise<HecModificationFailureReason> {
if (organizationId != this.organizationId$.getValue()) {
throw new Error("Organization ID mismatch");
}

const hecConfig = new HecConfiguration(url, bearerToken, service);
const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration(
organizationId,
new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()),
);

const newTemplate = new HecTemplate(index, service);
const newIntegrationConfigResponse =
await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration(
try {
const hecConfig = new HecConfiguration(url, bearerToken, service);
const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration(
organizationId,
newIntegrationResponse.id,
new OrganizationIntegrationConfigurationRequest(null, null, null, newTemplate.toString()),
new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()),
);

const newIntegration = this.mapResponsesToOrganizationIntegration(
newIntegrationResponse,
newIntegrationConfigResponse,
);
if (newIntegration !== null) {
this._integrations$.next([...this._integrations$.getValue(), newIntegration]);
const newTemplate = new HecTemplate(index, service);
const newIntegrationConfigResponse =
await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration(
organizationId,
newIntegrationResponse.id,
new OrganizationIntegrationConfigurationRequest(null, null, null, newTemplate.toString()),
);

const newIntegration = this.mapResponsesToOrganizationIntegration(
newIntegrationResponse,
newIntegrationConfigResponse,
);
if (newIntegration !== null) {
this._integrations$.next([...this._integrations$.getValue(), newIntegration]);
}
return { mustBeOwner: false, success: true };
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 404) {
return { mustBeOwner: true, success: false };
}
throw error;
}
}

Expand All @@ -119,69 +137,87 @@ export class HecOrganizationIntegrationService {
url: string,
bearerToken: string,
index: string,
) {
): Promise<HecModificationFailureReason> {
if (organizationId != this.organizationId$.getValue()) {
throw new Error("Organization ID mismatch");
}

const hecConfig = new HecConfiguration(url, bearerToken, service);
const updatedIntegrationResponse =
await this.integrationApiService.updateOrganizationIntegration(
organizationId,
OrganizationIntegrationId,
new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()),
);

const updatedTemplate = new HecTemplate(index, service);
const updatedIntegrationConfigResponse =
await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration(
organizationId,
OrganizationIntegrationId,
OrganizationIntegrationConfigurationId,
new OrganizationIntegrationConfigurationRequest(
null,
null,
null,
updatedTemplate.toString(),
),
try {
const hecConfig = new HecConfiguration(url, bearerToken, service);
const updatedIntegrationResponse =
await this.integrationApiService.updateOrganizationIntegration(
organizationId,
OrganizationIntegrationId,
new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()),
);

const updatedTemplate = new HecTemplate(index, service);
const updatedIntegrationConfigResponse =
await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration(
organizationId,
OrganizationIntegrationId,
OrganizationIntegrationConfigurationId,
new OrganizationIntegrationConfigurationRequest(
null,
null,
null,
updatedTemplate.toString(),
),
);

const updatedIntegration = this.mapResponsesToOrganizationIntegration(
updatedIntegrationResponse,
updatedIntegrationConfigResponse,
);

const updatedIntegration = this.mapResponsesToOrganizationIntegration(
updatedIntegrationResponse,
updatedIntegrationConfigResponse,
);

if (updatedIntegration !== null) {
this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]);
if (updatedIntegration !== null) {
this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]);
}
return { mustBeOwner: false, success: true };
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 404) {
return { mustBeOwner: true, success: false };
}
throw error;
}
}

async deleteHec(
organizationId: OrganizationId,
OrganizationIntegrationId: OrganizationIntegrationId,
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
) {
): Promise<HecModificationFailureReason> {
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,
);
try {
// delete the configuration first due to foreign key constraint
await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
organizationId,
OrganizationIntegrationId,
OrganizationIntegrationConfigurationId,
);

// update the local observable
const updatedIntegrations = this._integrations$
.getValue()
.filter((i) => i.id !== OrganizationIntegrationId);
this._integrations$.next(updatedIntegrations);
// 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);

return { mustBeOwner: false, success: true };
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 404) {
return { mustBeOwner: true, success: false };
}
throw error;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ export class OrganizationIntegrationApiService {
async getOrganizationIntegrations(
orgId: OrganizationId,
): Promise<OrganizationIntegrationResponse[]> {
const response = await this.apiService.send(
"GET",
`/organizations/${orgId}/integrations`,
null,
true,
true,
);
return response;
try {
const response = await this.apiService.send(
"GET",
`/organizations/${orgId}/integrations`,
null,
true,
true,
);
return response;
} catch {
return [];
}
}

async createOrganizationIntegration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BehaviorSubject, of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
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";
Expand Down Expand Up @@ -314,7 +315,7 @@ describe("IntegrationCardComponent", () => {

jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(false);

mockIntegrationService.saveHec.mockResolvedValue(undefined);
mockIntegrationService.saveHec.mockResolvedValue({ mustBeOwner: false, success: true });

await component.setupConnection();

Expand All @@ -340,7 +341,7 @@ describe("IntegrationCardComponent", () => {
}),
});

mockIntegrationService.deleteHec.mockResolvedValue(undefined);
mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true });

await component.setupConnection();

Expand Down Expand Up @@ -368,7 +369,7 @@ describe("IntegrationCardComponent", () => {
}),
});

mockIntegrationService.deleteHec.mockResolvedValue(undefined);
mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true });

await component.setupConnection();

Expand Down Expand Up @@ -407,6 +408,52 @@ describe("IntegrationCardComponent", () => {
});
});

it("should show mustBeOwner toast on error while inserting data", async () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: HecConnectDialogResultStatus.Edited,
url: "test-url",
bearerToken: "token",
index: "index",
}),
});

jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404));

await component.setupConnection();

expect(mockIntegrationService.updateHec).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
message: mockI18nService.t("mustBeOrgOwnerToPerformAction"),
});
});

it("should show mustBeOwner toast on error while updating data", async () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
success: HecConnectDialogResultStatus.Edited,
url: "test-url",
bearerToken: "token",
index: "index",
}),
});

jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404));

await component.setupConnection();

expect(mockIntegrationService.updateHec).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
message: mockI18nService.t("mustBeOrgOwnerToPerformAction"),
});
});

it("should show toast on error while deleting", async () => {
(openHecConnectDialog as jest.Mock).mockReturnValue({
closed: of({
Expand All @@ -429,5 +476,28 @@ describe("IntegrationCardComponent", () => {
message: mockI18nService.t("failedToDeleteIntegration"),
});
});

it("should show mustbeOwner toast on 404 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 ErrorResponse("Not Found", 404));

await component.setupConnection();

expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: "",
message: mockI18nService.t("mustBeOrgOwnerToPerformAction"),
});
});
});
});
Loading
Loading