mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +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 { VaultModule } from "./collections/vault.module";
|
||||||
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||||
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||||
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
|
|
||||||
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
||||||
import { GroupsComponent } from "./manage/groups.component";
|
import { GroupsComponent } from "./manage/groups.component";
|
||||||
|
|
||||||
@@ -39,14 +38,6 @@ const routes: Routes = [
|
|||||||
path: "vault",
|
path: "vault",
|
||||||
loadChildren: () => VaultModule,
|
loadChildren: () => VaultModule,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "integrations",
|
|
||||||
canActivate: [organizationPermissionsGuard(canAccessIntegrations)],
|
|
||||||
component: AdminConsoleIntegrationsComponent,
|
|
||||||
data: {
|
|
||||||
titleId: "integrations",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "settings",
|
path: "settings",
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@@ -103,10 +94,6 @@ function getOrganizationRoute(organization: Organization): string {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAccessIntegrations(organization: Organization) {
|
|
||||||
return organization.canAccessIntegrations;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
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,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
} from "@bitwarden/auth/common";
|
} 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
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";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -405,11 +403,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultDeviceManagementComponentService,
|
useClass: DefaultDeviceManagementComponentService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: OrganizationIntegrationApiService,
|
|
||||||
useClass: OrganizationIntegrationApiService,
|
|
||||||
deps: [ApiService],
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -7437,6 +7437,9 @@
|
|||||||
"off": {
|
"off": {
|
||||||
"message": "Off"
|
"message": "Off"
|
||||||
},
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Connected"
|
||||||
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"message": "Members"
|
"message": "Members"
|
||||||
},
|
},
|
||||||
@@ -9694,6 +9697,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"updateIntegrationButtonDesc": {
|
||||||
|
"message": "Update $INTEGRATION$",
|
||||||
|
"placeholders": {
|
||||||
|
"integration": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Crowdstrike"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrationCardTooltip": {
|
"integrationCardTooltip": {
|
||||||
"message": "Launch $INTEGRATION$ implementation guide.",
|
"message": "Launch $INTEGRATION$ implementation guide.",
|
||||||
"placeholders": {
|
"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 */
|
/** Integration or SDK */
|
||||||
export type Integration = {
|
export type Integration = {
|
||||||
@@ -21,4 +23,8 @@ export type Integration = {
|
|||||||
isConnected?: boolean;
|
isConnected?: boolean;
|
||||||
canSetupConnection?: boolean;
|
canSetupConnection?: boolean;
|
||||||
configuration?: string;
|
configuration?: string;
|
||||||
|
template?: string;
|
||||||
|
|
||||||
|
// OrganizationIntegration
|
||||||
|
organizationIntegration?: OrganizationIntegration | null;
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
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 {
|
export class OrganizationIntegrationConfigurationResponse extends BaseResponse {
|
||||||
id: OrganizationIntegrationConfigurationId;
|
id: OrganizationIntegrationConfigurationId;
|
||||||
@@ -18,3 +21,16 @@ export class OrganizationIntegrationConfigurationResponse extends BaseResponse {
|
|||||||
this.template = this.getResponseProperty("Template");
|
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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
|
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
|
||||||
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
|
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OrganizationIntegrationApiService {
|
export class OrganizationIntegrationApiService {
|
||||||
constructor(private apiService: ApiService) {}
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Injectable } from "@angular/core";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import {
|
import {
|
||||||
OrganizationId,
|
OrganizationId,
|
||||||
@@ -10,7 +8,6 @@ import {
|
|||||||
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
|
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
|
||||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OrganizationIntegrationConfigurationApiService {
|
export class OrganizationIntegrationConfigurationApiService {
|
||||||
constructor(private apiService: ApiService) {}
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
@@ -85,6 +85,14 @@ const routes: Routes = [
|
|||||||
(m) => m.AccessIntelligenceModule,
|
(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()) {
|
@if (showConnectedBadge()) {
|
||||||
<span class="tw-ml-3">
|
<span class="tw-ml-3">
|
||||||
@if (isConnected) {
|
@if (isConnected) {
|
||||||
<span bitBadge variant="success">{{ "on" | i18n }}</span>
|
<span bitBadge variant="success">{{ "connected" | i18n }}</span>
|
||||||
}
|
}
|
||||||
@if (!isConnected) {
|
@if (!isConnected) {
|
||||||
<span bitBadge>{{ "off" | i18n }}</span>
|
<span bitBadge>{{ "off" | i18n }}</span>
|
||||||
@@ -34,7 +34,11 @@
|
|||||||
|
|
||||||
@if (canSetupConnection) {
|
@if (canSetupConnection) {
|
||||||
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
|
<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>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@
|
|||||||
[href]="linkURL"
|
[href]="linkURL"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
title="{{ linkURL }}"
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,34 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { mock } from "jest-mock-extended";
|
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";
|
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||||
// eslint-disable-next-line no-restricted-imports
|
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
|
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { ToastService } from "@bitwarden/components";
|
import { DialogService, 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 { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
|
|
||||||
|
import { openHecConnectDialog } from "../integration-dialog";
|
||||||
|
|
||||||
import { IntegrationCardComponent } from "./integration-card.component";
|
import { IntegrationCardComponent } from "./integration-card.component";
|
||||||
|
|
||||||
|
jest.mock("../integration-dialog", () => ({
|
||||||
|
openHecConnectDialog: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("IntegrationCardComponent", () => {
|
describe("IntegrationCardComponent", () => {
|
||||||
let component: IntegrationCardComponent;
|
let component: IntegrationCardComponent;
|
||||||
let fixture: ComponentFixture<IntegrationCardComponent>;
|
let fixture: ComponentFixture<IntegrationCardComponent>;
|
||||||
const mockI18nService = mock<I18nService>();
|
const mockI18nService = mock<I18nService>();
|
||||||
const activatedRoute = mock<ActivatedRoute>();
|
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 systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||||
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||||
@@ -43,8 +50,9 @@ describe("IntegrationCardComponent", () => {
|
|||||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||||
{ provide: I18nService, useValue: mockI18nService },
|
{ provide: I18nService, useValue: mockI18nService },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
|
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
{ provide: ToastService, useValue: toastService },
|
||||||
|
{ provide: DialogService, useValue: dialogService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
@@ -186,27 +194,160 @@ describe("IntegrationCardComponent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("connected badge", () => {
|
describe("showNewBadge", () => {
|
||||||
it("shows connected badge when isConnected is true", () => {
|
beforeEach(() => {
|
||||||
component.isConnected = true;
|
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);
|
expect(component.showConnectedBadge()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show connected badge when isConnected is false", () => {
|
it("returns false when canSetupConnection is false", () => {
|
||||||
component.isConnected = false;
|
component.canSetupConnection = false;
|
||||||
fixture.detectChanges();
|
expect(component.showConnectedBadge()).toBe(false);
|
||||||
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("does not show connected badge when isConnected is undefined", () => {
|
it("returns false when canSetupConnection is undefined", () => {
|
||||||
component.isConnected = undefined;
|
component.canSetupConnection = undefined;
|
||||||
expect(component.showConnectedBadge()).toBe(false);
|
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 {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
@@ -13,22 +11,17 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs";
|
import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||||
// eslint-disable-next-line no-restricted-imports
|
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import {
|
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||||
OrganizationIntegrationType,
|
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||||
OrganizationIntegrationRequest,
|
|
||||||
OrganizationIntegrationResponse,
|
|
||||||
OrganizationIntegrationApiService,
|
|
||||||
} from "@bitwarden/bit-common/dirt/integrations/index";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
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 { openHecConnectDialog } from "../integration-dialog/index";
|
||||||
import { Integration } from "../models";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-integration-card",
|
selector: "app-integration-card",
|
||||||
@@ -37,13 +30,13 @@ import { Integration } from "../models";
|
|||||||
})
|
})
|
||||||
export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||||
private destroyed$: Subject<void> = new Subject();
|
private destroyed$: Subject<void> = new Subject();
|
||||||
@ViewChild("imageEle") imageEle: ElementRef<HTMLImageElement>;
|
@ViewChild("imageEle") imageEle!: ElementRef<HTMLImageElement>;
|
||||||
|
|
||||||
@Input() name: string;
|
@Input() name: string = "";
|
||||||
@Input() image: string;
|
@Input() image: string = "";
|
||||||
@Input() imageDarkMode?: string;
|
@Input() imageDarkMode: string = "";
|
||||||
@Input() linkURL: string;
|
@Input() linkURL: string = "";
|
||||||
@Input() integrationSettings: Integration;
|
@Input() integrationSettings!: Integration;
|
||||||
|
|
||||||
/** Adds relevant `rel` attribute to external links */
|
/** Adds relevant `rel` attribute to external links */
|
||||||
@Input() externalURL?: boolean;
|
@Input() externalURL?: boolean;
|
||||||
@@ -56,19 +49,24 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Input() newBadgeExpiration?: string;
|
@Input() newBadgeExpiration?: string;
|
||||||
@Input() description?: string;
|
@Input() description?: string;
|
||||||
@Input() isConnected?: boolean;
|
|
||||||
@Input() canSetupConnection?: boolean;
|
@Input() canSetupConnection?: boolean;
|
||||||
|
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private themeStateService: ThemeStateService,
|
private themeStateService: ThemeStateService,
|
||||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||||
private systemTheme$: Observable<ThemeType>,
|
private systemTheme$: Observable<ThemeType>,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private apiService: OrganizationIntegrationApiService,
|
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
) {}
|
) {
|
||||||
|
this.organizationId = this.activatedRoute.snapshot.paramMap.get(
|
||||||
|
"organizationId",
|
||||||
|
) as OrganizationId;
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$])
|
combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$])
|
||||||
@@ -116,8 +114,16 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
|||||||
return expirationDate > new Date();
|
return expirationDate > new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isConnected(): boolean {
|
||||||
|
return !!this.integrationSettings.organizationIntegration?.configuration;
|
||||||
|
}
|
||||||
|
|
||||||
showConnectedBadge(): boolean {
|
showConnectedBadge(): boolean {
|
||||||
return this.isConnected !== undefined;
|
return this.canSetupConnection ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUpdateAvailable(): boolean {
|
||||||
|
return !!this.integrationSettings.organizationIntegration;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupConnection() {
|
async setupConnection() {
|
||||||
@@ -135,43 +141,41 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// save the integration
|
|
||||||
try {
|
try {
|
||||||
const dbResponse = await this.saveHecIntegration(result.configuration);
|
if (this.isUpdateAvailable) {
|
||||||
this.isConnected = !!dbResponse.id;
|
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 {
|
} catch {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: null,
|
title: "",
|
||||||
message: this.i18nService.t("failedToSaveIntegration"),
|
message: this.i18nService.t("failedToSaveIntegration"),
|
||||||
});
|
});
|
||||||
return;
|
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>
|
</div>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||||
{{ "save" | i18n }}
|
@if (isUpdateAvailable) {
|
||||||
|
{{ "update" | i18n }}
|
||||||
|
} @else {
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
@@ -3,14 +3,13 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
|||||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import { IntegrationType } from "@bitwarden/common/enums";
|
import { IntegrationType } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
|
|
||||||
import { Integration } from "../../models";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConnectHecDialogComponent,
|
ConnectHecDialogComponent,
|
||||||
HecConnectDialogParams,
|
HecConnectDialogParams,
|
||||||
@@ -70,7 +69,9 @@ describe("ConnectDialogHecComponent", () => {
|
|||||||
canSetupConnection: true,
|
canSetupConnection: true,
|
||||||
type: IntegrationType.EVENT,
|
type: IntegrationType.EVENT,
|
||||||
} as Integration;
|
} as Integration;
|
||||||
const connectInfo: HecConnectDialogParams = { settings: integrationMock };
|
const connectInfo: HecConnectDialogParams = {
|
||||||
|
settings: integrationMock, // Provide appropriate mock template if needed
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
|
dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
|
||||||
@@ -150,12 +151,10 @@ describe("ConnectDialogHecComponent", () => {
|
|||||||
|
|
||||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||||
integrationSettings: integrationMock,
|
integrationSettings: integrationMock,
|
||||||
configuration: JSON.stringify({
|
url: "https://test.com",
|
||||||
url: "https://test.com",
|
bearerToken: "token",
|
||||||
bearerToken: "token",
|
index: "1",
|
||||||
index: "1",
|
service: "Test Service",
|
||||||
service: "Test Service",
|
|
||||||
}),
|
|
||||||
success: true,
|
success: true,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
import { Component, Inject, OnInit } from "@angular/core";
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
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 { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
|
|
||||||
import { Integration } from "../../models";
|
|
||||||
|
|
||||||
export type HecConnectDialogParams = {
|
export type HecConnectDialogParams = {
|
||||||
settings: Integration;
|
settings: Integration;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface HecConnectDialogResult {
|
export interface HecConnectDialogResult {
|
||||||
integrationSettings: Integration;
|
integrationSettings: Integration;
|
||||||
configuration: string;
|
url: string;
|
||||||
|
bearerToken: string;
|
||||||
|
index: string;
|
||||||
|
service: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@@ -23,6 +27,8 @@ export interface HecConnectDialogResult {
|
|||||||
})
|
})
|
||||||
export class ConnectHecDialogComponent implements OnInit {
|
export class ConnectHecDialogComponent implements OnInit {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
hecConfig: HecConfiguration | null = null;
|
||||||
|
hecTemplate: HecTemplate | null = null;
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
url: ["", [Validators.required, Validators.pattern("https?://.+")]],
|
url: ["", [Validators.required, Validators.pattern("https?://.+")]],
|
||||||
bearerToken: ["", Validators.required],
|
bearerToken: ["", Validators.required],
|
||||||
@@ -37,24 +43,23 @@ export class ConnectHecDialogComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
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({
|
||||||
this.formGroup.patchValue({
|
url: this.hecConfig?.uri || "",
|
||||||
url: settings?.url || "",
|
bearerToken: this.hecConfig?.token || "",
|
||||||
bearerToken: settings?.bearerToken || "",
|
index: this.hecTemplate?.index || "",
|
||||||
index: settings?.index || "",
|
service: this.connectInfo.settings.name,
|
||||||
service: this.connectInfo.settings.name,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettingsAsJson(configuration: string) {
|
get isUpdateAvailable(): boolean {
|
||||||
try {
|
return !!this.hecConfig;
|
||||||
return JSON.parse(configuration);
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async (): Promise<void> => {
|
submit = async (): Promise<void> => {
|
||||||
@@ -62,7 +67,10 @@ export class ConnectHecDialogComponent implements OnInit {
|
|||||||
|
|
||||||
const result: HecConnectDialogResult = {
|
const result: HecConnectDialogResult = {
|
||||||
integrationSettings: this.connectInfo.settings,
|
integrationSettings: this.connectInfo.settings,
|
||||||
configuration: JSON.stringify(formJson),
|
url: formJson.url || "",
|
||||||
|
bearerToken: formJson.bearerToken || "",
|
||||||
|
index: formJson.index || "",
|
||||||
|
service: formJson.service || "",
|
||||||
success: true,
|
success: true,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
[externalURL]="integration.type === IntegrationType.SDK"
|
[externalURL]="integration.type === IntegrationType.SDK"
|
||||||
[newBadgeExpiration]="integration.newBadgeExpiration"
|
[newBadgeExpiration]="integration.newBadgeExpiration"
|
||||||
[description]="integration.description | i18n"
|
[description]="integration.description | i18n"
|
||||||
[isConnected]="integration.isConnected"
|
|
||||||
[canSetupConnection]="integration.canSetupConnection"
|
[canSetupConnection]="integration.canSetupConnection"
|
||||||
[integrationSettings]="integration"
|
[integrationSettings]="integration"
|
||||||
></app-integration-card>
|
></app-integration-card>
|
||||||
@@ -5,22 +5,17 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||||
// eslint-disable-next-line no-restricted-imports
|
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
|
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||||
import { IntegrationType } from "@bitwarden/common/enums";
|
import { IntegrationType } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
// eslint-disable-next-line import/order
|
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
// FIXME: remove `src` and fix import
|
|
||||||
|
|
||||||
import { ToastService } from "@bitwarden/components";
|
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 { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
|
|
||||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||||
import { Integration } from "../models";
|
|
||||||
|
|
||||||
import { IntegrationGridComponent } from "./integration-grid.component";
|
import { IntegrationGridComponent } from "./integration-grid.component";
|
||||||
|
|
||||||
@@ -28,7 +23,7 @@ describe("IntegrationGridComponent", () => {
|
|||||||
let component: IntegrationGridComponent;
|
let component: IntegrationGridComponent;
|
||||||
let fixture: ComponentFixture<IntegrationGridComponent>;
|
let fixture: ComponentFixture<IntegrationGridComponent>;
|
||||||
const mockActivatedRoute = mock<ActivatedRoute>();
|
const mockActivatedRoute = mock<ActivatedRoute>();
|
||||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||||
const integrations: Integration[] = [
|
const integrations: Integration[] = [
|
||||||
{
|
{
|
||||||
name: "Integration 1",
|
name: "Integration 1",
|
||||||
@@ -74,10 +69,7 @@ describe("IntegrationGridComponent", () => {
|
|||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
useValue: mockActivatedRoute,
|
useValue: mockActivatedRoute,
|
||||||
},
|
},
|
||||||
{
|
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||||
provide: OrganizationIntegrationApiService,
|
|
||||||
useValue: mockOrgIntegrationApiService,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: ToastService,
|
provide: ToastService,
|
||||||
useValue: mock<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 { Component, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import { IntegrationType } from "@bitwarden/common/enums";
|
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 { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||||
import { Integration } from "../models";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-integration-grid",
|
selector: "app-integration-grid",
|
||||||
@@ -14,7 +12,7 @@ import { Integration } from "../models";
|
|||||||
imports: [IntegrationCardComponent, SharedModule],
|
imports: [IntegrationCardComponent, SharedModule],
|
||||||
})
|
})
|
||||||
export class IntegrationGridComponent {
|
export class IntegrationGridComponent {
|
||||||
@Input() integrations: Integration[];
|
@Input() integrations: Integration[] = [];
|
||||||
|
|
||||||
@Input() ariaI18nKey: string = "integrationCardAriaLabel";
|
@Input() ariaI18nKey: string = "integrationCardAriaLabel";
|
||||||
@Input() tooltipI18nKey: string = "integrationCardTooltip";
|
@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 { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
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 { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
|
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 {
|
import {
|
||||||
getOrganizationById,
|
getOrganizationById,
|
||||||
OrganizationService,
|
OrganizationService,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { IntegrationType } from "@bitwarden/common/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
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 { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
|
||||||
import { SharedModule } from "../../../shared/shared.module";
|
import { FilterIntegrationsPipe } from "./integrations.pipe";
|
||||||
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";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "ac-integrations",
|
selector: "ac-integrations",
|
||||||
templateUrl: "./integrations.component.html",
|
templateUrl: "./integrations.component.html",
|
||||||
imports: [
|
imports: [SharedModule, IntegrationGridComponent, HeaderModule, FilterIntegrationsPipe],
|
||||||
SharedModule,
|
|
||||||
SharedOrganizationModule,
|
|
||||||
IntegrationGridComponent,
|
|
||||||
HeaderModule,
|
|
||||||
FilterIntegrationsPipe,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||||
// integrationsList: Integration[] = [];
|
tabIndex: number = 0;
|
||||||
tabIndex: number;
|
organization$: Observable<Organization> = new Observable<Organization>();
|
||||||
organization$: Observable<Organization>;
|
|
||||||
isEventBasedIntegrationsEnabled: boolean = false;
|
isEventBasedIntegrationsEnabled: boolean = false;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -218,39 +209,26 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
ngOnInit(): void {
|
async ngOnInit() {
|
||||||
const orgId = this.route.snapshot.params.organizationId;
|
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("User ID not found");
|
||||||
|
}
|
||||||
|
|
||||||
this.organization$ = this.route.params.pipe(
|
this.organization$ = this.route.params.pipe(
|
||||||
switchMap((params) =>
|
switchMap((params) =>
|
||||||
this.accountService.activeAccount$.pipe(
|
this.organizationService.organizations$(userId).pipe(
|
||||||
switchMap((account) =>
|
getOrganizationById(params.organizationId),
|
||||||
this.organizationService
|
// Filter out undefined values
|
||||||
.organizations$(account?.id)
|
takeWhile((org: Organization | undefined) => !!org),
|
||||||
.pipe(getOrganizationById(params.organizationId)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
scheduled(this.orgIntegrationApiService.getOrganizationIntegrations(orgId), asyncScheduler)
|
// Sets the organization ID which also loads the integrations$
|
||||||
.pipe(takeUntil(this.destroy$))
|
this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => {
|
||||||
.subscribe((integrations) => {
|
this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id);
|
||||||
// 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 || "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -258,7 +236,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private orgIntegrationApiService: OrganizationIntegrationApiService,
|
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||||
) {
|
) {
|
||||||
this.configService
|
this.configService
|
||||||
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
|
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
|
||||||
@@ -267,23 +245,40 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
|||||||
this.isEventBasedIntegrationsEnabled = isEnabled;
|
this.isEventBasedIntegrationsEnabled = isEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add the new event based items to the list
|
||||||
if (this.isEventBasedIntegrationsEnabled) {
|
if (this.isEventBasedIntegrationsEnabled) {
|
||||||
this.integrationsList.push({
|
const crowdstrikeIntegration: Integration = {
|
||||||
name: "Crowdstrike",
|
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||||
linkURL: "",
|
linkURL: "",
|
||||||
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
|
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
|
||||||
type: IntegrationType.EVENT,
|
type: IntegrationType.EVENT,
|
||||||
description: "crowdstrikeEventIntegrationDesc",
|
description: "crowdstrikeEventIntegrationDesc",
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
canSetupConnection: true,
|
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 {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use in the view
|
||||||
get IntegrationType(): typeof IntegrationType {
|
get IntegrationType(): typeof IntegrationType {
|
||||||
return IntegrationType;
|
return IntegrationType;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from "@angular/core";
|
import { Pipe, PipeTransform } from "@angular/core";
|
||||||
|
|
||||||
|
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import { IntegrationType } from "@bitwarden/common/enums";
|
import { IntegrationType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
import { Integration } from "../../../shared/components/integrations/models";
|
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: "filterIntegrations",
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { ToastService } from "@bitwarden/components";
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
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";
|
import { IntegrationsComponent } from "./integrations.component";
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ class MockNewMenuComponent {}
|
|||||||
|
|
||||||
describe("IntegrationsComponent", () => {
|
describe("IntegrationsComponent", () => {
|
||||||
let fixture: ComponentFixture<IntegrationsComponent>;
|
let fixture: ComponentFixture<IntegrationsComponent>;
|
||||||
|
const hecOrgIntegrationSvc = mock<HecOrganizationIntegrationService>();
|
||||||
|
|
||||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
|
||||||
const activatedRouteMock = {
|
const activatedRouteMock = {
|
||||||
snapshot: { paramMap: { get: jest.fn() } },
|
snapshot: { paramMap: { get: jest.fn() } },
|
||||||
};
|
};
|
||||||
@@ -52,10 +52,9 @@ describe("IntegrationsComponent", () => {
|
|||||||
{ provide: ThemeStateService, useValue: mock<ThemeStateService>() },
|
{ provide: ThemeStateService, useValue: mock<ThemeStateService>() },
|
||||||
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) },
|
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||||
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
|
|
||||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
|
||||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||||
{ provide: I18nService, useValue: mockI18nService },
|
{ provide: I18nService, useValue: mockI18nService },
|
||||||
|
{ provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
fixture = TestBed.createComponent(IntegrationsComponent);
|
fixture = TestBed.createComponent(IntegrationsComponent);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||||
import { IntegrationType } from "@bitwarden/common/enums";
|
import { IntegrationType } from "@bitwarden/common/enums";
|
||||||
import { Integration } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/models";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-integrations",
|
selector: "sm-integrations",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component";
|
import { IntegrationCardComponent } from "../../dirt/organization-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 { IntegrationGridComponent } from "../../dirt/organization-integrations/integration-grid/integration-grid.component";
|
||||||
|
|
||||||
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||||
|
|
||||||
import { IntegrationsRoutingModule } from "./integrations-routing.module";
|
import { IntegrationsRoutingModule } from "./integrations-routing.module";
|
||||||
|
|||||||
Reference in New Issue
Block a user