mirror of
https://github.com/bitwarden/browser
synced 2026-01-27 06:43:41 +00:00
Merge branch 'main' into billing/pm-29600/update-tax-client
This commit is contained in:
1
apps/web/src/images/integrations/logo-huntress-siem.svg
Normal file
1
apps/web/src/images/integrations/logo-huntress-siem.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.0 KiB |
@@ -10432,6 +10432,9 @@
|
||||
"datadogEventIntegrationDesc": {
|
||||
"message": "Send vault event data to your Datadog instance"
|
||||
},
|
||||
"huntressEventIntegrationDesc": {
|
||||
"message": "Send event data to your Huntress SIEM instance"
|
||||
},
|
||||
"failedToSaveIntegration": {
|
||||
"message": "Failed to save integration. Please try again later."
|
||||
},
|
||||
@@ -10543,6 +10546,12 @@
|
||||
"index": {
|
||||
"message": "Index"
|
||||
},
|
||||
"httpEventCollectorUrl": {
|
||||
"message": "HTTP Event Collector URL"
|
||||
},
|
||||
"httpEventCollectorToken": {
|
||||
"message": "HTTP Event Collector Token"
|
||||
},
|
||||
"selectAPlan": {
|
||||
"message": "Select a plan"
|
||||
},
|
||||
|
||||
@@ -3,15 +3,21 @@ import { OrganizationIntegrationServiceName } from "../organization-integration-
|
||||
|
||||
export class HecConfiguration implements OrgIntegrationConfiguration {
|
||||
uri: string;
|
||||
scheme = "Bearer";
|
||||
scheme: string;
|
||||
token: string;
|
||||
service?: string;
|
||||
bw_serviceName: OrganizationIntegrationServiceName;
|
||||
|
||||
constructor(uri: string, token: string, bw_serviceName: OrganizationIntegrationServiceName) {
|
||||
constructor(
|
||||
uri: string,
|
||||
token: string,
|
||||
bw_serviceName: OrganizationIntegrationServiceName,
|
||||
scheme: string = "Bearer",
|
||||
) {
|
||||
this.uri = uri;
|
||||
this.token = token;
|
||||
this.bw_serviceName = bw_serviceName;
|
||||
this.scheme = scheme;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import { DatadogConfiguration } from "./configuration/datadog-configuration";
|
||||
import { HecConfiguration } from "./configuration/hec-configuration";
|
||||
import { OrgIntegrationBuilder } from "./integration-builder";
|
||||
import { DatadogTemplate } from "./integration-configuration-config/configuration-template/datadog-template";
|
||||
import { HecTemplate } from "./integration-configuration-config/configuration-template/hec-template";
|
||||
import { OrganizationIntegrationServiceName } from "./organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "./organization-integration-type";
|
||||
|
||||
describe("OrgIntegrationBuilder", () => {
|
||||
describe("buildHecConfiguration", () => {
|
||||
const testUri = "https://hec.example.com:8088/services/collector";
|
||||
const testToken = "test-token";
|
||||
|
||||
it("should create HecConfiguration with correct values", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
testUri,
|
||||
testToken,
|
||||
OrganizationIntegrationServiceName.Huntress,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(HecConfiguration);
|
||||
expect((config as HecConfiguration).uri).toBe(testUri);
|
||||
expect((config as HecConfiguration).token).toBe(testToken);
|
||||
expect(config.bw_serviceName).toBe(OrganizationIntegrationServiceName.Huntress);
|
||||
});
|
||||
|
||||
it("should use default Bearer scheme", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
testUri,
|
||||
testToken,
|
||||
OrganizationIntegrationServiceName.Huntress,
|
||||
);
|
||||
|
||||
expect((config as HecConfiguration).scheme).toBe("Bearer");
|
||||
});
|
||||
|
||||
it("should use custom scheme when provided", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
testUri,
|
||||
testToken,
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
"Splunk",
|
||||
);
|
||||
|
||||
expect((config as HecConfiguration).scheme).toBe("Splunk");
|
||||
});
|
||||
|
||||
it("should work with CrowdStrike service name", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
testUri,
|
||||
testToken,
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
|
||||
expect(config.bw_serviceName).toBe(OrganizationIntegrationServiceName.CrowdStrike);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildHecTemplate", () => {
|
||||
it("should create HecTemplate with correct values", () => {
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"main",
|
||||
OrganizationIntegrationServiceName.Huntress,
|
||||
);
|
||||
|
||||
expect(template).toBeInstanceOf(HecTemplate);
|
||||
expect((template as HecTemplate).index).toBe("main");
|
||||
expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.Huntress);
|
||||
});
|
||||
|
||||
it("should handle empty index", () => {
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"",
|
||||
OrganizationIntegrationServiceName.Huntress,
|
||||
);
|
||||
|
||||
expect((template as HecTemplate).index).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDataDogConfiguration", () => {
|
||||
const testUri = "https://http-intake.logs.datadoghq.com/api/v2/logs";
|
||||
const testApiKey = "test-api-key";
|
||||
|
||||
it("should create DatadogConfiguration with correct values", () => {
|
||||
const config = OrgIntegrationBuilder.buildDataDogConfiguration(testUri, testApiKey);
|
||||
|
||||
expect(config).toBeInstanceOf(DatadogConfiguration);
|
||||
expect((config as DatadogConfiguration).uri).toBe(testUri);
|
||||
expect((config as DatadogConfiguration).apiKey).toBe(testApiKey);
|
||||
});
|
||||
|
||||
it("should always use Datadog service name", () => {
|
||||
const config = OrgIntegrationBuilder.buildDataDogConfiguration(testUri, testApiKey);
|
||||
|
||||
expect(config.bw_serviceName).toBe(OrganizationIntegrationServiceName.Datadog);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDataDogTemplate", () => {
|
||||
it("should create DatadogTemplate with correct service name", () => {
|
||||
const template = OrgIntegrationBuilder.buildDataDogTemplate(
|
||||
OrganizationIntegrationServiceName.Datadog,
|
||||
);
|
||||
|
||||
expect(template).toBeInstanceOf(DatadogTemplate);
|
||||
expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.Datadog);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConfiguration", () => {
|
||||
describe("HEC type", () => {
|
||||
it("should build HecConfiguration from JSON string", () => {
|
||||
const json = JSON.stringify({
|
||||
Uri: "https://hec.example.com",
|
||||
Token: "test-token",
|
||||
Scheme: "Bearer",
|
||||
bw_serviceName: OrganizationIntegrationServiceName.Huntress,
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
json,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(HecConfiguration);
|
||||
expect((config as HecConfiguration).uri).toBe("https://hec.example.com");
|
||||
expect((config as HecConfiguration).token).toBe("test-token");
|
||||
expect((config as HecConfiguration).scheme).toBe("Bearer");
|
||||
});
|
||||
|
||||
it("should normalize PascalCase properties to camelCase", () => {
|
||||
const json = JSON.stringify({
|
||||
Uri: "https://hec.example.com",
|
||||
Token: "test-token",
|
||||
Scheme: "Splunk",
|
||||
bw_serviceName: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
json,
|
||||
);
|
||||
|
||||
expect((config as HecConfiguration).uri).toBe("https://hec.example.com");
|
||||
expect((config as HecConfiguration).token).toBe("test-token");
|
||||
expect((config as HecConfiguration).scheme).toBe("Splunk");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Datadog type", () => {
|
||||
it("should build DatadogConfiguration from JSON string", () => {
|
||||
const json = JSON.stringify({
|
||||
Uri: "https://datadoghq.com/api",
|
||||
ApiKey: "dd-api-key",
|
||||
bw_serviceName: OrganizationIntegrationServiceName.Datadog,
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
json,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(DatadogConfiguration);
|
||||
expect((config as DatadogConfiguration).uri).toBe("https://datadoghq.com/api");
|
||||
expect((config as DatadogConfiguration).apiKey).toBe("dd-api-key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should throw for unsupported integration type", () => {
|
||||
const json = JSON.stringify({ uri: "test" });
|
||||
|
||||
expect(() =>
|
||||
OrgIntegrationBuilder.buildConfiguration(999 as OrganizationIntegrationType, json),
|
||||
).toThrow("Unsupported integration type: 999");
|
||||
});
|
||||
|
||||
it("should throw for invalid JSON", () => {
|
||||
expect(() =>
|
||||
OrgIntegrationBuilder.buildConfiguration(OrganizationIntegrationType.Hec, "invalid-json"),
|
||||
).toThrow("Invalid integration configuration: JSON parse error");
|
||||
});
|
||||
|
||||
it("should handle empty JSON string by using empty object", () => {
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
"",
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(HecConfiguration);
|
||||
});
|
||||
|
||||
it("should handle undefined values in JSON", () => {
|
||||
const json = JSON.stringify({});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
json,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(HecConfiguration);
|
||||
expect((config as HecConfiguration).uri).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTemplate", () => {
|
||||
describe("HEC type", () => {
|
||||
it("should build HecTemplate from JSON string", () => {
|
||||
const json = JSON.stringify({
|
||||
index: "main",
|
||||
bw_serviceName: OrganizationIntegrationServiceName.Huntress,
|
||||
});
|
||||
|
||||
const template = OrgIntegrationBuilder.buildTemplate(OrganizationIntegrationType.Hec, json);
|
||||
|
||||
expect(template).toBeInstanceOf(HecTemplate);
|
||||
expect((template as HecTemplate).index).toBe("main");
|
||||
expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.Huntress);
|
||||
});
|
||||
|
||||
it("should normalize PascalCase properties", () => {
|
||||
const json = JSON.stringify({
|
||||
Index: "security",
|
||||
bw_serviceName: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
});
|
||||
|
||||
const template = OrgIntegrationBuilder.buildTemplate(OrganizationIntegrationType.Hec, json);
|
||||
|
||||
expect((template as HecTemplate).index).toBe("security");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Datadog type", () => {
|
||||
it("should build DatadogTemplate from JSON string", () => {
|
||||
const json = JSON.stringify({
|
||||
bw_serviceName: OrganizationIntegrationServiceName.Datadog,
|
||||
});
|
||||
|
||||
const template = OrgIntegrationBuilder.buildTemplate(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
json,
|
||||
);
|
||||
|
||||
expect(template).toBeInstanceOf(DatadogTemplate);
|
||||
expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.Datadog);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should throw for unsupported integration type", () => {
|
||||
const json = JSON.stringify({ index: "test" });
|
||||
|
||||
expect(() =>
|
||||
OrgIntegrationBuilder.buildTemplate(999 as OrganizationIntegrationType, json),
|
||||
).toThrow("Unsupported integration type: 999");
|
||||
});
|
||||
|
||||
it("should throw for invalid JSON", () => {
|
||||
expect(() =>
|
||||
OrgIntegrationBuilder.buildTemplate(OrganizationIntegrationType.Hec, "invalid-json"),
|
||||
).toThrow("Invalid integration configuration: JSON parse error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("property case normalization", () => {
|
||||
it("should convert first character to lowercase", () => {
|
||||
const json = JSON.stringify({
|
||||
Uri: "https://example.com",
|
||||
Token: "token",
|
||||
Scheme: "Bearer",
|
||||
bw_serviceName: "Huntress",
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
json,
|
||||
);
|
||||
|
||||
// Verify the properties were normalized (accessed via camelCase)
|
||||
expect((config as HecConfiguration).uri).toBe("https://example.com");
|
||||
expect((config as HecConfiguration).token).toBe("token");
|
||||
});
|
||||
|
||||
it("should handle nested objects", () => {
|
||||
// Using Datadog type which has nested enrichment_details
|
||||
const json = JSON.stringify({
|
||||
Uri: "https://datadoghq.com",
|
||||
ApiKey: "key",
|
||||
NestedObject: {
|
||||
InnerProperty: "value",
|
||||
},
|
||||
});
|
||||
|
||||
// This tests that nested properties are also normalized
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
json,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(DatadogConfiguration);
|
||||
});
|
||||
|
||||
it("should handle arrays", () => {
|
||||
const json = JSON.stringify({
|
||||
Uri: "https://example.com",
|
||||
Token: "token",
|
||||
Items: [{ Name: "item1" }, { Name: "item2" }],
|
||||
bw_serviceName: "Huntress",
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
json,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(HecConfiguration);
|
||||
});
|
||||
|
||||
it("should preserve properties that start with lowercase", () => {
|
||||
const json = JSON.stringify({
|
||||
uri: "https://example.com",
|
||||
token: "token",
|
||||
bw_serviceName: "Huntress",
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
OrganizationIntegrationType.Hec,
|
||||
json,
|
||||
);
|
||||
|
||||
expect((config as HecConfiguration).uri).toBe("https://example.com");
|
||||
expect((config as HecConfiguration).token).toBe("token");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,11 @@ export interface OrgIntegrationTemplate {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export const Schemas = {
|
||||
Bearer: "Bearer",
|
||||
Splunk: "Splunk",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Builder class for creating organization integration configurations and templates
|
||||
*/
|
||||
@@ -29,8 +34,9 @@ export class OrgIntegrationBuilder {
|
||||
uri: string,
|
||||
token: string,
|
||||
bw_serviceName: OrganizationIntegrationServiceName,
|
||||
scheme: string = Schemas.Bearer,
|
||||
): OrgIntegrationConfiguration {
|
||||
return new HecConfiguration(uri, token, bw_serviceName);
|
||||
return new HecConfiguration(uri, token, bw_serviceName, scheme);
|
||||
}
|
||||
|
||||
static buildHecTemplate(
|
||||
@@ -57,7 +63,12 @@ export class OrgIntegrationBuilder {
|
||||
switch (type) {
|
||||
case OrganizationIntegrationType.Hec: {
|
||||
const hecConfig = this.convertToJson<HecConfiguration>(configuration);
|
||||
return this.buildHecConfiguration(hecConfig.uri, hecConfig.token, hecConfig.bw_serviceName);
|
||||
return this.buildHecConfiguration(
|
||||
hecConfig.uri,
|
||||
hecConfig.token,
|
||||
hecConfig.bw_serviceName,
|
||||
hecConfig.scheme,
|
||||
);
|
||||
}
|
||||
case OrganizationIntegrationType.Datadog: {
|
||||
const datadogConfig = this.convertToJson<DatadogConfiguration>(configuration);
|
||||
|
||||
@@ -2,8 +2,6 @@ import { OrgIntegrationTemplate } from "../../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../../organization-integration-service-type";
|
||||
|
||||
export class HecTemplate implements OrgIntegrationTemplate {
|
||||
event = "#EventMessage#";
|
||||
source = "Bitwarden";
|
||||
index: string;
|
||||
bw_serviceName: OrganizationIntegrationServiceName;
|
||||
|
||||
@@ -12,12 +10,46 @@ export class HecTemplate implements OrgIntegrationTemplate {
|
||||
this.bw_serviceName = service;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify({
|
||||
Event: this.event,
|
||||
Source: this.source,
|
||||
Index: this.index,
|
||||
private toJSON() {
|
||||
const template: Record<string, any> = {
|
||||
bw_serviceName: this.bw_serviceName,
|
||||
});
|
||||
source: "bitwarden",
|
||||
service: "event-logs",
|
||||
event: {
|
||||
object: "event",
|
||||
type: "#Type#",
|
||||
itemId: "#CipherId#",
|
||||
collectionId: "#CollectionId#",
|
||||
groupId: "#GroupId#",
|
||||
policyId: "#PolicyId#",
|
||||
memberId: "#UserId#",
|
||||
actingUserId: "#ActingUserId#",
|
||||
installationId: "#InstallationId#",
|
||||
date: "#DateIso8601#",
|
||||
device: "#DeviceType#",
|
||||
ipAddress: "#IpAddress#",
|
||||
secretId: "#SecretId#",
|
||||
projectId: "#ProjectId#",
|
||||
serviceAccountId: "#ServiceAccountId#",
|
||||
actingUserName: "#ActingUserName#",
|
||||
actingUserEmail: "#ActingUserEmail#",
|
||||
actingUserType: "#ActingUserType#",
|
||||
userName: "#UserName#",
|
||||
userEmail: "#UserEmail#",
|
||||
userType: "#UserType#",
|
||||
groupName: "#GroupName#",
|
||||
},
|
||||
};
|
||||
|
||||
// Only include index if it's provided
|
||||
if (this.index && this.index.trim() !== "") {
|
||||
template.index = this.index;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const OrganizationIntegrationServiceName = Object.freeze({
|
||||
CrowdStrike: "CrowdStrike",
|
||||
Datadog: "Datadog",
|
||||
Huntress: "Huntress",
|
||||
} as const);
|
||||
|
||||
export type OrganizationIntegrationServiceName =
|
||||
|
||||
@@ -16,13 +16,15 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { HecConnectDialogResultStatus, openHecConnectDialog } from "../integration-dialog";
|
||||
import { IntegrationDialogResultStatus, openHecConnectDialog } from "../integration-dialog";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
jest.mock("../integration-dialog", () => ({
|
||||
openHecConnectDialog: jest.fn(),
|
||||
HecConnectDialogResultStatus: { Edited: "edit", Delete: "delete" },
|
||||
openDatadogConnectDialog: jest.fn(),
|
||||
openHuntressConnectDialog: jest.fn(),
|
||||
IntegrationDialogResultStatus: { Edited: "edit", Delete: "delete" },
|
||||
}));
|
||||
|
||||
describe("IntegrationCardComponent", () => {
|
||||
@@ -276,7 +278,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should call updateHec if isUpdateAvailable is true", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -317,7 +319,7 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -354,7 +356,7 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
success: IntegrationDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -382,7 +384,7 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
success: IntegrationDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -404,7 +406,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should show toast on error while saving", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -427,7 +429,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should show mustBeOwner toast on error while inserting data", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -450,7 +452,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should show mustBeOwner toast on error while updating data", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -472,7 +474,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should show toast on error while deleting", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
success: IntegrationDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -495,7 +497,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should show mustbeOwner toast on 404 while deleting", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
success: IntegrationDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
|
||||
@@ -12,7 +12,12 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrgIntegrationBuilder } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder";
|
||||
import {
|
||||
OrgIntegrationBuilder,
|
||||
OrgIntegrationConfiguration,
|
||||
OrgIntegrationTemplate,
|
||||
Schemas,
|
||||
} from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
@@ -23,7 +28,6 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
BaseCardComponent,
|
||||
CardContentComponent,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
@@ -32,10 +36,11 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
import {
|
||||
HecConnectDialogResult,
|
||||
DatadogConnectDialogResult,
|
||||
HecConnectDialogResultStatus,
|
||||
DatadogConnectDialogResultStatus,
|
||||
HuntressConnectDialogResult,
|
||||
IntegrationDialogResultStatus,
|
||||
openDatadogConnectDialog,
|
||||
openHecConnectDialog,
|
||||
openHuntressConnectDialog,
|
||||
} from "../integration-dialog/index";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -164,14 +169,12 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
async setupConnection() {
|
||||
let dialog: DialogRef<DatadogConnectDialogResult | HecConnectDialogResult, unknown>;
|
||||
|
||||
if (this.integrationSettings?.integrationType === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) {
|
||||
dialog = openDatadogConnectDialog(this.dialogService, {
|
||||
const dialog = openDatadogConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
@@ -179,37 +182,29 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
// the dialog was cancelled
|
||||
if (!result || !result.success) {
|
||||
return;
|
||||
}
|
||||
await this.handleIntegrationDialogResult(
|
||||
result,
|
||||
() => this.deleteDatadog(),
|
||||
(res) => this.saveDatadog(res),
|
||||
);
|
||||
} else if (this.integrationSettings.name === OrganizationIntegrationServiceName.Huntress) {
|
||||
// Huntress uses HEC protocol but has its own dialog
|
||||
const dialog = openHuntressConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Delete) {
|
||||
await this.deleteDatadog();
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToDeleteIntegration"),
|
||||
});
|
||||
}
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
try {
|
||||
if (result.success === DatadogConnectDialogResultStatus.Edited) {
|
||||
await this.saveDatadog(result as DatadogConnectDialogResult);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
}
|
||||
await this.handleIntegrationDialogResult(
|
||||
result,
|
||||
() => this.deleteHuntress(),
|
||||
(res) => this.saveHuntress(res),
|
||||
);
|
||||
} else {
|
||||
// invoke the dialog to connect the integration
|
||||
dialog = openHecConnectDialog(this.dialogService, {
|
||||
const dialog = openHecConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
@@ -217,15 +212,113 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
// the dialog was cancelled
|
||||
if (!result || !result.success) {
|
||||
return;
|
||||
await this.handleIntegrationDialogResult(
|
||||
result,
|
||||
() => this.deleteHec(),
|
||||
(res) => this.saveHec(res),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic save method
|
||||
*/
|
||||
private async saveIntegration(
|
||||
integrationType: OrganizationIntegrationType,
|
||||
config: OrgIntegrationConfiguration,
|
||||
template: OrgIntegrationTemplate,
|
||||
): Promise<void> {
|
||||
let response = { mustBeOwner: false, success: false };
|
||||
|
||||
if (this.isUpdateAvailable) {
|
||||
// retrieve org integration and configuration ids
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
// update existing integration and configuration
|
||||
response = await this.organizationIntegrationService.update(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
integrationType,
|
||||
orgIntegrationConfigurationId,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
response = await this.organizationIntegrationService.save(
|
||||
this.organizationId,
|
||||
integrationType,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic delete method
|
||||
*/
|
||||
private async deleteIntegration(): Promise<void> {
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
const response = await this.organizationIntegrationService.delete(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
);
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic dialog result handler
|
||||
* Handles both delete and edit actions with proper error handling
|
||||
*/
|
||||
private async handleIntegrationDialogResult<T extends { success: string | null }>(
|
||||
result: T | undefined,
|
||||
deleteCallback: () => Promise<void>,
|
||||
saveCallback: (result: T) => Promise<void>,
|
||||
): Promise<void> {
|
||||
// User cancelled the dialog or closed it without saving
|
||||
if (!result || !result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle delete action
|
||||
if (result.success === IntegrationDialogResultStatus.Delete) {
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Delete) {
|
||||
await this.deleteHec();
|
||||
}
|
||||
await deleteCallback();
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
@@ -233,11 +326,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
message: this.i18nService.t("failedToDeleteIntegration"),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle edit/save action
|
||||
if (result.success === IntegrationDialogResultStatus.Edited) {
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Edited) {
|
||||
await this.saveHec(result as HecConnectDialogResult);
|
||||
}
|
||||
await saveCallback(result);
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
@@ -249,8 +344,6 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
async saveHec(result: HecConnectDialogResult) {
|
||||
let response = { mustBeOwner: false, success: false };
|
||||
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
@@ -261,148 +354,45 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
|
||||
if (this.isUpdateAvailable) {
|
||||
// retrieve org integration and configuration ids
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
// update existing integration and configuration
|
||||
response = await this.organizationIntegrationService.update(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
orgIntegrationConfigurationId,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
response = await this.organizationIntegrationService.save(
|
||||
this.organizationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
await this.saveIntegration(OrganizationIntegrationType.Hec, config, template);
|
||||
}
|
||||
|
||||
async deleteHec() {
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
await this.deleteIntegration();
|
||||
}
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
const response = await this.organizationIntegrationService.delete(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
async saveHuntress(result: HuntressConnectDialogResult) {
|
||||
// Huntress uses "Splunk" scheme for HEC protocol compatibility
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
result.url,
|
||||
result.token,
|
||||
OrganizationIntegrationServiceName.Huntress,
|
||||
Schemas.Splunk,
|
||||
);
|
||||
// Huntress SIEM doesn't require the index field
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"",
|
||||
OrganizationIntegrationServiceName.Huntress,
|
||||
);
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
await this.saveIntegration(OrganizationIntegrationType.Hec, config, template);
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
async deleteHuntress() {
|
||||
await this.deleteIntegration();
|
||||
}
|
||||
|
||||
async saveDatadog(result: DatadogConnectDialogResult) {
|
||||
let response = { mustBeOwner: false, success: false };
|
||||
|
||||
const config = OrgIntegrationBuilder.buildDataDogConfiguration(result.url, result.apiKey);
|
||||
const template = OrgIntegrationBuilder.buildDataDogTemplate(
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
|
||||
if (this.isUpdateAvailable) {
|
||||
// retrieve org integration and configuration ids
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
// update existing integration and configuration
|
||||
response = await this.organizationIntegrationService.update(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
orgIntegrationConfigurationId,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
response = await this.organizationIntegrationService.save(
|
||||
this.organizationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
await this.saveIntegration(OrganizationIntegrationType.Datadog, config, template);
|
||||
}
|
||||
|
||||
async deleteDatadog() {
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
const response = await this.organizationIntegrationService.delete(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
);
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
});
|
||||
await this.deleteIntegration();
|
||||
}
|
||||
|
||||
private showMustBeOwnerToast() {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="apiKey" />
|
||||
<input bitInput type="password" formControlName="apiKey" />
|
||||
<bit-hint>{{ "apiKey" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
|
||||
@@ -10,11 +10,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationDialogResultStatus } from "../integration-dialog-result-status";
|
||||
|
||||
import {
|
||||
ConnectDatadogDialogComponent,
|
||||
DatadogConnectDialogParams,
|
||||
DatadogConnectDialogResult,
|
||||
DatadogConnectDialogResultStatus,
|
||||
openDatadogConnectDialog,
|
||||
} from "./connect-dialog-datadog.component";
|
||||
|
||||
@@ -149,7 +150,7 @@ describe("ConnectDialogDatadogComponent", () => {
|
||||
url: "https://test.com",
|
||||
apiKey: "token",
|
||||
service: "Test Service",
|
||||
success: DatadogConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,11 @@ import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integration
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import {
|
||||
IntegrationDialogResultStatus,
|
||||
IntegrationDialogResultStatusType,
|
||||
} from "../integration-dialog-result-status";
|
||||
|
||||
export type DatadogConnectDialogParams = {
|
||||
settings: Integration;
|
||||
};
|
||||
@@ -16,17 +21,9 @@ export interface DatadogConnectDialogResult {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
service: string;
|
||||
success: DatadogConnectDialogResultStatusType | null;
|
||||
success: IntegrationDialogResultStatusType | null;
|
||||
}
|
||||
|
||||
export const DatadogConnectDialogResultStatus = {
|
||||
Edited: "edit",
|
||||
Delete: "delete",
|
||||
} as const;
|
||||
|
||||
export type DatadogConnectDialogResultStatusType =
|
||||
(typeof DatadogConnectDialogResultStatus)[keyof typeof DatadogConnectDialogResultStatus];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@@ -78,7 +75,7 @@ export class ConnectDatadogDialogComponent implements OnInit {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Edited);
|
||||
const result = this.getDatadogConnectDialogResult(IntegrationDialogResultStatus.Edited);
|
||||
|
||||
this.dialogRef.close(result);
|
||||
|
||||
@@ -95,13 +92,13 @@ export class ConnectDatadogDialogComponent implements OnInit {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Delete);
|
||||
const result = this.getDatadogConnectDialogResult(IntegrationDialogResultStatus.Delete);
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
};
|
||||
|
||||
private getDatadogConnectDialogResult(
|
||||
status: DatadogConnectDialogResultStatusType,
|
||||
status: IntegrationDialogResultStatusType,
|
||||
): DatadogConnectDialogResult {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "bearerToken" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="bearerToken" />
|
||||
<input bitInput type="password" formControlName="bearerToken" />
|
||||
<bit-hint>{{ "apiKey" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationDialogResultStatus } from "../integration-dialog-result-status";
|
||||
|
||||
import {
|
||||
ConnectHecDialogComponent,
|
||||
HecConnectDialogParams,
|
||||
HecConnectDialogResult,
|
||||
HecConnectDialogResultStatus,
|
||||
openHecConnectDialog,
|
||||
} from "./connect-dialog-hec.component";
|
||||
|
||||
@@ -155,7 +156,7 @@ describe("ConnectDialogHecComponent", () => {
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,11 @@ import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integration
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import {
|
||||
IntegrationDialogResultStatus,
|
||||
IntegrationDialogResultStatusType,
|
||||
} from "../integration-dialog-result-status";
|
||||
|
||||
export type HecConnectDialogParams = {
|
||||
settings: Integration;
|
||||
};
|
||||
@@ -17,17 +22,9 @@ export interface HecConnectDialogResult {
|
||||
bearerToken: string;
|
||||
index: string;
|
||||
service: string;
|
||||
success: HecConnectDialogResultStatusType | null;
|
||||
success: IntegrationDialogResultStatusType | null;
|
||||
}
|
||||
|
||||
export const HecConnectDialogResultStatus = {
|
||||
Edited: "edit",
|
||||
Delete: "delete",
|
||||
} as const;
|
||||
|
||||
export type HecConnectDialogResultStatusType =
|
||||
(typeof HecConnectDialogResultStatus)[keyof typeof HecConnectDialogResultStatus];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@@ -81,7 +78,7 @@ export class ConnectHecDialogComponent implements OnInit {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Edited);
|
||||
const result = this.getHecConnectDialogResult(IntegrationDialogResultStatus.Edited);
|
||||
|
||||
this.dialogRef.close(result);
|
||||
|
||||
@@ -98,13 +95,13 @@ export class ConnectHecDialogComponent implements OnInit {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Delete);
|
||||
const result = this.getHecConnectDialogResult(IntegrationDialogResultStatus.Delete);
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
};
|
||||
|
||||
private getHecConnectDialogResult(
|
||||
status: HecConnectDialogResultStatusType,
|
||||
status: IntegrationDialogResultStatusType,
|
||||
): HecConnectDialogResult {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
|
||||
</span>
|
||||
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
|
||||
@if (loading) {
|
||||
<ng-container #spinner>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
}
|
||||
@if (!loading) {
|
||||
<ng-container>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "httpEventCollectorUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="url"
|
||||
placeholder="https://hec.huntress.io/services/collector"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "httpEventCollectorToken" | i18n }}</bit-label>
|
||||
<input bitInput type="password" formControlName="token" />
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||
@if (isUpdateAvailable) {
|
||||
{{ "update" | i18n }}
|
||||
} @else {
|
||||
{{ "save" | i18n }}
|
||||
}
|
||||
</button>
|
||||
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
@if (canDelete) {
|
||||
<div class="tw-ml-auto">
|
||||
<button
|
||||
bitIconButton="bwi-trash"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
label="{{ 'delete' | i18n }}"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
[bitAction]="delete"
|
||||
></button>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,206 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationDialogResultStatus } from "../integration-dialog-result-status";
|
||||
|
||||
import {
|
||||
ConnectHuntressDialogComponent,
|
||||
HuntressConnectDialogParams,
|
||||
HuntressConnectDialogResult,
|
||||
openHuntressConnectDialog,
|
||||
} from "./connect-dialog-huntress.component";
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock element.animate for jsdom
|
||||
// the animate function is not available in jsdom, so we provide a mock implementation
|
||||
// This is necessary for tests that rely on animations
|
||||
// This mock does not perform any actual animations, it just provides a structure that allows tests
|
||||
// to run without throwing errors related to missing animate function
|
||||
if (!HTMLElement.prototype.animate) {
|
||||
HTMLElement.prototype.animate = function () {
|
||||
return {
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
finish: () => {},
|
||||
cancel: () => {},
|
||||
reverse: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
onfinish: null,
|
||||
oncancel: null,
|
||||
startTime: 0,
|
||||
currentTime: 0,
|
||||
playbackRate: 1,
|
||||
playState: "idle",
|
||||
replaceState: "active",
|
||||
effect: null,
|
||||
finished: Promise.resolve(),
|
||||
id: "",
|
||||
remove: () => {},
|
||||
timeline: null,
|
||||
ready: Promise.resolve(),
|
||||
} as unknown as Animation;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe("ConnectHuntressDialogComponent", () => {
|
||||
let component: ConnectHuntressDialogComponent;
|
||||
let fixture: ComponentFixture<ConnectHuntressDialogComponent>;
|
||||
let dialogRefMock = mock<DialogRef<HuntressConnectDialogResult>>();
|
||||
const mockI18nService = mock<I18nService>();
|
||||
|
||||
const integrationMock: Integration = {
|
||||
name: "Huntress",
|
||||
image: "test-image.png",
|
||||
linkURL: "https://example.com",
|
||||
imageDarkMode: "test-image-dark.png",
|
||||
newBadgeExpiration: "2024-12-31",
|
||||
description: "Test Description",
|
||||
canSetupConnection: true,
|
||||
type: IntegrationType.EVENT,
|
||||
} as Integration;
|
||||
|
||||
const connectInfo: HuntressConnectDialogParams = {
|
||||
settings: integrationMock,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRefMock = mock<DialogRef<HuntressConnectDialogResult>>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
{ provide: DIALOG_DATA, useValue: connectInfo },
|
||||
{ provide: DialogRef, useValue: dialogRefMock },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConnectHuntressDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
mockI18nService.t.mockImplementation((key) => key);
|
||||
});
|
||||
|
||||
it("should create the component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize form with empty values and service name", () => {
|
||||
expect(component.formGroup.value).toEqual({
|
||||
url: "",
|
||||
token: "",
|
||||
service: "Huntress",
|
||||
});
|
||||
});
|
||||
|
||||
it("should have required validators for url and token fields", () => {
|
||||
component.formGroup.setValue({ url: "", token: "", service: "" });
|
||||
expect(component.formGroup.valid).toBeFalsy();
|
||||
|
||||
component.formGroup.setValue({
|
||||
url: "https://hec.huntress.io/services/collector",
|
||||
token: "test-token",
|
||||
service: "Huntress",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should require url to be at least 7 characters long", () => {
|
||||
component.formGroup.setValue({
|
||||
url: "test",
|
||||
token: "token",
|
||||
service: "Huntress",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeFalsy();
|
||||
|
||||
component.formGroup.setValue({
|
||||
url: "https://hec.huntress.io",
|
||||
token: "token",
|
||||
service: "Huntress",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should call dialogRef.close with correct result on submit", async () => {
|
||||
component.formGroup.setValue({
|
||||
url: "https://hec.huntress.io/services/collector",
|
||||
token: "test-token",
|
||||
service: "Huntress",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
integrationSettings: integrationMock,
|
||||
url: "https://hec.huntress.io/services/collector",
|
||||
token: "test-token",
|
||||
service: "Huntress",
|
||||
success: IntegrationDialogResultStatus.Edited,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not submit when form is invalid", async () => {
|
||||
component.formGroup.setValue({
|
||||
url: "",
|
||||
token: "",
|
||||
service: "Huntress",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(dialogRefMock.close).not.toHaveBeenCalled();
|
||||
expect(component.formGroup.touched).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false for isUpdateAvailable when no config exists", () => {
|
||||
component.huntressConfig = null;
|
||||
expect(component.isUpdateAvailable).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true for isUpdateAvailable when config exists", () => {
|
||||
component.huntressConfig = { uri: "test", token: "test" } as any;
|
||||
expect(component.isUpdateAvailable).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false for canDelete when no config exists", () => {
|
||||
component.huntressConfig = null;
|
||||
expect(component.canDelete).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true for canDelete when config exists", () => {
|
||||
component.huntressConfig = { uri: "test", token: "test" } as any;
|
||||
expect(component.canDelete).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openHuntressConnectDialog", () => {
|
||||
it("should call dialogService.open with correct params", () => {
|
||||
const dialogServiceMock = mock<DialogService>();
|
||||
const config: DialogConfig<
|
||||
HuntressConnectDialogParams,
|
||||
DialogRef<HuntressConnectDialogResult>
|
||||
> = {
|
||||
data: { settings: { name: "Huntress" } as Integration },
|
||||
} as any;
|
||||
|
||||
openHuntressConnectDialog(dialogServiceMock, config);
|
||||
|
||||
expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectHuntressDialogComponent, config);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { HecConfiguration } from "@bitwarden/bit-common/dirt/organization-integrations/models/configuration/hec-configuration";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import {
|
||||
IntegrationDialogResultStatus,
|
||||
IntegrationDialogResultStatusType,
|
||||
} from "../integration-dialog-result-status";
|
||||
|
||||
export type HuntressConnectDialogParams = {
|
||||
settings: Integration;
|
||||
};
|
||||
|
||||
export interface HuntressConnectDialogResult {
|
||||
integrationSettings: Integration;
|
||||
url: string;
|
||||
token: string;
|
||||
service: string;
|
||||
success: IntegrationDialogResultStatusType | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./connect-dialog-huntress.component.html",
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class ConnectHuntressDialogComponent implements OnInit {
|
||||
loading = false;
|
||||
huntressConfig: HecConfiguration | null = null;
|
||||
formGroup = this.formBuilder.group({
|
||||
url: ["", [Validators.required, Validators.minLength(7)]],
|
||||
token: ["", Validators.required],
|
||||
service: [""], // Programmatically set in ngOnInit, not shown to user
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected connectInfo: HuntressConnectDialogParams,
|
||||
protected formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<HuntressConnectDialogResult>,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.huntressConfig =
|
||||
this.connectInfo.settings.organizationIntegration?.getConfiguration<HecConfiguration>() ??
|
||||
null;
|
||||
|
||||
this.formGroup.patchValue({
|
||||
url: this.huntressConfig?.uri || "",
|
||||
token: this.huntressConfig?.token || "",
|
||||
service: this.connectInfo.settings.name,
|
||||
});
|
||||
}
|
||||
|
||||
get isUpdateAvailable(): boolean {
|
||||
return !!this.huntressConfig;
|
||||
}
|
||||
|
||||
get canDelete(): boolean {
|
||||
return !!this.huntressConfig;
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const result = this.getHuntressConnectDialogResult(IntegrationDialogResultStatus.Edited);
|
||||
|
||||
this.dialogRef.close(result);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
delete = async (): Promise<void> => {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: {
|
||||
key: "deleteItemConfirmation",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const result = this.getHuntressConnectDialogResult(IntegrationDialogResultStatus.Delete);
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
};
|
||||
|
||||
private getHuntressConnectDialogResult(
|
||||
status: IntegrationDialogResultStatusType,
|
||||
): HuntressConnectDialogResult {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
|
||||
return {
|
||||
integrationSettings: this.connectInfo.settings,
|
||||
url: formJson.url || "",
|
||||
token: formJson.token || "",
|
||||
service: formJson.service || "",
|
||||
success: status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function openHuntressConnectDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<HuntressConnectDialogParams, DialogRef<HuntressConnectDialogResult>>,
|
||||
) {
|
||||
return dialogService.open<HuntressConnectDialogResult>(ConnectHuntressDialogComponent, config);
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./connect-dialog/connect-dialog-hec.component";
|
||||
export * from "./connect-dialog/connect-dialog-datadog.component";
|
||||
export * from "./connect-dialog/connect-dialog-huntress.component";
|
||||
export * from "./integration-dialog-result-status";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Shared status types for integration dialog results
|
||||
* Used across all SIEM integration dialogs (HEC, Datadog, Huntress, etc.)
|
||||
*/
|
||||
export const IntegrationDialogResultStatus = {
|
||||
Edited: "edit",
|
||||
Delete: "delete",
|
||||
} as const;
|
||||
|
||||
export type IntegrationDialogResultStatusType =
|
||||
(typeof IntegrationDialogResultStatus)[keyof typeof IntegrationDialogResultStatus];
|
||||
@@ -32,6 +32,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
tabIndex: number = 0;
|
||||
organization$: Observable<Organization> = new Observable<Organization>();
|
||||
isEventManagementForDataDogAndCrowdStrikeEnabled: boolean = false;
|
||||
isEventManagementForHuntressEnabled: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// initialize the integrations list with default integrations
|
||||
@@ -258,6 +259,13 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
this.isEventManagementForDataDogAndCrowdStrikeEnabled = isEnabled;
|
||||
});
|
||||
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.EventManagementForHuntress)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((isEnabled) => {
|
||||
this.isEventManagementForHuntressEnabled = isEnabled;
|
||||
});
|
||||
|
||||
// Add the new event based items to the list
|
||||
if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) {
|
||||
const crowdstrikeIntegration: Integration = {
|
||||
@@ -285,6 +293,21 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
this.integrationsList.push(datadogIntegration);
|
||||
}
|
||||
|
||||
// Add Huntress SIEM integration (separate feature flag)
|
||||
if (this.isEventManagementForHuntressEnabled) {
|
||||
const huntressIntegration: Integration = {
|
||||
name: OrganizationIntegrationServiceName.Huntress,
|
||||
linkURL: "https://bitwarden.com/help/huntress-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-huntress-siem.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
description: "huntressEventIntegrationDesc",
|
||||
canSetupConnection: true,
|
||||
integrationType: OrganizationIntegrationType.Hec,
|
||||
};
|
||||
|
||||
this.integrationsList.push(huntressIntegration);
|
||||
}
|
||||
|
||||
// For all existing event based configurations loop through and assign the
|
||||
// organizationIntegration for the correct services.
|
||||
this.organizationIntegrationService.integrations$
|
||||
|
||||
@@ -56,6 +56,7 @@ export enum FeatureFlag {
|
||||
|
||||
/* DIRT */
|
||||
EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike",
|
||||
EventManagementForHuntress = "event-management-for-huntress",
|
||||
PhishingDetection = "phishing-detection",
|
||||
|
||||
/* Vault */
|
||||
@@ -119,6 +120,7 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* DIRT */
|
||||
[FeatureFlag.EventManagementForDataDogAndCrowdStrike]: FALSE,
|
||||
[FeatureFlag.EventManagementForHuntress]: FALSE,
|
||||
[FeatureFlag.PhishingDetection]: FALSE,
|
||||
|
||||
/* Vault */
|
||||
|
||||
@@ -260,6 +260,13 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
});
|
||||
|
||||
describe("getVaultTimeoutByUserId$", () => {
|
||||
beforeEach(() => {
|
||||
// Return the input value unchanged
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockImplementation(
|
||||
async (timeout) => timeout,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if no user id is provided", async () => {
|
||||
expect(() => vaultTimeoutSettingsService.getVaultTimeoutByUserId$(null)).toThrow(
|
||||
"User id required. Cannot get vault timeout.",
|
||||
@@ -277,6 +284,9 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
defaultVaultTimeout,
|
||||
);
|
||||
expect(result).toBe(defaultVaultTimeout);
|
||||
});
|
||||
|
||||
@@ -299,8 +309,31 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
vaultTimeout,
|
||||
);
|
||||
expect(result).toBe(vaultTimeout);
|
||||
});
|
||||
|
||||
it("promotes timeout when unavailable on client", async () => {
|
||||
const determinedTimeout = VaultTimeoutNumberType.OnMinute;
|
||||
const promotedValue = VaultTimeoutStringType.OnRestart;
|
||||
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(promotedValue);
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, determinedTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
determinedTimeout,
|
||||
);
|
||||
expect(result).toBe(promotedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: custom", () => {
|
||||
@@ -327,6 +360,9 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
policyMinutes,
|
||||
);
|
||||
expect(result).toBe(policyMinutes);
|
||||
},
|
||||
);
|
||||
@@ -345,6 +381,9 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
vaultTimeout,
|
||||
);
|
||||
expect(result).toBe(vaultTimeout);
|
||||
},
|
||||
);
|
||||
@@ -365,8 +404,36 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
expect(result).toBe(VaultTimeoutNumberType.Immediately);
|
||||
});
|
||||
|
||||
it("promotes policy minutes when unavailable on client", async () => {
|
||||
const promotedValue = VaultTimeoutStringType.Never;
|
||||
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(promotedValue);
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "custom", minutes: policyMinutes } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(
|
||||
VAULT_TIMEOUT,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
policyMinutes,
|
||||
);
|
||||
expect(result).toBe(promotedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: immediately", () => {
|
||||
@@ -383,7 +450,6 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
"when current timeout is %s, returns immediately or promoted value",
|
||||
async (currentTimeout) => {
|
||||
const expectedTimeout = VaultTimeoutNumberType.Immediately;
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(expectedTimeout);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "immediately" } }] as unknown as Policy[]),
|
||||
);
|
||||
@@ -400,6 +466,26 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
expect(result).toBe(expectedTimeout);
|
||||
},
|
||||
);
|
||||
|
||||
it("promotes immediately when unavailable on client", async () => {
|
||||
const promotedValue = VaultTimeoutNumberType.OnMinute;
|
||||
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(promotedValue);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "immediately" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
expect(result).toBe(promotedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: onSystemLock", () => {
|
||||
@@ -413,7 +499,6 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
"when current timeout is %s, returns onLocked or promoted value",
|
||||
async (currentTimeout) => {
|
||||
const expectedTimeout = VaultTimeoutStringType.OnLocked;
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(expectedTimeout);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "onSystemLock" } }] as unknown as Policy[]),
|
||||
);
|
||||
@@ -446,9 +531,31 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
currentTimeout,
|
||||
);
|
||||
expect(result).toBe(currentTimeout);
|
||||
});
|
||||
|
||||
it("promotes onLocked when unavailable on client", async () => {
|
||||
const promotedValue = VaultTimeoutStringType.OnRestart;
|
||||
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(promotedValue);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "onSystemLock" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
);
|
||||
expect(result).toBe(promotedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: onAppRestart", () => {
|
||||
@@ -468,7 +575,9 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
);
|
||||
expect(result).toBe(VaultTimeoutStringType.OnRestart);
|
||||
});
|
||||
|
||||
@@ -488,32 +597,40 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
currentTimeout,
|
||||
);
|
||||
expect(result).toBe(currentTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: never", () => {
|
||||
it("when current timeout is never, returns never or promoted value", async () => {
|
||||
const expectedTimeout = VaultTimeoutStringType.Never;
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(expectedTimeout);
|
||||
it("promotes onRestart when unavailable on client", async () => {
|
||||
const promotedValue = VaultTimeoutStringType.Never;
|
||||
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(promotedValue);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "never" } }] as unknown as Policy[]),
|
||||
of([{ data: { type: "onAppRestart" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
|
||||
await stateProvider.setUserState(
|
||||
VAULT_TIMEOUT,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
);
|
||||
expect(result).toBe(expectedTimeout);
|
||||
expect(result).toBe(promotedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: never", () => {
|
||||
it.each([
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
@@ -532,9 +649,32 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
currentTimeout,
|
||||
);
|
||||
expect(result).toBe(currentTimeout);
|
||||
});
|
||||
|
||||
it("promotes timeout when unavailable on client", async () => {
|
||||
const determinedTimeout = VaultTimeoutStringType.Never;
|
||||
const promotedValue = VaultTimeoutStringType.OnRestart;
|
||||
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(promotedValue);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "never" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, determinedTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
determinedTimeout,
|
||||
);
|
||||
expect(result).toBe(promotedValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -179,7 +179,20 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
private async determineVaultTimeout(
|
||||
currentVaultTimeout: VaultTimeout | null,
|
||||
maxSessionTimeoutPolicyData: MaximumSessionTimeoutPolicyData | null,
|
||||
): Promise<VaultTimeout | null> {
|
||||
): Promise<VaultTimeout> {
|
||||
const determinedTimeout = await this.determineVaultTimeoutInternal(
|
||||
currentVaultTimeout,
|
||||
maxSessionTimeoutPolicyData,
|
||||
);
|
||||
|
||||
// Ensures the timeout is available on this client
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(determinedTimeout);
|
||||
}
|
||||
|
||||
private async determineVaultTimeoutInternal(
|
||||
currentVaultTimeout: VaultTimeout | null,
|
||||
maxSessionTimeoutPolicyData: MaximumSessionTimeoutPolicyData | null,
|
||||
): Promise<VaultTimeout> {
|
||||
// if current vault timeout is null, apply the client specific default
|
||||
currentVaultTimeout = currentVaultTimeout ?? this.defaultVaultTimeout;
|
||||
|
||||
@@ -190,9 +203,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
|
||||
switch (maxSessionTimeoutPolicyData.type) {
|
||||
case "immediately":
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
return VaultTimeoutNumberType.Immediately;
|
||||
case "custom":
|
||||
case null:
|
||||
case undefined:
|
||||
@@ -211,9 +222,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnIdle ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnSleep
|
||||
) {
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
);
|
||||
return VaultTimeoutStringType.OnLocked;
|
||||
}
|
||||
break;
|
||||
case "onAppRestart":
|
||||
@@ -227,11 +236,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
}
|
||||
break;
|
||||
case "never":
|
||||
if (currentVaultTimeout === VaultTimeoutStringType.Never) {
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.Never,
|
||||
);
|
||||
}
|
||||
// Policy doesn't override user preference for "never"
|
||||
break;
|
||||
}
|
||||
return currentVaultTimeout;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<!-- 53rem = ~850px -->
|
||||
<!-- This is a magic number. This number was selected by going to the UI and finding the number that felt the best to me and design. No real rhyme or reason :) -->
|
||||
<div
|
||||
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full"
|
||||
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-bg-sidenav"
|
||||
>
|
||||
<bit-nav-divider></bit-nav-divider>
|
||||
@if (data.open) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
@@ -259,7 +260,18 @@ describe("keyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears the Auto key if vault timeout is set to anything other than null", async () => {
|
||||
it("sets an Auto key if vault timeout is set to 10 minutes and is Cli", async () => {
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId);
|
||||
platformUtilService.getClientType.mockReturnValue(ClientType.Cli);
|
||||
|
||||
await keyService.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(mockUserKey.keyB64, {
|
||||
userId: mockUserId,
|
||||
});
|
||||
});
|
||||
|
||||
it("clears the Auto key if vault timeout is set to 10 minutes", async () => {
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId);
|
||||
|
||||
await keyService.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
|
||||
import { BaseEncryptedOrganizationKey } from "@bitwarden/common/admin-console/models/domain/encrypted-organization-key";
|
||||
import { ProfileOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-organization.response";
|
||||
@@ -671,9 +672,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId) {
|
||||
let shouldStoreKey = false;
|
||||
switch (keySuffix) {
|
||||
case KeySuffixOptions.Auto: {
|
||||
// Cli has fixed Never vault timeout, and it should not be affected by a policy.
|
||||
if (this.platformUtilService.getClientType() == ClientType.Cli) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Sharing the UserKeyDefinition is temporary to get around a circ dep issue between
|
||||
// the VaultTimeoutSettingsSvc and this service.
|
||||
// This should be fixed as part of the PM-7082 - Auto Key Service work.
|
||||
@@ -683,11 +688,14 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
.pipe(filter((timeout) => timeout != null)),
|
||||
);
|
||||
|
||||
shouldStoreKey = vaultTimeout == VaultTimeoutStringType.Never;
|
||||
break;
|
||||
this.logService.debug(
|
||||
`[KeyService] Should store auto key for vault timeout ${vaultTimeout}`,
|
||||
);
|
||||
|
||||
return vaultTimeout == VaultTimeoutStringType.Never;
|
||||
}
|
||||
}
|
||||
return shouldStoreKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async getKeyFromStorage(
|
||||
|
||||
Reference in New Issue
Block a user