mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-23824] Implement HEC integration (#16274)
This commit is contained in:
@@ -19,7 +19,6 @@ import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard";
|
||||
import { VaultModule } from "./collections/vault.module";
|
||||
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
|
||||
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
||||
import { GroupsComponent } from "./manage/groups.component";
|
||||
|
||||
@@ -39,14 +38,6 @@ const routes: Routes = [
|
||||
path: "vault",
|
||||
loadChildren: () => VaultModule,
|
||||
},
|
||||
{
|
||||
path: "integrations",
|
||||
canActivate: [organizationPermissionsGuard(canAccessIntegrations)],
|
||||
component: AdminConsoleIntegrationsComponent,
|
||||
data: {
|
||||
titleId: "integrations",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
loadChildren: () =>
|
||||
@@ -103,10 +94,6 @@ function getOrganizationRoute(organization: Organization): string {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function canAccessIntegrations(organization: Organization) {
|
||||
return organization.canAccessIntegrations;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from "./integrations.pipe";
|
||||
export * from "./integration-card/integration-card.component";
|
||||
export * from "./integration-grid/integration-grid.component";
|
||||
export * from "./models";
|
||||
@@ -41,8 +41,6 @@ import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginEmailService,
|
||||
} from "@bitwarden/auth/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -405,11 +403,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultDeviceManagementComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationIntegrationApiService,
|
||||
useClass: OrganizationIntegrationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -7437,6 +7437,9 @@
|
||||
"off": {
|
||||
"message": "Off"
|
||||
},
|
||||
"connected": {
|
||||
"message": "Connected"
|
||||
},
|
||||
"members": {
|
||||
"message": "Members"
|
||||
},
|
||||
@@ -9694,6 +9697,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateIntegrationButtonDesc": {
|
||||
"message": "Update $INTEGRATION$",
|
||||
"placeholders": {
|
||||
"integration": {
|
||||
"content": "$1",
|
||||
"example": "Crowdstrike"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationCardTooltip": {
|
||||
"message": "Launch $INTEGRATION$ implementation guide.",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from "./services";
|
||||
export * from "./models/organization-integration-type";
|
||||
export * from "./models/organization-integration-request";
|
||||
export * from "./models/organization-integration-response";
|
||||
export * from "./models/organization-integration-configuration-request";
|
||||
export * from "./models/organization-integration-configuration-response";
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./organization-integration-api.service";
|
||||
export * from "./organization-integration-configuration-api.service";
|
||||
@@ -0,0 +1,18 @@
|
||||
import { OrganizationIntegrationServiceType } from "../organization-integration-service-type";
|
||||
|
||||
export class HecConfiguration {
|
||||
uri: string;
|
||||
scheme = "Bearer";
|
||||
token: string;
|
||||
service: OrganizationIntegrationServiceType;
|
||||
|
||||
constructor(uri: string, token: string, service: string) {
|
||||
this.uri = uri;
|
||||
this.token = token;
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Added to reflect how future webhook integrations could be structured within the OrganizationIntegration
|
||||
export class WebhookConfiguration {
|
||||
propA: string;
|
||||
propB: string;
|
||||
|
||||
constructor(propA: string, propB: string) {
|
||||
this.propA = propA;
|
||||
this.propB = propB;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { OrganizationIntegrationServiceType } from "../../organization-integration-service-type";
|
||||
|
||||
export class HecTemplate {
|
||||
event = "#EventMessage#";
|
||||
source = "Bitwarden";
|
||||
index: string;
|
||||
service: OrganizationIntegrationServiceType;
|
||||
|
||||
constructor(index: string, service: string) {
|
||||
this.index = index;
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Added to reflect how future webhook integrations could be structured within the OrganizationIntegration
|
||||
export class WebhookTemplate {
|
||||
propA: string;
|
||||
propB: string;
|
||||
|
||||
constructor(propA: string, propB: string) {
|
||||
this.propA = propA;
|
||||
this.propB = propB;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export class WebhookIntegrationConfigurationConfig {
|
||||
propA: string;
|
||||
propB: string;
|
||||
|
||||
constructor(propA: string, propB: string) {
|
||||
this.propA = propA;
|
||||
this.propB = propB;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
|
||||
import { OrganizationIntegration } from "./organization-integration";
|
||||
|
||||
/** Integration or SDK */
|
||||
export type Integration = {
|
||||
@@ -21,4 +23,8 @@ export type Integration = {
|
||||
isConnected?: boolean;
|
||||
canSetupConnection?: boolean;
|
||||
configuration?: string;
|
||||
template?: string;
|
||||
|
||||
// OrganizationIntegration
|
||||
organizationIntegration?: OrganizationIntegration | null;
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { OrganizationIntegrationConfigurationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
OrganizationIntegrationConfigurationId,
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
export class OrganizationIntegrationConfigurationResponse extends BaseResponse {
|
||||
id: OrganizationIntegrationConfigurationId;
|
||||
@@ -18,3 +21,16 @@ export class OrganizationIntegrationConfigurationResponse extends BaseResponse {
|
||||
this.template = this.getResponseProperty("Template");
|
||||
}
|
||||
}
|
||||
|
||||
export class OrganizationIntegrationConfigurationResponseWithIntegrationId {
|
||||
integrationId: OrganizationIntegrationId;
|
||||
configurationResponses: OrganizationIntegrationConfigurationResponse[];
|
||||
|
||||
constructor(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
configurationResponses: OrganizationIntegrationConfigurationResponse[],
|
||||
) {
|
||||
this.integrationId = integrationId;
|
||||
this.configurationResponses = configurationResponses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
OrganizationIntegrationConfigurationId,
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
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";
|
||||
|
||||
export class OrganizationIntegrationConfiguration {
|
||||
id: OrganizationIntegrationConfigurationId;
|
||||
integrationId: OrganizationIntegrationId;
|
||||
eventType?: EventType | null;
|
||||
configuration?: WebhookIntegrationConfigurationConfig | null;
|
||||
filters?: string;
|
||||
template?: HecTemplate | WebhookTemplate | null;
|
||||
|
||||
constructor(
|
||||
id: OrganizationIntegrationConfigurationId,
|
||||
integrationId: OrganizationIntegrationId,
|
||||
eventType?: EventType | null,
|
||||
configuration?: WebhookIntegrationConfigurationConfig | null,
|
||||
filters?: string,
|
||||
template?: HecTemplate | WebhookTemplate | null,
|
||||
) {
|
||||
this.id = id;
|
||||
this.integrationId = integrationId;
|
||||
this.eventType = eventType;
|
||||
this.configuration = configuration;
|
||||
this.filters = filters;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
getTemplate<T>(): T | null {
|
||||
if (this.template && typeof this.template === "object") {
|
||||
return this.template as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { OrganizationIntegrationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { HecConfiguration } from "./configuration/hec-configuration";
|
||||
import { WebhookConfiguration } from "./configuration/webhook-configuration";
|
||||
import { OrganizationIntegrationConfiguration } from "./organization-integration-configuration";
|
||||
import { OrganizationIntegrationServiceType } from "./organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "./organization-integration-type";
|
||||
|
||||
export class OrganizationIntegration {
|
||||
id: OrganizationIntegrationId;
|
||||
type: OrganizationIntegrationType;
|
||||
serviceType: OrganizationIntegrationServiceType;
|
||||
configuration: HecConfiguration | WebhookConfiguration | null;
|
||||
integrationConfiguration: OrganizationIntegrationConfiguration[] = [];
|
||||
|
||||
constructor(
|
||||
id: OrganizationIntegrationId,
|
||||
type: OrganizationIntegrationType,
|
||||
serviceType: OrganizationIntegrationServiceType,
|
||||
configuration: HecConfiguration | WebhookConfiguration | null,
|
||||
integrationConfiguration: OrganizationIntegrationConfiguration[] = [],
|
||||
) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.serviceType = serviceType;
|
||||
this.configuration = configuration;
|
||||
this.integrationConfiguration = integrationConfiguration;
|
||||
}
|
||||
|
||||
getConfiguration<T>(): T | null {
|
||||
if (this.configuration && typeof this.configuration === "object") {
|
||||
return this.configuration as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { HecConfiguration } from "../models/configuration/hec-configuration";
|
||||
import { HecTemplate } from "../models/integration-configuration-config/configuration-template/hec-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 { HecOrganizationIntegrationService } from "./hec-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
|
||||
|
||||
describe("HecOrganizationIntegrationService", () => {
|
||||
let service: HecOrganizationIntegrationService;
|
||||
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 bearerToken = "token";
|
||||
const index = "main";
|
||||
|
||||
beforeEach(() => {
|
||||
service = new HecOrganizationIntegrationService(
|
||||
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 Hec integration", async () => {
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
|
||||
const integrationResponse = {
|
||||
id: integrationId,
|
||||
type: OrganizationIntegrationType.Hec,
|
||||
configuration: JSON.stringify({ url, bearerToken, service: serviceType }),
|
||||
} as OrganizationIntegrationResponse;
|
||||
|
||||
const configResponse = {
|
||||
id: configId,
|
||||
template: JSON.stringify({ index, service: serviceType }),
|
||||
} as OrganizationIntegrationConfigurationResponse;
|
||||
|
||||
mockIntegrationApiService.createOrganizationIntegration.mockResolvedValue(integrationResponse);
|
||||
mockIntegrationConfigurationApiService.createOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
|
||||
await service.saveHec(organizationId, serviceType, url, bearerToken, index);
|
||||
|
||||
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 saveHec", async () => {
|
||||
service.setOrganizationIntegrations("other-org" as OrganizationId);
|
||||
await expect(
|
||||
service.saveHec(organizationId, serviceType, url, bearerToken, index),
|
||||
).rejects.toThrow(Error("Organization ID mismatch"));
|
||||
});
|
||||
|
||||
it("should update an existing Hec integration", async () => {
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
|
||||
const integrationResponse = {
|
||||
id: integrationId,
|
||||
type: OrganizationIntegrationType.Hec,
|
||||
configuration: JSON.stringify({ url, bearerToken, service: serviceType }),
|
||||
} as OrganizationIntegrationResponse;
|
||||
|
||||
const configResponse = {
|
||||
id: configId,
|
||||
template: JSON.stringify({ index, service: serviceType }),
|
||||
} as OrganizationIntegrationConfigurationResponse;
|
||||
|
||||
mockIntegrationApiService.updateOrganizationIntegration.mockResolvedValue(integrationResponse);
|
||||
mockIntegrationConfigurationApiService.updateOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
|
||||
await service.updateHec(
|
||||
organizationId,
|
||||
integrationId,
|
||||
configId,
|
||||
serviceType,
|
||||
url,
|
||||
bearerToken,
|
||||
index,
|
||||
);
|
||||
|
||||
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 updateHec", async () => {
|
||||
service.setOrganizationIntegrations("other-org" as OrganizationId);
|
||||
await expect(
|
||||
service.updateHec(
|
||||
organizationId,
|
||||
integrationId,
|
||||
configId,
|
||||
serviceType,
|
||||
url,
|
||||
bearerToken,
|
||||
index,
|
||||
),
|
||||
).rejects.toThrow(Error("Organization ID mismatch"));
|
||||
});
|
||||
|
||||
it("should get integration by id", async () => {
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
serviceType,
|
||||
{} as HecConfiguration,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
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.Hec,
|
||||
serviceType,
|
||||
{} as HecConfiguration,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
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 HecTemplate,
|
||||
);
|
||||
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
serviceType,
|
||||
{} as HecConfiguration,
|
||||
[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,284 @@
|
||||
import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { HecConfiguration } from "../models/configuration/hec-configuration";
|
||||
import { HecTemplate } from "../models/integration-configuration-config/configuration-template/hec-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 class HecOrganizationIntegrationService {
|
||||
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 bearerToken api token
|
||||
* @param index index in service
|
||||
*/
|
||||
async saveHec(
|
||||
organizationId: OrganizationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
bearerToken: string,
|
||||
index: string,
|
||||
) {
|
||||
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(
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bearerToken api token
|
||||
* @param index index in service
|
||||
*/
|
||||
async updateHec(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
bearerToken: string,
|
||||
index: string,
|
||||
) {
|
||||
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(),
|
||||
),
|
||||
);
|
||||
|
||||
const updatedIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
updatedIntegrationResponse,
|
||||
updatedIntegrationConfigResponse,
|
||||
);
|
||||
|
||||
if (updatedIntegration !== null) {
|
||||
this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 hecConfig = this.convertToJson<HecConfiguration>(integrationResponse.configuration);
|
||||
const template = this.convertToJson<HecTemplate>(configurationResponse.template);
|
||||
|
||||
if (!hecConfig || !template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const integrationConfig = new OrganizationIntegrationConfiguration(
|
||||
configurationResponse.id,
|
||||
integrationResponse.id,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
template,
|
||||
);
|
||||
|
||||
return new OrganizationIntegration(
|
||||
integrationResponse.id,
|
||||
integrationResponse.type,
|
||||
hecConfig.service,
|
||||
hecConfig,
|
||||
[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 hec 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) => {
|
||||
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,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
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) {}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
OrganizationId,
|
||||
@@ -10,7 +8,6 @@ import {
|
||||
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
|
||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationIntegrationConfigurationApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
@@ -85,6 +85,14 @@ const routes: Routes = [
|
||||
(m) => m.AccessIntelligenceModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "integrations",
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessIntegrations)],
|
||||
loadChildren: () =>
|
||||
import("../../dirt/organization-integrations/organization-integrations.module").then(
|
||||
(m) => m.OrganizationIntegrationsModule,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
@if (showConnectedBadge()) {
|
||||
<span class="tw-ml-3">
|
||||
@if (isConnected) {
|
||||
<span bitBadge variant="success">{{ "on" | i18n }}</span>
|
||||
<span bitBadge variant="success">{{ "connected" | i18n }}</span>
|
||||
}
|
||||
@if (!isConnected) {
|
||||
<span bitBadge>{{ "off" | i18n }}</span>
|
||||
@@ -34,7 +34,11 @@
|
||||
|
||||
@if (canSetupConnection) {
|
||||
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
|
||||
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
|
||||
@if (isUpdateAvailable) {
|
||||
<span>{{ "updateIntegrationButtonDesc" | i18n: name }}</span>
|
||||
} @else {
|
||||
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -44,6 +48,7 @@
|
||||
[href]="linkURL"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="{{ linkURL }}"
|
||||
>
|
||||
</a>
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
|
||||
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 { 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 { ToastService } from "@bitwarden/components";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SharedModule } from "@bitwarden/components/src/shared";
|
||||
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 { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
jest.mock("../integration-dialog", () => ({
|
||||
openHecConnectDialog: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("IntegrationCardComponent", () => {
|
||||
let component: IntegrationCardComponent;
|
||||
let fixture: ComponentFixture<IntegrationCardComponent>;
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const activatedRoute = mock<ActivatedRoute>();
|
||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const toastService = mock<ToastService>();
|
||||
|
||||
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
@@ -43,8 +50,9 @@ describe("IntegrationCardComponent", () => {
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -186,27 +194,160 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("connected badge", () => {
|
||||
it("shows connected badge when isConnected is true", () => {
|
||||
component.isConnected = true;
|
||||
describe("showNewBadge", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-06-01"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is undefined", () => {
|
||||
component.newBadgeExpiration = undefined;
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is an invalid date", () => {
|
||||
component.newBadgeExpiration = "invalid-date";
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when newBadgeExpiration is in the future", () => {
|
||||
component.newBadgeExpiration = "2024-06-02";
|
||||
expect(component.showNewBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is today", () => {
|
||||
component.newBadgeExpiration = "2024-06-01";
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is in the past", () => {
|
||||
component.newBadgeExpiration = "2024-05-31";
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
describe("showConnectedBadge", () => {
|
||||
it("returns true when canSetupConnection is true", () => {
|
||||
component.canSetupConnection = true;
|
||||
expect(component.showConnectedBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show connected badge when isConnected is false", () => {
|
||||
component.isConnected = false;
|
||||
fixture.detectChanges();
|
||||
const name = fixture.nativeElement.querySelector("h3 > span > span > span");
|
||||
|
||||
expect(name.textContent).toContain("off");
|
||||
// when isConnected is true/false, the badge should be shown as on/off
|
||||
// when isConnected is undefined, the badge should not be shown
|
||||
expect(component.showConnectedBadge()).toBe(true);
|
||||
it("returns false when canSetupConnection is false", () => {
|
||||
component.canSetupConnection = false;
|
||||
expect(component.showConnectedBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show connected badge when isConnected is undefined", () => {
|
||||
component.isConnected = undefined;
|
||||
it("returns false when canSetupConnection is undefined", () => {
|
||||
component.canSetupConnection = undefined;
|
||||
expect(component.showConnectedBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupConnection", () => {
|
||||
beforeEach(() => {
|
||||
component.integrationSettings = {
|
||||
organizationIntegration: {
|
||||
id: "integration-id",
|
||||
configuration: {},
|
||||
integrationConfiguration: [{ id: "config-id" }],
|
||||
},
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
} as any;
|
||||
component.organizationId = "org-id" as any;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should not proceed if dialog is cancelled", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({ success: false }),
|
||||
});
|
||||
await component.setupConnection();
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call updateHec if isUpdateAvailable is true", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: true,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
}),
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.updateHec).toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
"integration-id",
|
||||
"config-id",
|
||||
OrganizationIntegrationServiceType.CrowdStrike,
|
||||
"test-url",
|
||||
"token",
|
||||
"index",
|
||||
);
|
||||
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call saveHec if isUpdateAvailable is false", async () => {
|
||||
component.integrationSettings = {
|
||||
organizationIntegration: null,
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
} as any;
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: true,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
}),
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(false);
|
||||
|
||||
mockIntegrationService.saveHec.mockResolvedValue(undefined);
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.saveHec).toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
OrganizationIntegrationServiceType.CrowdStrike,
|
||||
"test-url",
|
||||
"token",
|
||||
"index",
|
||||
);
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show toast on error", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: true,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
}),
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.updateHec.mockRejectedValue(new Error("fail"));
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.updateHec).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: mockI18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
@@ -13,22 +11,17 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationIntegrationType,
|
||||
OrganizationIntegrationRequest,
|
||||
OrganizationIntegrationResponse,
|
||||
OrganizationIntegrationApiService,
|
||||
} from "@bitwarden/bit-common/dirt/integrations/index";
|
||||
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 { 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 { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { SharedModule } from "../../../../../../shared/shared.module";
|
||||
import { openHecConnectDialog } from "../integration-dialog/index";
|
||||
import { Integration } from "../models";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-card",
|
||||
@@ -37,13 +30,13 @@ import { Integration } from "../models";
|
||||
})
|
||||
export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
@ViewChild("imageEle") imageEle: ElementRef<HTMLImageElement>;
|
||||
@ViewChild("imageEle") imageEle!: ElementRef<HTMLImageElement>;
|
||||
|
||||
@Input() name: string;
|
||||
@Input() image: string;
|
||||
@Input() imageDarkMode?: string;
|
||||
@Input() linkURL: string;
|
||||
@Input() integrationSettings: Integration;
|
||||
@Input() name: string = "";
|
||||
@Input() image: string = "";
|
||||
@Input() imageDarkMode: string = "";
|
||||
@Input() linkURL: string = "";
|
||||
@Input() integrationSettings!: Integration;
|
||||
|
||||
/** Adds relevant `rel` attribute to external links */
|
||||
@Input() externalURL?: boolean;
|
||||
@@ -56,19 +49,24 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
*/
|
||||
@Input() newBadgeExpiration?: string;
|
||||
@Input() description?: string;
|
||||
@Input() isConnected?: boolean;
|
||||
@Input() canSetupConnection?: boolean;
|
||||
|
||||
organizationId: OrganizationId;
|
||||
|
||||
constructor(
|
||||
private themeStateService: ThemeStateService,
|
||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
private dialogService: DialogService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: OrganizationIntegrationApiService,
|
||||
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = this.activatedRoute.snapshot.paramMap.get(
|
||||
"organizationId",
|
||||
) as OrganizationId;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$])
|
||||
@@ -116,8 +114,16 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
return expirationDate > new Date();
|
||||
}
|
||||
|
||||
get isConnected(): boolean {
|
||||
return !!this.integrationSettings.organizationIntegration?.configuration;
|
||||
}
|
||||
|
||||
showConnectedBadge(): boolean {
|
||||
return this.isConnected !== undefined;
|
||||
return this.canSetupConnection ?? false;
|
||||
}
|
||||
|
||||
get isUpdateAvailable(): boolean {
|
||||
return !!this.integrationSettings.organizationIntegration;
|
||||
}
|
||||
|
||||
async setupConnection() {
|
||||
@@ -135,43 +141,41 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// save the integration
|
||||
try {
|
||||
const dbResponse = await this.saveHecIntegration(result.configuration);
|
||||
this.isConnected = !!dbResponse.id;
|
||||
if (this.isUpdateAvailable) {
|
||||
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.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,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async saveHecIntegration(configuration: string): Promise<OrganizationIntegrationResponse> {
|
||||
const organizationId = this.activatedRoute.snapshot.paramMap.get(
|
||||
"organizationId",
|
||||
) as OrganizationId;
|
||||
|
||||
const request = new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Hec,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const integrations = await this.apiService.getOrganizationIntegrations(organizationId);
|
||||
const existingIntegration = integrations.find(
|
||||
(i) => i.type === OrganizationIntegrationType.Hec,
|
||||
);
|
||||
|
||||
if (existingIntegration) {
|
||||
return await this.apiService.updateOrganizationIntegration(
|
||||
organizationId,
|
||||
existingIntegration.id,
|
||||
request,
|
||||
);
|
||||
} else {
|
||||
return await this.apiService.createOrganizationIntegration(organizationId, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,11 @@
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||
{{ "save" | i18n }}
|
||||
@if (isUpdateAvailable) {
|
||||
{{ "update" | i18n }}
|
||||
} @else {
|
||||
{{ "save" | i18n }}
|
||||
}
|
||||
</button>
|
||||
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
||||
{{ "cancel" | i18n }}
|
||||
@@ -3,14 +3,13 @@ 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 { Integration } from "../../models";
|
||||
|
||||
import {
|
||||
ConnectHecDialogComponent,
|
||||
HecConnectDialogParams,
|
||||
@@ -70,7 +69,9 @@ describe("ConnectDialogHecComponent", () => {
|
||||
canSetupConnection: true,
|
||||
type: IntegrationType.EVENT,
|
||||
} as Integration;
|
||||
const connectInfo: HecConnectDialogParams = { settings: integrationMock };
|
||||
const connectInfo: HecConnectDialogParams = {
|
||||
settings: integrationMock, // Provide appropriate mock template if needed
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
|
||||
@@ -150,12 +151,10 @@ describe("ConnectDialogHecComponent", () => {
|
||||
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
integrationSettings: integrationMock,
|
||||
configuration: JSON.stringify({
|
||||
url: "https://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
}),
|
||||
url: "https://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
success: true,
|
||||
error: null,
|
||||
});
|
||||
@@ -1,18 +1,22 @@
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { HecConfiguration } from "@bitwarden/bit-common/dirt/organization-integrations/models/configuration/hec-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";
|
||||
|
||||
import { Integration } from "../../models";
|
||||
|
||||
export type HecConnectDialogParams = {
|
||||
settings: Integration;
|
||||
};
|
||||
|
||||
export interface HecConnectDialogResult {
|
||||
integrationSettings: Integration;
|
||||
configuration: string;
|
||||
url: string;
|
||||
bearerToken: string;
|
||||
index: string;
|
||||
service: string;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -23,6 +27,8 @@ export interface HecConnectDialogResult {
|
||||
})
|
||||
export class ConnectHecDialogComponent implements OnInit {
|
||||
loading = false;
|
||||
hecConfig: HecConfiguration | null = null;
|
||||
hecTemplate: HecTemplate | null = null;
|
||||
formGroup = this.formBuilder.group({
|
||||
url: ["", [Validators.required, Validators.pattern("https?://.+")]],
|
||||
bearerToken: ["", Validators.required],
|
||||
@@ -37,24 +43,23 @@ export class ConnectHecDialogComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const settings = this.getSettingsAsJson(this.connectInfo.settings.configuration ?? "");
|
||||
this.hecConfig =
|
||||
this.connectInfo.settings.organizationIntegration?.getConfiguration<HecConfiguration>() ??
|
||||
null;
|
||||
this.hecTemplate =
|
||||
this.connectInfo.settings.organizationIntegration?.integrationConfiguration?.[0]?.getTemplate<HecTemplate>() ??
|
||||
null;
|
||||
|
||||
if (settings) {
|
||||
this.formGroup.patchValue({
|
||||
url: settings?.url || "",
|
||||
bearerToken: settings?.bearerToken || "",
|
||||
index: settings?.index || "",
|
||||
service: this.connectInfo.settings.name,
|
||||
});
|
||||
}
|
||||
this.formGroup.patchValue({
|
||||
url: this.hecConfig?.uri || "",
|
||||
bearerToken: this.hecConfig?.token || "",
|
||||
index: this.hecTemplate?.index || "",
|
||||
service: this.connectInfo.settings.name,
|
||||
});
|
||||
}
|
||||
|
||||
getSettingsAsJson(configuration: string) {
|
||||
try {
|
||||
return JSON.parse(configuration);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
get isUpdateAvailable(): boolean {
|
||||
return !!this.hecConfig;
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
@@ -62,7 +67,10 @@ export class ConnectHecDialogComponent implements OnInit {
|
||||
|
||||
const result: HecConnectDialogResult = {
|
||||
integrationSettings: this.connectInfo.settings,
|
||||
configuration: JSON.stringify(formJson),
|
||||
url: formJson.url || "",
|
||||
bearerToken: formJson.bearerToken || "",
|
||||
index: formJson.index || "",
|
||||
service: formJson.service || "",
|
||||
success: true,
|
||||
error: null,
|
||||
};
|
||||
@@ -14,7 +14,6 @@
|
||||
[externalURL]="integration.type === IntegrationType.SDK"
|
||||
[newBadgeExpiration]="integration.newBadgeExpiration"
|
||||
[description]="integration.description | i18n"
|
||||
[isConnected]="integration.isConnected"
|
||||
[canSetupConnection]="integration.canSetupConnection"
|
||||
[integrationSettings]="integration"
|
||||
></app-integration-card>
|
||||
@@ -5,22 +5,17 @@ import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
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";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
// eslint-disable-next-line import/order
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
// FIXME: remove `src` and fix import
|
||||
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SharedModule } from "@bitwarden/components/src/shared";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { Integration } from "../models";
|
||||
|
||||
import { IntegrationGridComponent } from "./integration-grid.component";
|
||||
|
||||
@@ -28,7 +23,7 @@ describe("IntegrationGridComponent", () => {
|
||||
let component: IntegrationGridComponent;
|
||||
let fixture: ComponentFixture<IntegrationGridComponent>;
|
||||
const mockActivatedRoute = mock<ActivatedRoute>();
|
||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
name: "Integration 1",
|
||||
@@ -74,10 +69,7 @@ describe("IntegrationGridComponent", () => {
|
||||
provide: ActivatedRoute,
|
||||
useValue: mockActivatedRoute,
|
||||
},
|
||||
{
|
||||
provide: OrganizationIntegrationApiService,
|
||||
useValue: mockOrgIntegrationApiService,
|
||||
},
|
||||
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: mock<ToastService>(),
|
||||
@@ -1,12 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { SharedModule } from "../../../../../../shared/shared.module";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { Integration } from "../models";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-grid",
|
||||
@@ -14,7 +12,7 @@ import { Integration } from "../models";
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
})
|
||||
export class IntegrationGridComponent {
|
||||
@Input() integrations: Integration[];
|
||||
@Input() integrations: Integration[] = [];
|
||||
|
||||
@Input() ariaI18nKey: string = "integrationCardAriaLabel";
|
||||
@Input() tooltipI18nKey: string = "integrationCardTooltip";
|
||||
@@ -1,43 +1,34 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, Subject, switchMap, takeUntil, scheduled, asyncScheduler } from "rxjs";
|
||||
import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
|
||||
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 { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { SharedOrganizationModule } from "../shared";
|
||||
import { IntegrationGridComponent } from "../shared/components/integrations/integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "../shared/components/integrations/integrations.pipe";
|
||||
import { Integration } from "../shared/components/integrations/models";
|
||||
import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "./integrations.pipe";
|
||||
|
||||
@Component({
|
||||
selector: "ac-integrations",
|
||||
templateUrl: "./integrations.component.html",
|
||||
imports: [
|
||||
SharedModule,
|
||||
SharedOrganizationModule,
|
||||
IntegrationGridComponent,
|
||||
HeaderModule,
|
||||
FilterIntegrationsPipe,
|
||||
],
|
||||
imports: [SharedModule, IntegrationGridComponent, HeaderModule, FilterIntegrationsPipe],
|
||||
})
|
||||
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
// integrationsList: Integration[] = [];
|
||||
tabIndex: number;
|
||||
organization$: Observable<Organization>;
|
||||
tabIndex: number = 0;
|
||||
organization$: Observable<Organization> = new Observable<Organization>();
|
||||
isEventBasedIntegrationsEnabled: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -218,39 +209,26 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
const orgId = this.route.snapshot.params.organizationId;
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
if (!userId) {
|
||||
throw new Error("User ID not found");
|
||||
}
|
||||
|
||||
this.organization$ = this.route.params.pipe(
|
||||
switchMap((params) =>
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.organizationService
|
||||
.organizations$(account?.id)
|
||||
.pipe(getOrganizationById(params.organizationId)),
|
||||
),
|
||||
this.organizationService.organizations$(userId).pipe(
|
||||
getOrganizationById(params.organizationId),
|
||||
// Filter out undefined values
|
||||
takeWhile((org: Organization | undefined) => !!org),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
scheduled(this.orgIntegrationApiService.getOrganizationIntegrations(orgId), asyncScheduler)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// Update the integrations list with the fetched integrations
|
||||
if (integrations && integrations.length > 0) {
|
||||
integrations.forEach((integration) => {
|
||||
const configJson = JSON.parse(integration.configuration || "{}");
|
||||
const serviceName = configJson.service ?? "";
|
||||
const existingIntegration = this.integrationsList.find((i) => i.name === serviceName);
|
||||
|
||||
if (existingIntegration) {
|
||||
// if a configuration exists, then it is connected
|
||||
existingIntegration.isConnected = !!integration.configuration;
|
||||
existingIntegration.configuration = integration.configuration || "";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Sets the organization ID which also loads the integrations$
|
||||
this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => {
|
||||
this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id);
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -258,7 +236,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private orgIntegrationApiService: OrganizationIntegrationApiService,
|
||||
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||
) {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
|
||||
@@ -267,23 +245,40 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
this.isEventBasedIntegrationsEnabled = isEnabled;
|
||||
});
|
||||
|
||||
// Add the new event based items to the list
|
||||
if (this.isEventBasedIntegrationsEnabled) {
|
||||
this.integrationsList.push({
|
||||
name: "Crowdstrike",
|
||||
const crowdstrikeIntegration: Integration = {
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
linkURL: "",
|
||||
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
description: "crowdstrikeEventIntegrationDesc",
|
||||
isConnected: false,
|
||||
canSetupConnection: true,
|
||||
});
|
||||
};
|
||||
|
||||
this.integrationsList.push(crowdstrikeIntegration);
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
integrations.map((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
|
||||
if (item) {
|
||||
item.organizationIntegration = integration;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
// use in the view
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
return IntegrationType;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { Integration } from "../../../shared/components/integrations/models";
|
||||
|
||||
@Pipe({
|
||||
name: "filterIntegrations",
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
|
||||
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessIntegrations)],
|
||||
component: AdminConsoleIntegrationsComponent,
|
||||
data: {
|
||||
titleId: "integrations",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OrganizationIntegrationsRoutingModule {}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
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";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
|
||||
import { OrganizationIntegrationsRoutingModule } from "./organization-integrations-routing.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: HecOrganizationIntegrationService,
|
||||
useClass: HecOrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationIntegrationApiService,
|
||||
useClass: OrganizationIntegrationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationIntegrationConfigurationApiService,
|
||||
useClass: OrganizationIntegrationConfigurationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class OrganizationIntegrationsModule {}
|
||||
@@ -9,14 +9,14 @@ 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 { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
|
||||
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 { ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component";
|
||||
|
||||
import { IntegrationCardComponent } from "../../dirt/organization-integrations/integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "../../dirt/organization-integrations/integration-grid/integration-grid.component";
|
||||
|
||||
import { IntegrationsComponent } from "./integrations.component";
|
||||
|
||||
@@ -36,8 +36,8 @@ class MockNewMenuComponent {}
|
||||
|
||||
describe("IntegrationsComponent", () => {
|
||||
let fixture: ComponentFixture<IntegrationsComponent>;
|
||||
const hecOrgIntegrationSvc = mock<HecOrganizationIntegrationService>();
|
||||
|
||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
const activatedRouteMock = {
|
||||
snapshot: { paramMap: { get: jest.fn() } },
|
||||
};
|
||||
@@ -52,10 +52,9 @@ describe("IntegrationsComponent", () => {
|
||||
{ provide: ThemeStateService, useValue: mock<ThemeStateService>() },
|
||||
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(IntegrationsComponent);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { Integration } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/models";
|
||||
|
||||
@Component({
|
||||
selector: "sm-integrations",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component";
|
||||
|
||||
import { IntegrationCardComponent } from "../../dirt/organization-integrations/integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "../../dirt/organization-integrations/integration-grid/integration-grid.component";
|
||||
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||
|
||||
import { IntegrationsRoutingModule } from "./integrations-routing.module";
|
||||
|
||||
Reference in New Issue
Block a user