mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
PM-26015 Datadog integration card (#16559)
* PM-26015 adding Datadog integration card * PM-26015 removing 2 changes * PM-26015 Removing 1 change * PM-26015 adding datadog integration card * PM-26015 fixing code to accept new toast owner changes * PM-26015 fixing linting error * PM-26015 fixing pr comment
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { OrganizationIntegrationServiceType } from "../organization-integration-service-type";
|
||||
|
||||
export class DatadogConfiguration {
|
||||
uri: string;
|
||||
apiKey: string;
|
||||
service: OrganizationIntegrationServiceType;
|
||||
|
||||
constructor(uri: string, apiKey: string, service: string) {
|
||||
this.uri = uri;
|
||||
this.apiKey = apiKey;
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { OrganizationIntegrationServiceType } from "../../organization-integration-service-type";
|
||||
|
||||
export class DatadogTemplate {
|
||||
source_type_name = "Bitwarden";
|
||||
title: string = "#Title#";
|
||||
text: string =
|
||||
"ActingUser: #ActingUserId#\nUser: #UserId#\nEvent: #Type#\nOrganization: #OrganizationId#\nPolicyId: #PolicyId#\nIpAddress: #IpAddress#\nDomainName: #DomainName#\nCipherId: #CipherId#\n";
|
||||
service: OrganizationIntegrationServiceType;
|
||||
|
||||
constructor(service: string) {
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
|
||||
import { OrganizationIntegration } from "./organization-integration";
|
||||
import { OrganizationIntegrationType } from "./organization-integration-type";
|
||||
|
||||
/** Integration or SDK */
|
||||
export type Integration = {
|
||||
@@ -23,6 +24,7 @@ export type Integration = {
|
||||
canSetupConnection?: boolean;
|
||||
configuration?: string;
|
||||
template?: string;
|
||||
integrationType?: OrganizationIntegrationType | null;
|
||||
|
||||
// OrganizationIntegration
|
||||
organizationIntegration?: OrganizationIntegration | null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogTemplate } from "./integration-configuration-config/configuration-template/datadog-template";
|
||||
import { HecTemplate } from "./integration-configuration-config/configuration-template/hec-template";
|
||||
import { WebhookTemplate } from "./integration-configuration-config/configuration-template/webhook-template";
|
||||
import { WebhookIntegrationConfigurationConfig } from "./integration-configuration-config/webhook-integration-configuration-config";
|
||||
@@ -14,7 +15,7 @@ export class OrganizationIntegrationConfiguration {
|
||||
eventType?: EventType | null;
|
||||
configuration?: WebhookIntegrationConfigurationConfig | null;
|
||||
filters?: string;
|
||||
template?: HecTemplate | WebhookTemplate | null;
|
||||
template?: HecTemplate | WebhookTemplate | DatadogTemplate | null;
|
||||
|
||||
constructor(
|
||||
id: OrganizationIntegrationConfigurationId,
|
||||
@@ -22,7 +23,7 @@ export class OrganizationIntegrationConfiguration {
|
||||
eventType?: EventType | null,
|
||||
configuration?: WebhookIntegrationConfigurationConfig | null,
|
||||
filters?: string,
|
||||
template?: HecTemplate | WebhookTemplate | null,
|
||||
template?: HecTemplate | WebhookTemplate | DatadogTemplate | null,
|
||||
) {
|
||||
this.id = id;
|
||||
this.integrationId = integrationId;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const OrganizationIntegrationServiceType = Object.freeze({
|
||||
CrowdStrike: "CrowdStrike",
|
||||
Datadog: "Datadog",
|
||||
} as const);
|
||||
|
||||
export type OrganizationIntegrationServiceType =
|
||||
|
||||
@@ -4,6 +4,7 @@ export const OrganizationIntegrationType = Object.freeze({
|
||||
Slack: 3,
|
||||
Webhook: 4,
|
||||
Hec: 5,
|
||||
Datadog: 6,
|
||||
} as const);
|
||||
|
||||
export type OrganizationIntegrationType =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { OrganizationIntegrationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogConfiguration } from "./configuration/datadog-configuration";
|
||||
import { HecConfiguration } from "./configuration/hec-configuration";
|
||||
import { WebhookConfiguration } from "./configuration/webhook-configuration";
|
||||
import { OrganizationIntegrationConfiguration } from "./organization-integration-configuration";
|
||||
@@ -10,14 +11,14 @@ export class OrganizationIntegration {
|
||||
id: OrganizationIntegrationId;
|
||||
type: OrganizationIntegrationType;
|
||||
serviceType: OrganizationIntegrationServiceType;
|
||||
configuration: HecConfiguration | WebhookConfiguration | null;
|
||||
configuration: HecConfiguration | WebhookConfiguration | DatadogConfiguration | null;
|
||||
integrationConfiguration: OrganizationIntegrationConfiguration[] = [];
|
||||
|
||||
constructor(
|
||||
id: OrganizationIntegrationId,
|
||||
type: OrganizationIntegrationType,
|
||||
serviceType: OrganizationIntegrationServiceType,
|
||||
configuration: HecConfiguration | WebhookConfiguration | null,
|
||||
configuration: HecConfiguration | WebhookConfiguration | DatadogConfiguration | null,
|
||||
integrationConfiguration: OrganizationIntegrationConfiguration[] = [],
|
||||
) {
|
||||
this.id = id;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogConfiguration } from "../models/configuration/datadog-configuration";
|
||||
import { DatadogTemplate } from "../models/integration-configuration-config/configuration-template/datadog-template";
|
||||
import { OrganizationIntegration } from "../models/organization-integration";
|
||||
import { OrganizationIntegrationConfiguration } from "../models/organization-integration-configuration";
|
||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
|
||||
import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "../models/organization-integration-type";
|
||||
|
||||
import { DatadogOrganizationIntegrationService } from "./datadog-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
|
||||
|
||||
describe("DatadogOrganizationIntegrationService", () => {
|
||||
let service: DatadogOrganizationIntegrationService;
|
||||
const mockIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
const mockIntegrationConfigurationApiService =
|
||||
mock<OrganizationIntegrationConfigurationApiService>();
|
||||
const organizationId = "org-1" as OrganizationId;
|
||||
const integrationId = "int-1" as OrganizationIntegrationId;
|
||||
const configId = "conf-1" as OrganizationIntegrationConfigurationId;
|
||||
const serviceType = OrganizationIntegrationServiceType.CrowdStrike;
|
||||
const url = "https://example.com";
|
||||
const apiKey = "token";
|
||||
|
||||
beforeEach(() => {
|
||||
service = new DatadogOrganizationIntegrationService(
|
||||
mockIntegrationApiService,
|
||||
mockIntegrationConfigurationApiService,
|
||||
);
|
||||
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should set organization integrations", (done) => {
|
||||
mockIntegrationApiService.getOrganizationIntegrations.mockResolvedValue([]);
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
const subscription = service.integrations$.subscribe((integrations) => {
|
||||
expect(integrations).toEqual([]);
|
||||
subscription.unsubscribe();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should save a new Datadog integration", async () => {
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
|
||||
const integrationResponse = {
|
||||
id: integrationId,
|
||||
type: OrganizationIntegrationType.Datadog,
|
||||
configuration: JSON.stringify({ url, apiKey, service: serviceType }),
|
||||
} as OrganizationIntegrationResponse;
|
||||
|
||||
const configResponse = {
|
||||
id: configId,
|
||||
template: JSON.stringify({ service: serviceType }),
|
||||
} as OrganizationIntegrationConfigurationResponse;
|
||||
|
||||
mockIntegrationApiService.createOrganizationIntegration.mockResolvedValue(integrationResponse);
|
||||
mockIntegrationConfigurationApiService.createOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
|
||||
await service.saveDatadog(organizationId, serviceType, url, apiKey);
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations.length).toBe(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
expect(integrations[0].serviceType).toBe(serviceType);
|
||||
});
|
||||
|
||||
it("should throw error on organization ID mismatch in saveDatadog", async () => {
|
||||
service.setOrganizationIntegrations("other-org" as OrganizationId);
|
||||
await expect(service.saveDatadog(organizationId, serviceType, url, apiKey)).rejects.toThrow(
|
||||
Error("Organization ID mismatch"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should update an existing Datadog integration", async () => {
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
|
||||
const integrationResponse = {
|
||||
id: integrationId,
|
||||
type: OrganizationIntegrationType.Datadog,
|
||||
configuration: JSON.stringify({ url, apiKey, service: serviceType }),
|
||||
} as OrganizationIntegrationResponse;
|
||||
|
||||
const configResponse = {
|
||||
id: configId,
|
||||
template: JSON.stringify({ service: serviceType }),
|
||||
} as OrganizationIntegrationConfigurationResponse;
|
||||
|
||||
mockIntegrationApiService.updateOrganizationIntegration.mockResolvedValue(integrationResponse);
|
||||
mockIntegrationConfigurationApiService.updateOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
|
||||
await service.updateDatadog(organizationId, integrationId, configId, serviceType, url, apiKey);
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations.length).toBe(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
});
|
||||
|
||||
it("should throw error on organization ID mismatch in updateDatadog", async () => {
|
||||
service.setOrganizationIntegrations("other-org" as OrganizationId);
|
||||
await expect(
|
||||
service.updateDatadog(organizationId, integrationId, configId, serviceType, url, apiKey),
|
||||
).rejects.toThrow(Error("Organization ID mismatch"));
|
||||
});
|
||||
|
||||
it("should get integration by id", async () => {
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
serviceType,
|
||||
{} as DatadogConfiguration,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
const integration = await service.getIntegrationById(integrationId);
|
||||
expect(integration).not.toBeNull();
|
||||
expect(integration!.id).toBe(integrationId);
|
||||
});
|
||||
|
||||
it("should get integration by service type", async () => {
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
serviceType,
|
||||
{} as DatadogConfiguration,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
const integration = await service.getIntegrationByServiceType(serviceType);
|
||||
expect(integration).not.toBeNull();
|
||||
expect(integration!.serviceType).toBe(serviceType);
|
||||
});
|
||||
|
||||
it("should get integration configurations", async () => {
|
||||
const config = new OrganizationIntegrationConfiguration(
|
||||
configId,
|
||||
integrationId,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
{} as DatadogTemplate,
|
||||
);
|
||||
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
serviceType,
|
||||
{} as DatadogConfiguration,
|
||||
[config],
|
||||
),
|
||||
]);
|
||||
const configs = await service.getIntegrationConfigurations(integrationId);
|
||||
expect(configs).not.toBeNull();
|
||||
expect(configs![0].id).toBe(configId);
|
||||
});
|
||||
|
||||
it("convertToJson should parse valid JSON", () => {
|
||||
const obj = service.convertToJson<{ a: number }>('{"a":1}');
|
||||
expect(obj).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it("convertToJson should return null for invalid JSON", () => {
|
||||
const obj = service.convertToJson<{ a: number }>("invalid");
|
||||
expect(obj).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,350 @@
|
||||
import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogConfiguration } from "../models/configuration/datadog-configuration";
|
||||
import { DatadogTemplate } from "../models/integration-configuration-config/configuration-template/datadog-template";
|
||||
import { OrganizationIntegration } from "../models/organization-integration";
|
||||
import { OrganizationIntegrationConfiguration } from "../models/organization-integration-configuration";
|
||||
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
|
||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
|
||||
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
|
||||
import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "../models/organization-integration-type";
|
||||
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
|
||||
|
||||
export type DatadogModificationFailureReason = {
|
||||
mustBeOwner: boolean;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export class DatadogOrganizationIntegrationService {
|
||||
private organizationId$ = new BehaviorSubject<OrganizationId | null>(null);
|
||||
private _integrations$ = new BehaviorSubject<OrganizationIntegration[]>([]);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
integrations$ = this._integrations$.asObservable();
|
||||
|
||||
private fetch$ = this.organizationId$
|
||||
.pipe(
|
||||
switchMap(async (orgId) => {
|
||||
if (orgId) {
|
||||
const data$ = await this.setIntegrations(orgId);
|
||||
return await firstValueFrom(data$);
|
||||
} else {
|
||||
return this._integrations$.getValue();
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe({
|
||||
next: (integrations) => {
|
||||
this._integrations$.next(integrations);
|
||||
},
|
||||
});
|
||||
|
||||
constructor(
|
||||
private integrationApiService: OrganizationIntegrationApiService,
|
||||
private integrationConfigurationApiService: OrganizationIntegrationConfigurationApiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets the organization Id and will trigger the retrieval of the
|
||||
* integrations for a given org.
|
||||
* @param orgId
|
||||
*/
|
||||
setOrganizationIntegrations(orgId: OrganizationId) {
|
||||
this.organizationId$.next(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new organization integration and updates the integrations$ observable
|
||||
* @param organizationId id of the organization
|
||||
* @param service service type of the integration
|
||||
* @param url url of the service
|
||||
* @param apiKey api token
|
||||
*/
|
||||
async saveDatadog(
|
||||
organizationId: OrganizationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
apiKey: string,
|
||||
): Promise<DatadogModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
const datadogConfig = new DatadogConfiguration(url, apiKey, service);
|
||||
const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration(
|
||||
organizationId,
|
||||
new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
datadogConfig.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
const newTemplate = new DatadogTemplate(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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing organization integration and updates the integrations$ observable
|
||||
* @param organizationId id of the organization
|
||||
* @param OrganizationIntegrationId id of the organization integration
|
||||
* @param OrganizationIntegrationConfigurationId id of the organization integration configuration
|
||||
* @param service service type of the integration
|
||||
* @param url url of the service
|
||||
* @param apiKey api token
|
||||
*/
|
||||
async updateDatadog(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
apiKey: string,
|
||||
): Promise<DatadogModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
const datadogConfig = new DatadogConfiguration(url, apiKey, service);
|
||||
const updatedIntegrationResponse =
|
||||
await this.integrationApiService.updateOrganizationIntegration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
datadogConfig.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
const updatedTemplate = new DatadogTemplate(service);
|
||||
const updatedIntegrationConfigResponse =
|
||||
await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
new OrganizationIntegrationConfigurationRequest(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
updatedTemplate.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
const updatedIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
updatedIntegrationResponse,
|
||||
updatedIntegrationConfigResponse,
|
||||
);
|
||||
|
||||
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 deleteDatadog(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
): Promise<DatadogModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
// 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);
|
||||
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for an OrganizationIntegrationId
|
||||
* @param integrationId id of the integration
|
||||
* @returns OrganizationIntegration or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationById(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
): Promise<OrganizationIntegration | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => integrations.find((i) => i.id === integrationId) || null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for a service type
|
||||
* @param serviceType type of the service
|
||||
* @returns OrganizationIntegration or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationByServiceType(
|
||||
serviceType: OrganizationIntegrationServiceType,
|
||||
): Promise<OrganizationIntegration | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => integrations.find((i) => i.serviceType === serviceType) || null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegrationConfigurations for an integration ID
|
||||
* @param integrationId id of the integration
|
||||
* @returns OrganizationIntegration array or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationConfigurations(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
): Promise<OrganizationIntegrationConfiguration[] | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => {
|
||||
const integration = integrations.find((i) => i.id === integrationId);
|
||||
return integration ? integration.integrationConfiguration : null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Move to data models to be more explicit for future services
|
||||
private mapResponsesToOrganizationIntegration(
|
||||
integrationResponse: OrganizationIntegrationResponse,
|
||||
configurationResponse: OrganizationIntegrationConfigurationResponse,
|
||||
): OrganizationIntegration | null {
|
||||
const datadogConfig = this.convertToJson<DatadogConfiguration>(
|
||||
integrationResponse.configuration,
|
||||
);
|
||||
const template = this.convertToJson<DatadogTemplate>(configurationResponse.template);
|
||||
|
||||
if (!datadogConfig || !template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const integrationConfig = new OrganizationIntegrationConfiguration(
|
||||
configurationResponse.id,
|
||||
integrationResponse.id,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
template,
|
||||
);
|
||||
|
||||
return new OrganizationIntegration(
|
||||
integrationResponse.id,
|
||||
integrationResponse.type,
|
||||
datadogConfig.service,
|
||||
datadogConfig,
|
||||
[integrationConfig],
|
||||
);
|
||||
}
|
||||
|
||||
// Could possibly be moved to a base service. All services would then assume that the
|
||||
// integration configuration would always be an array and this datadog specific service
|
||||
// would just assume a single entry.
|
||||
private setIntegrations(orgId: OrganizationId) {
|
||||
const results$ = zip(this.integrationApiService.getOrganizationIntegrations(orgId)).pipe(
|
||||
switchMap(([responses]) => {
|
||||
const integrations: OrganizationIntegration[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
responses.forEach((integration) => {
|
||||
if (integration.type === OrganizationIntegrationType.Datadog) {
|
||||
const promise = this.integrationConfigurationApiService
|
||||
.getOrganizationIntegrationConfigurations(orgId, integration.id)
|
||||
.then((response) => {
|
||||
// datadog events will only have one OrganizationIntegrationConfiguration
|
||||
const config = response[0];
|
||||
|
||||
const orgIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
integration,
|
||||
config,
|
||||
);
|
||||
|
||||
if (orgIntegration !== null) {
|
||||
integrations.push(orgIntegration);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises).then(() => {
|
||||
return integrations;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return results$;
|
||||
}
|
||||
|
||||
// TODO: Move to base service when necessary
|
||||
convertToJson<T>(jsonString?: string): T | null {
|
||||
try {
|
||||
return JSON.parse(jsonString || "") as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,22 +311,24 @@ export class HecOrganizationIntegrationService {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
responses.forEach((integration) => {
|
||||
const promise = this.integrationConfigurationApiService
|
||||
.getOrganizationIntegrationConfigurations(orgId, integration.id)
|
||||
.then((response) => {
|
||||
// Hec events will only have one OrganizationIntegrationConfiguration
|
||||
const config = response[0];
|
||||
if (integration.type === OrganizationIntegrationType.Hec) {
|
||||
const promise = this.integrationConfigurationApiService
|
||||
.getOrganizationIntegrationConfigurations(orgId, integration.id)
|
||||
.then((response) => {
|
||||
// Hec events will only have one OrganizationIntegrationConfiguration
|
||||
const config = response[0];
|
||||
|
||||
const orgIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
integration,
|
||||
config,
|
||||
);
|
||||
const orgIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
integration,
|
||||
config,
|
||||
);
|
||||
|
||||
if (orgIntegration !== null) {
|
||||
integrations.push(orgIntegration);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
if (orgIntegration !== null) {
|
||||
integrations.push(orgIntegration);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises).then(() => {
|
||||
return integrations;
|
||||
|
||||
@@ -5,6 +5,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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
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";
|
||||
@@ -29,6 +30,7 @@ describe("IntegrationCardComponent", () => {
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const activatedRoute = mock<ActivatedRoute>();
|
||||
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||
const mockDatadogIntegrationService = mock<DatadogOrganizationIntegrationService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const toastService = mock<ToastService>();
|
||||
|
||||
@@ -53,6 +55,7 @@ describe("IntegrationCardComponent", () => {
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
],
|
||||
|
||||
@@ -13,17 +13,22 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
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";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import {
|
||||
HecConnectDialogResult,
|
||||
DatadogConnectDialogResult,
|
||||
HecConnectDialogResultStatus,
|
||||
DatadogConnectDialogResultStatus,
|
||||
openDatadogConnectDialog,
|
||||
openHecConnectDialog,
|
||||
} from "../integration-dialog/index";
|
||||
|
||||
@@ -64,6 +69,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
private dialogService: DialogService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||
private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
@@ -131,42 +137,87 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
async setupConnection() {
|
||||
// invoke the dialog to connect the integration
|
||||
const dialog = openHecConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
});
|
||||
let dialog: DialogRef<DatadogConnectDialogResult | HecConnectDialogResult, unknown>;
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
// the dialog was cancelled
|
||||
if (!result || !result.success) {
|
||||
if (this.integrationSettings?.integrationType === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Delete) {
|
||||
await this.deleteHec();
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToDeleteIntegration"),
|
||||
if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) {
|
||||
dialog = openDatadogConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Edited) {
|
||||
await this.saveHec(result);
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
// the dialog was cancelled
|
||||
if (!result || !result.success) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Delete) {
|
||||
await this.deleteDatadog();
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToDeleteIntegration"),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.success === DatadogConnectDialogResultStatus.Edited) {
|
||||
await this.saveDatadog(result as DatadogConnectDialogResult);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// invoke the dialog to connect the integration
|
||||
dialog = openHecConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
// the dialog was cancelled
|
||||
if (!result || !result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Delete) {
|
||||
await this.deleteHec();
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToDeleteIntegration"),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Edited) {
|
||||
await this.saveHec(result as HecConnectDialogResult);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +293,69 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
async saveDatadog(result: DatadogConnectDialogResult) {
|
||||
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.datadogOrganizationIntegrationService.updateDatadog(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.apiKey,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
await this.datadogOrganizationIntegrationService.saveDatadog(
|
||||
this.organizationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.apiKey,
|
||||
);
|
||||
}
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDatadog() {
|
||||
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");
|
||||
}
|
||||
|
||||
const response = await this.datadogOrganizationIntegrationService.deleteDatadog(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
);
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
}
|
||||
|
||||
private showMustBeOwnerToast() {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
|
||||
</span>
|
||||
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
|
||||
@if (loading) {
|
||||
<ng-container #spinner>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
}
|
||||
@if (!loading) {
|
||||
<ng-container>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "url" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="url"
|
||||
placeholder="https://api.<region>.datadoghq.com"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="apiKey" />
|
||||
<bit-hint>{{ "apiKey" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||
@if (isUpdateAvailable) {
|
||||
{{ "update" | i18n }}
|
||||
} @else {
|
||||
{{ "save" | i18n }}
|
||||
}
|
||||
</button>
|
||||
<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>
|
||||
@@ -0,0 +1,171 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import {
|
||||
ConnectDatadogDialogComponent,
|
||||
DatadogConnectDialogParams,
|
||||
DatadogConnectDialogResult,
|
||||
DatadogConnectDialogResultStatus,
|
||||
openDatadogConnectDialog,
|
||||
} from "./connect-dialog-datadog.component";
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock element.animate for jsdom
|
||||
// the animate function is not available in jsdom, so we provide a mock implementation
|
||||
// This is necessary for tests that rely on animations
|
||||
// This mock does not perform any actual animations, it just provides a structure that allows tests
|
||||
// to run without throwing errors related to missing animate function
|
||||
if (!HTMLElement.prototype.animate) {
|
||||
HTMLElement.prototype.animate = function () {
|
||||
return {
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
finish: () => {},
|
||||
cancel: () => {},
|
||||
reverse: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
onfinish: null,
|
||||
oncancel: null,
|
||||
startTime: 0,
|
||||
currentTime: 0,
|
||||
playbackRate: 1,
|
||||
playState: "idle",
|
||||
replaceState: "active",
|
||||
effect: null,
|
||||
finished: Promise.resolve(),
|
||||
id: "",
|
||||
remove: () => {},
|
||||
timeline: null,
|
||||
ready: Promise.resolve(),
|
||||
} as unknown as Animation;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe("ConnectDialogDatadogComponent", () => {
|
||||
let component: ConnectDatadogDialogComponent;
|
||||
let fixture: ComponentFixture<ConnectDatadogDialogComponent>;
|
||||
let dialogRefMock = mock<DialogRef<DatadogConnectDialogResult>>();
|
||||
const mockI18nService = mock<I18nService>();
|
||||
|
||||
const integrationMock: Integration = {
|
||||
name: "Test Integration",
|
||||
image: "test-image.png",
|
||||
linkURL: "https://example.com",
|
||||
imageDarkMode: "test-image-dark.png",
|
||||
newBadgeExpiration: "2024-12-31",
|
||||
description: "Test Description",
|
||||
canSetupConnection: true,
|
||||
type: IntegrationType.EVENT,
|
||||
} as Integration;
|
||||
const connectInfo: DatadogConnectDialogParams = {
|
||||
settings: integrationMock, // Provide appropriate mock template if needed
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRefMock = mock<DialogRef<DatadogConnectDialogResult>>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
{ provide: DIALOG_DATA, useValue: connectInfo },
|
||||
{ provide: DialogRef, useValue: dialogRefMock },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConnectDatadogDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
mockI18nService.t.mockImplementation((key) => key);
|
||||
});
|
||||
|
||||
it("should create the component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize form with empty values", () => {
|
||||
expect(component.formGroup.value).toEqual({
|
||||
url: "",
|
||||
apiKey: "",
|
||||
service: "Test Integration",
|
||||
});
|
||||
});
|
||||
|
||||
it("should have required validators for all fields", () => {
|
||||
component.formGroup.setValue({ url: "", apiKey: "", service: "" });
|
||||
expect(component.formGroup.valid).toBeFalsy();
|
||||
|
||||
component.formGroup.setValue({
|
||||
url: "https://test.com",
|
||||
apiKey: "token",
|
||||
service: "Test Service",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should test url is at least 7 characters long", () => {
|
||||
component.formGroup.setValue({
|
||||
url: "test",
|
||||
apiKey: "token",
|
||||
service: "Test Service",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeFalsy();
|
||||
|
||||
component.formGroup.setValue({
|
||||
url: "https://test.com",
|
||||
apiKey: "token",
|
||||
service: "Test Service",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should call dialogRef.close with correct result on submit", async () => {
|
||||
component.formGroup.setValue({
|
||||
url: "https://test.com",
|
||||
apiKey: "token",
|
||||
service: "Test Service",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
integrationSettings: integrationMock,
|
||||
url: "https://test.com",
|
||||
apiKey: "token",
|
||||
service: "Test Service",
|
||||
success: DatadogConnectDialogResultStatus.Edited,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("openDatadogConnectDialog", () => {
|
||||
it("should call dialogService.open with correct params", () => {
|
||||
const dialogServiceMock = mock<DialogService>();
|
||||
const config: DialogConfig<
|
||||
DatadogConnectDialogParams,
|
||||
DialogRef<DatadogConnectDialogResult>
|
||||
> = {
|
||||
data: { settings: { name: "Test" } as Integration },
|
||||
} as any;
|
||||
|
||||
openDatadogConnectDialog(dialogServiceMock, config);
|
||||
|
||||
expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectDatadogDialogComponent, config);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { DatadogConfiguration } from "@bitwarden/bit-common/dirt/organization-integrations/models/configuration/datadog-configuration";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
export type DatadogConnectDialogParams = {
|
||||
settings: Integration;
|
||||
};
|
||||
|
||||
export interface DatadogConnectDialogResult {
|
||||
integrationSettings: Integration;
|
||||
url: string;
|
||||
apiKey: string;
|
||||
service: string;
|
||||
success: DatadogConnectDialogResultStatusType | null;
|
||||
}
|
||||
|
||||
export const DatadogConnectDialogResultStatus = {
|
||||
Edited: "edit",
|
||||
Delete: "delete",
|
||||
} as const;
|
||||
|
||||
export type DatadogConnectDialogResultStatusType =
|
||||
(typeof DatadogConnectDialogResultStatus)[keyof typeof DatadogConnectDialogResultStatus];
|
||||
|
||||
@Component({
|
||||
templateUrl: "./connect-dialog-datadog.component.html",
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class ConnectDatadogDialogComponent implements OnInit {
|
||||
loading = false;
|
||||
datadogConfig: DatadogConfiguration | null = null;
|
||||
hecTemplate: HecTemplate | null = null;
|
||||
formGroup = this.formBuilder.group({
|
||||
url: ["", [Validators.required, Validators.minLength(7)]],
|
||||
apiKey: ["", Validators.required],
|
||||
service: ["", Validators.required],
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected connectInfo: DatadogConnectDialogParams,
|
||||
protected formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<DatadogConnectDialogResult>,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.datadogConfig =
|
||||
this.connectInfo.settings.organizationIntegration?.getConfiguration<DatadogConfiguration>() ??
|
||||
null;
|
||||
this.hecTemplate =
|
||||
this.connectInfo.settings.organizationIntegration?.integrationConfiguration?.[0]?.getTemplate<HecTemplate>() ??
|
||||
null;
|
||||
|
||||
this.formGroup.patchValue({
|
||||
url: this.datadogConfig?.uri || "",
|
||||
apiKey: this.datadogConfig?.apiKey || "",
|
||||
service: this.connectInfo.settings.name,
|
||||
});
|
||||
}
|
||||
|
||||
get isUpdateAvailable(): boolean {
|
||||
return !!this.datadogConfig;
|
||||
}
|
||||
|
||||
get canDelete(): boolean {
|
||||
return !!this.datadogConfig;
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.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.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Delete);
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
};
|
||||
|
||||
private getDatadogConnectDialogResult(
|
||||
status: DatadogConnectDialogResultStatusType,
|
||||
): DatadogConnectDialogResult {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
|
||||
return {
|
||||
integrationSettings: this.connectInfo.settings,
|
||||
url: formJson.url || "",
|
||||
apiKey: formJson.apiKey || "",
|
||||
service: formJson.service || "",
|
||||
success: status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function openDatadogConnectDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<DatadogConnectDialogParams, DialogRef<DatadogConnectDialogResult>>,
|
||||
) {
|
||||
return dialogService.open<DatadogConnectDialogResult>(ConnectDatadogDialogComponent, config);
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./connect-dialog/connect-dialog-hec.component";
|
||||
export * from "./connect-dialog/connect-dialog-datadog.component";
|
||||
|
||||
@@ -6,6 +6,7 @@ import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -24,6 +25,7 @@ describe("IntegrationGridComponent", () => {
|
||||
let fixture: ComponentFixture<IntegrationGridComponent>;
|
||||
const mockActivatedRoute = mock<ActivatedRoute>();
|
||||
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||
const mockDatadogIntegrationService = mock<DatadogOrganizationIntegrationService>();
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
name: "Integration 1",
|
||||
@@ -70,6 +72,7 @@ describe("IntegrationGridComponent", () => {
|
||||
useValue: mockActivatedRoute,
|
||||
},
|
||||
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: mock<ToastService>(),
|
||||
|
||||
@@ -4,6 +4,8 @@ import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile }
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -226,6 +228,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
// Sets the organization ID which also loads the integrations$
|
||||
this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => {
|
||||
this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id);
|
||||
this.datadogOrganizationIntegrationService.setOrganizationIntegrations(org.id);
|
||||
});
|
||||
|
||||
// For all existing event based configurations loop through and assign the
|
||||
@@ -253,6 +256,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||
private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
|
||||
) {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
|
||||
@@ -270,10 +274,62 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
type: IntegrationType.EVENT,
|
||||
description: "crowdstrikeEventIntegrationDesc",
|
||||
canSetupConnection: true,
|
||||
integrationType: 5, // Assuming 5 corresponds to CrowdStrike in OrganizationIntegrationType
|
||||
};
|
||||
|
||||
this.integrationsList.push(crowdstrikeIntegration);
|
||||
|
||||
const datadogIntegration: Integration = {
|
||||
name: OrganizationIntegrationServiceType.Datadog,
|
||||
// TODO: Update link when help article is published
|
||||
linkURL: "",
|
||||
image: "../../../../../../../images/integrations/logo-datadog-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
description: "datadogEventIntegrationDesc",
|
||||
canSetupConnection: true,
|
||||
integrationType: 6, // Assuming 6 corresponds to Datadog in OrganizationIntegrationType
|
||||
};
|
||||
|
||||
this.integrationsList.push(datadogIntegration);
|
||||
}
|
||||
|
||||
// For all existing event based configurations loop through and assign the
|
||||
// organizationIntegration for the correct services.
|
||||
this.hecOrganizationIntegrationService.integrations$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// reset all integrations to null first - in case one was deleted
|
||||
this.integrationsList.forEach((i) => {
|
||||
if (i.integrationType === OrganizationIntegrationType.Hec) {
|
||||
i.organizationIntegration = null;
|
||||
}
|
||||
});
|
||||
|
||||
integrations.map((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
|
||||
if (item) {
|
||||
item.organizationIntegration = integration;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.datadogOrganizationIntegrationService.integrations$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// reset all integrations to null first - in case one was deleted
|
||||
this.integrationsList.forEach((i) => {
|
||||
if (i.integrationType === OrganizationIntegrationType.Datadog) {
|
||||
i.organizationIntegration = null;
|
||||
}
|
||||
});
|
||||
|
||||
integrations.map((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
|
||||
if (item) {
|
||||
item.organizationIntegration = integration;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
|
||||
@@ -12,6 +13,11 @@ import { OrganizationIntegrationsRoutingModule } from "./organization-integratio
|
||||
@NgModule({
|
||||
imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: DatadogOrganizationIntegrationService,
|
||||
useClass: DatadogOrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: HecOrganizationIntegrationService,
|
||||
useClass: HecOrganizationIntegrationService,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {} from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
@@ -37,6 +38,7 @@ class MockNewMenuComponent {}
|
||||
describe("IntegrationsComponent", () => {
|
||||
let fixture: ComponentFixture<IntegrationsComponent>;
|
||||
const hecOrgIntegrationSvc = mock<HecOrganizationIntegrationService>();
|
||||
const datadogOrgIntegrationSvc = mock<DatadogOrganizationIntegrationService>();
|
||||
|
||||
const activatedRouteMock = {
|
||||
snapshot: { paramMap: { get: jest.fn() } },
|
||||
@@ -55,6 +57,7 @@ describe("IntegrationsComponent", () => {
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc },
|
||||
{ provide: DatadogOrganizationIntegrationService, useValue: datadogOrgIntegrationSvc },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(IntegrationsComponent);
|
||||
|
||||
Reference in New Issue
Block a user