1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-23822] [PM-23823] Organization integration and configuration api services (#15763)

* Adding the organization integration api service and test cases

* Adding configuration api files and test cases. Fixing the id guids and integration type and event type nullable

* Adding get endpoint methods to the integration and config service and test cases

* fixing type check issues

* lowercase directory name
This commit is contained in:
Tom
2025-07-25 09:43:41 -04:00
committed by GitHub
parent 37987f4f97
commit b358d5663d
13 changed files with 479 additions and 0 deletions

View File

@@ -0,0 +1 @@
export * from "./services";

View File

@@ -0,0 +1,20 @@
import { EventType } from "@bitwarden/common/enums";
export class OrganizationIntegrationConfigurationRequest {
eventType?: EventType | null = null;
configuration?: string | null = null;
filters?: string | null = null;
template?: string | null = null;
constructor(
eventType?: EventType | null,
configuration?: string | null,
filters?: string | null,
template?: string | null,
) {
this.eventType = eventType;
this.configuration = configuration;
this.filters = filters;
this.template = template;
}
}

View File

@@ -0,0 +1,20 @@
import { EventType } from "@bitwarden/common/enums";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { OrganizationIntegrationConfigurationId } from "@bitwarden/common/types/guid";
export class OrganizationIntegrationConfigurationResponse extends BaseResponse {
id: OrganizationIntegrationConfigurationId;
eventType?: EventType;
configuration?: string;
filters?: string;
template?: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.eventType = this.getResponseProperty("EventType");
this.configuration = this.getResponseProperty("Configuration");
this.filters = this.getResponseProperty("Filters");
this.template = this.getResponseProperty("Template");
}
}

View File

@@ -0,0 +1,11 @@
import { OrganizationIntegrationType } from "./organization-integration-type";
export class OrganizationIntegrationRequest {
type: OrganizationIntegrationType;
configuration?: string;
constructor(integrationType: OrganizationIntegrationType, configuration?: string) {
this.type = integrationType;
this.configuration = configuration;
}
}

View File

@@ -0,0 +1,15 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { OrganizationIntegrationId } from "@bitwarden/common/types/guid";
import { OrganizationIntegrationType } from "./organization-integration-type";
export class OrganizationIntegrationResponse extends BaseResponse {
id: OrganizationIntegrationId;
organizationIntegrationType: OrganizationIntegrationType;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.organizationIntegrationType = this.getResponseProperty("Type");
}
}

View File

@@ -0,0 +1,6 @@
export const OrganizationIntegrationServiceType = Object.freeze({
CrowdStrike: "CrowdStrike",
} as const);
export type OrganizationIntegrationServiceType =
(typeof OrganizationIntegrationServiceType)[keyof typeof OrganizationIntegrationServiceType];

View File

@@ -0,0 +1,10 @@
export const OrganizationIntegrationType = Object.freeze({
CloudBillingSync: 1,
Scim: 2,
Slack: 3,
Webhook: 4,
Hec: 5,
} as const);
export type OrganizationIntegrationType =
(typeof OrganizationIntegrationType)[keyof typeof OrganizationIntegrationType];

View File

@@ -0,0 +1,2 @@
export * from "./organization-integration-api.service";
export * from "./organization-integration-configuration-api.service";

View File

@@ -0,0 +1,115 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid";
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type";
import { OrganizationIntegrationType } from "../models/organization-integration-type";
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
export const mockIntegrationResponse: any = {
id: "1" as OrganizationIntegrationId,
organizationIntegrationType: OrganizationIntegrationType.Hec,
};
export const mockIntegrationResponses: any[] = [
{
id: "1" as OrganizationIntegrationId,
OrganizationIntegrationType: OrganizationIntegrationType.Hec,
},
{
id: "2" as OrganizationIntegrationId,
OrganizationIntegrationType: OrganizationIntegrationType.Webhook,
},
];
describe("OrganizationIntegrationApiService", () => {
let service: OrganizationIntegrationApiService;
const apiService = mock<ApiService>();
beforeEach(() => {
service = new OrganizationIntegrationApiService(apiService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should call apiService.send with correct parameters for getOrganizationIntegrations", async () => {
const orgId = "org1" as OrganizationId;
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponses));
const result = await service.getOrganizationIntegrations(orgId);
expect(result).toEqual(mockIntegrationResponses);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`organizations/${orgId}/integrations`,
null,
true,
true,
);
});
it("should call apiService.send with correct parameters for createOrganizationIntegration", async () => {
const request = new OrganizationIntegrationRequest(
OrganizationIntegrationType.Hec,
`{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceType.CrowdStrike}' }`,
);
const orgId = "org1" as OrganizationId;
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse));
const result = await service.createOrganizationIntegration(orgId, request);
expect(result.organizationIntegrationType).toEqual(
mockIntegrationResponse.organizationIntegrationType,
);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`organizations/${orgId.toString()}/integrations`,
request,
true,
true,
);
});
it("should call apiService.send with the correct parameters for updateOrganizationIntegration", async () => {
const request = new OrganizationIntegrationRequest(
OrganizationIntegrationType.Hec,
`{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceType.CrowdStrike}' }`,
);
const orgId = "org1" as OrganizationId;
const integrationId = "integration1" as OrganizationIntegrationId;
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse));
const result = await service.updateOrganizationIntegration(orgId, integrationId, request);
expect(result.organizationIntegrationType).toEqual(
mockIntegrationResponse.organizationIntegrationType,
);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`organizations/${orgId}/integrations/${integrationId}`,
request,
true,
true,
);
});
it("should call apiService.send with the correct parameters for deleteOrganizationIntegration", async () => {
const orgId = "org1" as OrganizationId;
const integrationId = "integration1" as OrganizationIntegrationId;
await service.deleteOrganizationIntegration(orgId, integrationId);
expect(apiService.send).toHaveBeenCalledWith(
"DELETE",
`organizations/${orgId}/integrations/${integrationId}`,
null,
true,
false,
);
});
});

View File

@@ -0,0 +1,67 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid";
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
@Injectable()
export class OrganizationIntegrationApiService {
constructor(private apiService: ApiService) {}
async getOrganizationIntegrations(
orgId: OrganizationId,
): Promise<OrganizationIntegrationResponse[]> {
const response = await this.apiService.send(
"GET",
`organizations/${orgId}/integrations`,
null,
true,
true,
);
return response;
}
async createOrganizationIntegration(
orgId: OrganizationId,
request: OrganizationIntegrationRequest,
): Promise<OrganizationIntegrationResponse> {
const response = await this.apiService.send(
"POST",
`organizations/${orgId}/integrations`,
request,
true,
true,
);
return response;
}
async updateOrganizationIntegration(
orgId: OrganizationId,
integrationId: OrganizationIntegrationId,
request: OrganizationIntegrationRequest,
): Promise<OrganizationIntegrationResponse> {
const response = await this.apiService.send(
"PUT",
`organizations/${orgId}/integrations/${integrationId}`,
request,
true,
true,
);
return response;
}
async deleteOrganizationIntegration(
orgId: OrganizationId,
integrationId: OrganizationIntegrationId,
): Promise<any> {
await this.apiService.send(
"DELETE",
`organizations/${orgId}/integrations/${integrationId}`,
null,
true,
false,
);
}
}

View File

@@ -0,0 +1,132 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
OrganizationId,
OrganizationIntegrationId,
OrganizationIntegrationConfigurationId,
} from "@bitwarden/common/types/guid";
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
export const mockConfigurationResponse: any = {
id: "1" as OrganizationIntegrationConfigurationId,
template: "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }",
};
export const mockConfigurationResponses: any[] = [
{
id: "1" as OrganizationIntegrationConfigurationId,
template: "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }",
},
{
id: "2" as OrganizationIntegrationConfigurationId,
template: "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'otherIndex' }",
},
];
describe("OrganizationIntegrationConfigurationApiService", () => {
let service: OrganizationIntegrationConfigurationApiService;
const apiService = mock<ApiService>();
beforeEach(() => {
service = new OrganizationIntegrationConfigurationApiService(apiService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should call apiService.send with correct parameters for getOrganizationIntegrationConfigurations", async () => {
const orgId = "org1" as OrganizationId;
const integrationId = "integration1" as OrganizationIntegrationId;
apiService.send.mockReturnValue(Promise.resolve(mockConfigurationResponses));
const result = await service.getOrganizationIntegrationConfigurations(orgId, integrationId);
expect(result).toEqual(mockConfigurationResponses);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`organizations/${orgId}/integrations/${integrationId}/configurations`,
null,
true,
true,
);
});
it("should call apiService.send with correct parameters for createOrganizationIntegrationConfiguration", async () => {
const request = new OrganizationIntegrationConfigurationRequest(
null,
null,
null,
"{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }",
);
const orgId = "org1" as OrganizationId;
const integrationId = "integration1" as OrganizationIntegrationId;
apiService.send.mockReturnValue(Promise.resolve(mockConfigurationResponse));
const result = await service.createOrganizationIntegrationConfiguration(
orgId,
integrationId,
request,
);
expect(result.eventType).toEqual(mockConfigurationResponse.eventType);
expect(result.template).toEqual(mockConfigurationResponse.template);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`organizations/${orgId}/integrations/${integrationId}/configurations`,
request,
true,
true,
);
});
it("should call apiService.send with correct parameters for updateOrganizationIntegrationConfiguration", async () => {
const request = new OrganizationIntegrationConfigurationRequest(
null,
null,
null,
"{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }",
);
const orgId = "org1" as OrganizationId;
const integrationId = "integration1" as OrganizationIntegrationId;
const configurationId = "configurationId" as OrganizationIntegrationConfigurationId;
apiService.send.mockReturnValue(Promise.resolve(mockConfigurationResponse));
const result = await service.updateOrganizationIntegrationConfiguration(
orgId,
integrationId,
configurationId,
request,
);
expect(result.eventType).toEqual(mockConfigurationResponse.eventType);
expect(result.template).toEqual(mockConfigurationResponse.template);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`,
request,
true,
true,
);
});
it("should call apiService.send with correct parameters for deleteOrganizationIntegrationConfiguration", async () => {
const orgId = "org1" as OrganizationId;
const integrationId = "integration1" as OrganizationIntegrationId;
const configurationId = "configurationId" as OrganizationIntegrationConfigurationId;
await service.deleteOrganizationIntegrationConfiguration(orgId, integrationId, configurationId);
expect(apiService.send).toHaveBeenCalledWith(
"DELETE",
`organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`,
null,
true,
false,
);
});
});

View File

@@ -0,0 +1,75 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
OrganizationId,
OrganizationIntegrationConfigurationId,
OrganizationIntegrationId,
} from "@bitwarden/common/types/guid";
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
@Injectable()
export class OrganizationIntegrationConfigurationApiService {
constructor(private apiService: ApiService) {}
async getOrganizationIntegrationConfigurations(
orgId: OrganizationId,
integrationId: OrganizationIntegrationId,
): Promise<OrganizationIntegrationConfigurationResponse[]> {
const responses = await this.apiService.send(
"GET",
`organizations/${orgId}/integrations/${integrationId}/configurations`,
null,
true,
true,
);
return responses;
}
async createOrganizationIntegrationConfiguration(
orgId: OrganizationId,
integrationId: OrganizationIntegrationId,
request: OrganizationIntegrationConfigurationRequest,
): Promise<OrganizationIntegrationConfigurationResponse> {
const response = await this.apiService.send(
"POST",
`organizations/${orgId}/integrations/${integrationId}/configurations`,
request,
true,
true,
);
return response;
}
async updateOrganizationIntegrationConfiguration(
orgId: OrganizationId,
integrationId: OrganizationIntegrationId,
configurationId: OrganizationIntegrationConfigurationId,
request: OrganizationIntegrationConfigurationRequest,
): Promise<OrganizationIntegrationConfigurationResponse> {
const response = await this.apiService.send(
"PUT",
`organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`,
request,
true,
true,
);
return response;
}
async deleteOrganizationIntegrationConfiguration(
orgId: OrganizationId,
integrationId: OrganizationIntegrationId,
configurationId: OrganizationIntegrationConfigurationId,
): Promise<any> {
await this.apiService.send(
"DELETE",
`organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`,
null,
true,
false,
);
}
}

View File

@@ -15,3 +15,8 @@ export type IndexedEntityId = Opaque<string, "IndexedEntityId">;
export type SecurityTaskId = Opaque<string, "SecurityTaskId">;
export type NotificationId = Opaque<string, "NotificationId">;
export type EmergencyAccessId = Opaque<string, "EmergencyAccessId">;
export type OrganizationIntegrationId = Opaque<string, "OrganizationIntegrationId">;
export type OrganizationIntegrationConfigurationId = Opaque<
string,
"OrganizationIntegrationConfigurationId"
>;