mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 02:44:01 +00:00
Merge branch 'main' into desktop/pm-18769/migrate-vault-filters
This commit is contained in:
8
.github/renovate.json5
vendored
8
.github/renovate.json5
vendored
@@ -154,7 +154,6 @@
|
||||
"@types/glob",
|
||||
"@types/lowdb",
|
||||
"@types/node",
|
||||
"@types/node-forge",
|
||||
"@types/node-ipc",
|
||||
"@yao-pkg/pkg",
|
||||
"anyhow",
|
||||
@@ -192,7 +191,6 @@
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"node-forge",
|
||||
"node-ipc",
|
||||
"nx",
|
||||
"oo7",
|
||||
@@ -415,14 +413,16 @@
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"@types/node-forge",
|
||||
"aes",
|
||||
"big-integer",
|
||||
"cbc",
|
||||
"linux-keyutils",
|
||||
"memsec",
|
||||
"node-forge",
|
||||
"rsa",
|
||||
"russh-cryptovec",
|
||||
"sha2",
|
||||
"memsec",
|
||||
"linux-keyutils",
|
||||
],
|
||||
description: "Key Management owned dependencies",
|
||||
commitMessagePrefix: "[deps] KM:",
|
||||
|
||||
20
.github/workflows/build-desktop.yml
vendored
20
.github/workflows/build-desktop.yml
vendored
@@ -583,7 +583,9 @@ jobs:
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
run: node build.js cross-platform
|
||||
env:
|
||||
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
|
||||
run: node build.js cross-platform "$env:MODE"
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -846,7 +848,9 @@ jobs:
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
run: node build.js cross-platform
|
||||
env:
|
||||
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
|
||||
run: node build.js cross-platform "$env:MODE"
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -1202,7 +1206,9 @@ jobs:
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
run: node build.js cross-platform
|
||||
env:
|
||||
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
|
||||
run: node build.js cross-platform "$MODE"
|
||||
|
||||
- name: Build application (dev)
|
||||
run: npm run build
|
||||
@@ -1424,7 +1430,9 @@ jobs:
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
run: node build.js cross-platform
|
||||
env:
|
||||
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
|
||||
run: node build.js cross-platform "$MODE"
|
||||
|
||||
- name: Build
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
@@ -1705,7 +1713,9 @@ jobs:
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
run: node build.js cross-platform
|
||||
env:
|
||||
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
|
||||
run: node build.js cross-platform "$MODE"
|
||||
|
||||
- name: Build
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
|
||||
8
.github/workflows/release-desktop.yml
vendored
8
.github/workflows/release-desktop.yml
vendored
@@ -98,6 +98,14 @@ jobs:
|
||||
working-directory: apps/desktop/artifacts
|
||||
run: mv "Bitwarden-${PKG_VERSION}-universal.pkg" "Bitwarden-${PKG_VERSION}-universal.pkg.archive"
|
||||
|
||||
- name: Rename .tar.gz to include version
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.version.outputs.version }}
|
||||
working-directory: apps/desktop/artifacts
|
||||
run: |
|
||||
mv "bitwarden_desktop_x64.tar.gz" "bitwarden_${PKG_VERSION}_x64.tar.gz"
|
||||
mv "bitwarden_desktop_arm64.tar.gz" "bitwarden_${PKG_VERSION}_arm64.tar.gz"
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
|
||||
if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }}
|
||||
|
||||
@@ -436,8 +436,8 @@
|
||||
"sync": {
|
||||
"message": "Sync"
|
||||
},
|
||||
"syncVaultNow": {
|
||||
"message": "Sync vault now"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"lastSync": {
|
||||
"message": "Last sync:"
|
||||
@@ -455,9 +455,6 @@
|
||||
"bitWebVaultApp": {
|
||||
"message": "Bitwarden web app"
|
||||
},
|
||||
"importItems": {
|
||||
"message": "Import items"
|
||||
},
|
||||
"select": {
|
||||
"message": "Select"
|
||||
},
|
||||
@@ -1325,8 +1322,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Export vault"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "File format"
|
||||
@@ -4215,10 +4215,6 @@
|
||||
"ignore": {
|
||||
"message": "Ignore"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import data",
|
||||
"description": "Used for the header of the import dialog, the import button and within the file-password-prompt"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Import error"
|
||||
},
|
||||
@@ -4805,6 +4801,15 @@
|
||||
"accountSecurity": {
|
||||
"message": "Account security"
|
||||
},
|
||||
"phishingBlocker": {
|
||||
"message": "Phishing Blocker"
|
||||
},
|
||||
"enablePhishingDetection": {
|
||||
"message": "Phishing detection"
|
||||
},
|
||||
"enablePhishingDetectionDesc": {
|
||||
"message": "Display warning before accessing suspected phishing sites"
|
||||
},
|
||||
"notifications": {
|
||||
"message": "Notifications"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { AccountSwitcherService } from "./account-switcher.service";
|
||||
@@ -71,11 +72,10 @@ describe("AccountSwitcherService", () => {
|
||||
|
||||
describe("availableAccounts$", () => {
|
||||
it("should return all logged in accounts and an add account option when accounts are less than 5", async () => {
|
||||
const accountInfo: AccountInfo = {
|
||||
const accountInfo = mockAccountInfoWith({
|
||||
name: "Test User 1",
|
||||
email: "test1@email.com",
|
||||
emailVerified: true,
|
||||
};
|
||||
});
|
||||
|
||||
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
||||
accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo });
|
||||
@@ -109,11 +109,10 @@ describe("AccountSwitcherService", () => {
|
||||
const seedAccounts: Record<UserId, AccountInfo> = {};
|
||||
const seedStatuses: Record<UserId, AuthenticationStatus> = {};
|
||||
for (let i = 0; i < numberOfAccounts; i++) {
|
||||
seedAccounts[`${i}` as UserId] = {
|
||||
seedAccounts[`${i}` as UserId] = mockAccountInfoWith({
|
||||
email: `test${i}@email.com`,
|
||||
emailVerified: true,
|
||||
name: "Test User ${i}",
|
||||
};
|
||||
});
|
||||
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
|
||||
}
|
||||
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
||||
@@ -133,11 +132,10 @@ describe("AccountSwitcherService", () => {
|
||||
);
|
||||
|
||||
it("excludes logged out accounts", async () => {
|
||||
const user1AccountInfo: AccountInfo = {
|
||||
const user1AccountInfo = mockAccountInfoWith({
|
||||
name: "Test User 1",
|
||||
email: "",
|
||||
emailVerified: true,
|
||||
};
|
||||
});
|
||||
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
|
||||
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
|
||||
accountsSubject.next({
|
||||
|
||||
@@ -128,6 +128,20 @@
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
|
||||
<bit-section *ngIf="phishingDetectionAvailable$ | async">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "phishingBlocker" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<bit-switch formControlName="enablePhishingDetection" id="phishingDetectionAction">
|
||||
<bit-label for="phishingDetectionAction">{{
|
||||
"enablePhishingDetection" | i18n
|
||||
}}</bit-label>
|
||||
<bit-hint>{{ "enablePhishingDetectionDesc" | i18n }}</bit-hint>
|
||||
</bit-switch>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
|
||||
<bit-section disableMargin>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "otherOptions" | i18n }}</h2>
|
||||
|
||||
@@ -3,9 +3,10 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { firstValueFrom, of, BehaviorSubject } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { NudgesService } from "@bitwarden/angular/vault";
|
||||
import { LockService } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -14,12 +15,15 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
VaultTimeoutAction,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -27,12 +31,12 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
@@ -54,18 +58,27 @@ describe("AccountSecurityComponent", () => {
|
||||
let component: AccountSecurityComponent;
|
||||
let fixture: ComponentFixture<AccountSecurityComponent>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockUserId = newGuid() as UserId;
|
||||
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const billingService = mock<BillingAccountProfileStateService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const policyService = mock<PolicyService>();
|
||||
const pinServiceAbstraction = mock<PinServiceAbstraction>();
|
||||
const keyService = mock<KeyService>();
|
||||
const validationService = mock<ValidationService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
const lockService = mock<LockService>();
|
||||
const configService = mock<ConfigService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const keyService = mock<KeyService>();
|
||||
const lockService = mock<LockService>();
|
||||
const policyService = mock<PolicyService>();
|
||||
const phishingDetectionSettingsService = mock<PhishingDetectionSettingsServiceAbstraction>();
|
||||
const pinServiceAbstraction = mock<PinServiceAbstraction>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
const validationService = mock<ValidationService>();
|
||||
const vaultNudgesService = mock<NudgesService>();
|
||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
|
||||
// Mock subjects to control the phishing detection observables
|
||||
let phishingAvailableSubject: BehaviorSubject<boolean>;
|
||||
let phishingEnabledSubject: BehaviorSubject<boolean>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -73,29 +86,38 @@ describe("AccountSecurityComponent", () => {
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: AccountSecurityComponent, useValue: mock<AccountSecurityComponent>() },
|
||||
{ provide: ActivatedRoute, useValue: mock<ActivatedRoute>() },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: billingService,
|
||||
},
|
||||
{ provide: BiometricsService, useValue: mock<BiometricsService>() },
|
||||
{ provide: BiometricStateService, useValue: biometricStateService },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: MessageSender, useValue: mock<MessageSender>() },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: LockService, useValue: lockService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: MessageSender, useValue: mock<MessageSender>() },
|
||||
{ provide: NudgesService, useValue: vaultNudgesService },
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
{ provide: PinServiceAbstraction, useValue: pinServiceAbstraction },
|
||||
{
|
||||
provide: PhishingDetectionSettingsServiceAbstraction,
|
||||
useValue: phishingDetectionSettingsService,
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||
{ provide: StateProvider, useValue: mock<StateProvider>() },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||
{ provide: StateProvider, useValue: mock<StateProvider>() },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: ApiService, useValue: mock<ApiService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: ValidationService, useValue: validationService },
|
||||
{ provide: LockService, useValue: lockService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(AccountSecurityComponent, {
|
||||
@@ -110,10 +132,13 @@ describe("AccountSecurityComponent", () => {
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccountSecurityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
apiService.getProfile.mockResolvedValue(
|
||||
mock<ProfileResponse>({
|
||||
id: mockUserId,
|
||||
creationDate: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
vaultNudgesService.showNudgeSpotlight$.mockReturnValue(of(false));
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutStringType.OnLocked),
|
||||
);
|
||||
@@ -123,8 +148,25 @@ describe("AccountSecurityComponent", () => {
|
||||
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutAction.Lock),
|
||||
);
|
||||
vaultTimeoutSettingsService.availableVaultTimeoutActions$.mockReturnValue(of([]));
|
||||
biometricStateService.promptAutomatically$ = of(false);
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
billingService.hasPremiumPersonally$.mockReturnValue(of(true));
|
||||
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
|
||||
// Mock readonly observables for phishing detection using BehaviorSubjects so
|
||||
// tests can push different values after component creation.
|
||||
phishingAvailableSubject = new BehaviorSubject<boolean>(true);
|
||||
phishingEnabledSubject = new BehaviorSubject<boolean>(true);
|
||||
|
||||
(phishingDetectionSettingsService.available$ as any) = phishingAvailableSubject.asObservable();
|
||||
(phishingDetectionSettingsService.enabled$ as any) = phishingEnabledSubject.asObservable();
|
||||
|
||||
fixture = TestBed.createComponent(AccountSecurityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -233,6 +275,59 @@ describe("AccountSecurityComponent", () => {
|
||||
expect(pinInputElement).toBeNull();
|
||||
});
|
||||
|
||||
describe("phishing detection UI and setting", () => {
|
||||
it("updates phishing detection setting when form value changes", async () => {
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
|
||||
phishingAvailableSubject.next(true);
|
||||
phishingEnabledSubject.next(true);
|
||||
|
||||
// Init component
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial form value should match enabled$ observable defaulting to true
|
||||
expect(component.form.controls.enablePhishingDetection.value).toBe(true);
|
||||
|
||||
// Change the form value to false
|
||||
component.form.controls.enablePhishingDetection.setValue(false);
|
||||
fixture.detectChanges();
|
||||
// Wait briefly to allow any debounced or async valueChanges handlers to run
|
||||
// fixture.whenStable() does not work here
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(phishingDetectionSettingsService.setEnabled).toHaveBeenCalledWith(mockUserId, false);
|
||||
});
|
||||
|
||||
it("shows phishing detection element when available$ is true", async () => {
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
phishingAvailableSubject.next(true);
|
||||
phishingEnabledSubject.next(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const phishingDetectionElement = fixture.debugElement.query(
|
||||
By.css("#phishingDetectionAction"),
|
||||
);
|
||||
expect(phishingDetectionElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it("hides phishing detection element when available$ is false", async () => {
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
phishingAvailableSubject.next(false);
|
||||
phishingEnabledSubject.next(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const phishingDetectionElement = fixture.debugElement.query(
|
||||
By.css("#phishingDetectionAction"),
|
||||
);
|
||||
expect(phishingDetectionElement).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateBiometric", () => {
|
||||
let browserApiSpy: jest.SpyInstance;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
ToastService,
|
||||
SwitchComponent,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
KeyService,
|
||||
@@ -110,6 +112,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
SpotlightComponent,
|
||||
TypographyModule,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
SwitchComponent,
|
||||
],
|
||||
})
|
||||
export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
@@ -130,6 +133,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
pinLockWithMasterPassword: false,
|
||||
biometric: false,
|
||||
enableAutoBiometricsPrompt: true,
|
||||
enablePhishingDetection: true,
|
||||
});
|
||||
|
||||
protected showAccountSecurityNudge$: Observable<boolean> =
|
||||
@@ -141,6 +145,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
protected readonly consolidatedSessionTimeoutComponent$: Observable<boolean>;
|
||||
protected readonly phishingDetectionAvailable$: Observable<boolean>;
|
||||
|
||||
protected refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||
private destroy$ = new Subject<void>();
|
||||
@@ -167,10 +172,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
private vaultNudgesService: NudgesService,
|
||||
private validationService: ValidationService,
|
||||
private logService: LogService,
|
||||
private phishingDetectionSettingsService: PhishingDetectionSettingsServiceAbstraction,
|
||||
) {
|
||||
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ConsolidatedSessionTimeoutComponent,
|
||||
);
|
||||
|
||||
// Check if user phishing detection available
|
||||
this.phishingDetectionAvailable$ = this.phishingDetectionSettingsService.available$;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -251,6 +260,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
),
|
||||
enablePhishingDetection: await firstValueFrom(this.phishingDetectionSettingsService.enabled$),
|
||||
};
|
||||
this.form.patchValue(initialValues, { emitEvent: false });
|
||||
|
||||
@@ -361,6 +371,16 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.form.controls.enablePhishingDetection.valueChanges
|
||||
.pipe(
|
||||
concatMap(async (enabled) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.phishingDetectionSettingsService.setEnabled(userId, enabled);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.refreshTimeoutSettings$
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
|
||||
@@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -80,11 +81,12 @@ describe("NotificationBackground", () => {
|
||||
const organizationService = mock<OrganizationService>();
|
||||
|
||||
const userId = "testId" as UserId;
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
const activeAccountSubject = new BehaviorSubject({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -123,9 +124,10 @@ describe("context-menu", () => {
|
||||
autofillSettingsService.enableContextMenu$ = of(true);
|
||||
accountService.activeAccount$ = of({
|
||||
id: "userId" as UserId,
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
name: undefined,
|
||||
...mockAccountInfoWith({
|
||||
email: "",
|
||||
name: undefined,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ import {
|
||||
import { isUrlInList } from "@bitwarden/common/autofill/utils";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import { PhishingDetectionSettingsService } from "@bitwarden/common/dirt/services/phishing-detection/phishing-detection-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import {
|
||||
@@ -497,6 +499,7 @@ export default class MainBackground {
|
||||
|
||||
// DIRT
|
||||
private phishingDataService: PhishingDataService;
|
||||
private phishingDetectionSettingsService: PhishingDetectionSettingsServiceAbstraction;
|
||||
|
||||
constructor() {
|
||||
const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) =>
|
||||
@@ -1475,12 +1478,18 @@ export default class MainBackground {
|
||||
this.platformUtilsService,
|
||||
);
|
||||
|
||||
PhishingDetectionService.initialize(
|
||||
this.phishingDetectionSettingsService = new PhishingDetectionSettingsService(
|
||||
this.accountService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.configService,
|
||||
this.organizationService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
PhishingDetectionService.initialize(
|
||||
this.logService,
|
||||
this.phishingDataService,
|
||||
this.phishingDetectionSettingsService,
|
||||
messageListener,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessageListener } from "@bitwarden/messaging";
|
||||
|
||||
@@ -11,17 +9,12 @@ import { PhishingDataService } from "./phishing-data.service";
|
||||
import { PhishingDetectionService } from "./phishing-detection.service";
|
||||
|
||||
describe("PhishingDetectionService", () => {
|
||||
let accountService: AccountService;
|
||||
let billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||
let configService: ConfigService;
|
||||
let logService: LogService;
|
||||
let phishingDataService: MockProxy<PhishingDataService>;
|
||||
let messageListener: MockProxy<MessageListener>;
|
||||
let phishingDetectionSettingsService: MockProxy<PhishingDetectionSettingsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = { getAccount$: jest.fn(() => of(null)) } as any;
|
||||
billingAccountProfileStateService = {} as any;
|
||||
configService = { getFeatureFlag$: jest.fn(() => of(false)) } as any;
|
||||
logService = { info: jest.fn(), debug: jest.fn(), warning: jest.fn(), error: jest.fn() } as any;
|
||||
phishingDataService = mock();
|
||||
messageListener = mock<MessageListener>({
|
||||
@@ -29,16 +22,17 @@ describe("PhishingDetectionService", () => {
|
||||
return new Observable();
|
||||
},
|
||||
});
|
||||
phishingDetectionSettingsService = mock<PhishingDetectionSettingsServiceAbstraction>({
|
||||
on$: of(true),
|
||||
});
|
||||
});
|
||||
|
||||
it("should initialize without errors", () => {
|
||||
expect(() => {
|
||||
PhishingDetectionService.initialize(
|
||||
accountService,
|
||||
billingAccountProfileStateService,
|
||||
configService,
|
||||
logService,
|
||||
phishingDataService,
|
||||
phishingDetectionSettingsService,
|
||||
messageListener,
|
||||
);
|
||||
}).not.toThrow();
|
||||
@@ -61,6 +55,7 @@ describe("PhishingDetectionService", () => {
|
||||
// logService,
|
||||
// phishingDataService,
|
||||
// messageListener,
|
||||
// phishingDetectionSettingsService,
|
||||
// );
|
||||
// });
|
||||
|
||||
@@ -81,6 +76,7 @@ describe("PhishingDetectionService", () => {
|
||||
// logService,
|
||||
// phishingDataService,
|
||||
// messageListener,
|
||||
// phishingDetectionSettingsService,
|
||||
// );
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import {
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
filter,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
|
||||
|
||||
@@ -50,11 +45,9 @@ export class PhishingDetectionService {
|
||||
private static _didInit = false;
|
||||
|
||||
static initialize(
|
||||
accountService: AccountService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
configService: ConfigService,
|
||||
logService: LogService,
|
||||
phishingDataService: PhishingDataService,
|
||||
phishingDetectionSettingsService: PhishingDetectionSettingsServiceAbstraction,
|
||||
messageListener: MessageListener,
|
||||
) {
|
||||
if (this._didInit) {
|
||||
@@ -118,22 +111,9 @@ export class PhishingDetectionService {
|
||||
.messages$(PHISHING_DETECTION_CANCEL_COMMAND)
|
||||
.pipe(switchMap((message) => BrowserApi.closeTab(message.tabId)));
|
||||
|
||||
const activeAccountHasAccess$ = combineLatest([
|
||||
accountService.activeAccount$,
|
||||
configService.getFeatureFlag$(FeatureFlag.PhishingDetection),
|
||||
]).pipe(
|
||||
switchMap(([account, featureEnabled]) => {
|
||||
if (!account) {
|
||||
logService.debug("[PhishingDetectionService] No active account.");
|
||||
return of(false);
|
||||
}
|
||||
return billingAccountProfileStateService
|
||||
.hasPremiumFromAnySource$(account.id)
|
||||
.pipe(map((hasPremium) => hasPremium && featureEnabled));
|
||||
}),
|
||||
);
|
||||
const phishingDetectionActive$ = phishingDetectionSettingsService.on$;
|
||||
|
||||
const initSub = activeAccountHasAccess$
|
||||
const initSub = phishingDetectionActive$
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((activeUserHasAccess) => {
|
||||
|
||||
@@ -120,8 +120,8 @@ export class PopupRouterCacheService {
|
||||
/**
|
||||
* Navigate back in history
|
||||
*/
|
||||
async back() {
|
||||
if (!BrowserPopupUtils.inPopup(window)) {
|
||||
async back(updateCache = false) {
|
||||
if (!updateCache && !BrowserPopupUtils.inPopup(window)) {
|
||||
this.location.back();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
<popup-page [disablePadding]="true">
|
||||
<popup-header
|
||||
slot="header"
|
||||
[background]="'alt'"
|
||||
[showBackButton]="showBackButton"
|
||||
[pageTitle]="''"
|
||||
>
|
||||
<div class="tw-w-32">
|
||||
<bit-icon *ngIf="showLogo" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
|
||||
</div>
|
||||
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<auth-anon-layout
|
||||
[title]="pageTitle"
|
||||
[subtitle]="pageSubtitle"
|
||||
[icon]="pageIcon"
|
||||
[showReadonlyHostname]="showReadonlyHostname"
|
||||
[hideLogo]="true"
|
||||
[hideLogo]="!showLogo"
|
||||
[maxWidth]="maxWidth"
|
||||
[hideFooter]="hideFooter"
|
||||
[hideCardWrapper]="hideCardWrapper"
|
||||
>
|
||||
<router-outlet></router-outlet>
|
||||
<div class="tw-flex tw-gap-2" slot="header-actions">
|
||||
<app-pop-out></app-pop-out>
|
||||
@if (showAcctSwitcher && hasLoggedInAccount) {
|
||||
<app-current-account></app-current-account>
|
||||
}
|
||||
</div>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
<router-outlet slot="environment-selector" name="environment-selector"></router-outlet>
|
||||
</auth-anon-layout>
|
||||
|
||||
@@ -76,11 +76,14 @@ const decorators = (options: {
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
// We can't use mockAccountInfoWith() here because we can't take a dependency on @bitwarden/common/spec.
|
||||
// This is because that package relies on jest dependencies that aren't available here.
|
||||
activeAccount$: of({
|
||||
id: "test-user-id" as UserId,
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -238,6 +241,11 @@ export const DefaultContentExample: Story = {
|
||||
},
|
||||
],
|
||||
}),
|
||||
parameters: {
|
||||
chromatic: {
|
||||
viewports: [380, 1280],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Dynamic Content Example
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
AccountService,
|
||||
@@ -67,6 +68,8 @@ import {
|
||||
UserNotificationSettingsServiceAbstraction,
|
||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { PhishingDetectionSettingsService } from "@bitwarden/common/dirt/services/phishing-detection/phishing-detection-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
@@ -512,6 +515,17 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: UserNotificationSettingsService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PhishingDetectionSettingsServiceAbstraction,
|
||||
useClass: PhishingDetectionSettingsService,
|
||||
deps: [
|
||||
AccountService,
|
||||
BillingAccountProfileStateService,
|
||||
ConfigService,
|
||||
OrganizationService,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageListener,
|
||||
useFactory: (subject: Subject<Message<Record<string, unknown>>>, ngZone: NgZone) =>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
@@ -96,9 +97,10 @@ describe("SendV2Component", () => {
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
id: "123",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'exportVault' | i18n" showBackButton>
|
||||
<popup-header slot="header" [pageTitle]="'export' | i18n" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
@@ -21,7 +21,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "exportVault" | i18n }}
|
||||
{{ "export" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" [popupBackAction]>
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'importData' | i18n" showBackButton>
|
||||
<popup-header slot="header" [pageTitle]="'import' | i18n" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
@@ -22,7 +22,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "importData" | i18n }}
|
||||
{{ "import" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
|
||||
@@ -51,6 +51,6 @@ export class AttachmentsV2Component {
|
||||
|
||||
/** Navigate the user back to the edit screen after uploading an attachment */
|
||||
async navigateBack() {
|
||||
await this.popupRouterCacheService.back();
|
||||
await this.popupRouterCacheService.back(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
@@ -60,9 +61,10 @@ describe("OpenAttachmentsComponent", () => {
|
||||
const accountService = {
|
||||
activeAccount$: of({
|
||||
id: mockUserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
}),
|
||||
};
|
||||
const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled");
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<bit-item>
|
||||
<button type="button" bit-item-content (click)="import()">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-gap-2">
|
||||
<p>{{ "importItems" | i18n }}</p>
|
||||
<p>{{ "import" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="emptyVaultImportBadge$ | async"
|
||||
bitBadge
|
||||
@@ -30,7 +30,7 @@
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/export">
|
||||
{{ "exportVault" | i18n }}
|
||||
{{ "export" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
@@ -64,7 +64,7 @@
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button type="button" bit-item-content (click)="sync()">
|
||||
{{ "syncVaultNow" | i18n }}
|
||||
{{ "syncNow" | i18n }}
|
||||
<span slot="secondary">{{ lastSync }}</span>
|
||||
<i slot="end" class="bwi bwi-refresh" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -48,9 +49,10 @@ describe("UnlockCommand", () => {
|
||||
const mockMasterPassword = "testExample";
|
||||
const activeAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "user@example.com",
|
||||
emailVerified: true,
|
||||
name: "User",
|
||||
...mockAccountInfoWith({
|
||||
email: "user@example.com",
|
||||
name: "User",
|
||||
}),
|
||||
};
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockSessionKey = new Uint8Array(64) as CsprngArray;
|
||||
|
||||
@@ -113,8 +113,8 @@ if (process.platform === "linux") {
|
||||
|
||||
platformTargets.forEach(([target, _]) => {
|
||||
installTarget(target);
|
||||
buildNapiModule(target);
|
||||
buildProxyBin(target);
|
||||
buildImporterBinaries(target);
|
||||
buildNapiModule(target, mode === "release");
|
||||
buildProxyBin(target, mode === "release");
|
||||
buildImporterBinaries(target, mode === "release");
|
||||
buildProcessIsolation();
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: &[".config/google-chrome"],
|
||||
data_dir: &[".config/google-chrome", "snap/chromium/common/chromium"],
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Chromium",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "napi build --platform --no-js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "cargo test"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
22
apps/desktop/desktop_native/napi/scripts/build.js
Normal file
22
apps/desktop/desktop_native/napi/scripts/build.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const isRelease = args.includes('--release');
|
||||
|
||||
const argsString = args.join(' ');
|
||||
|
||||
if (isRelease) {
|
||||
console.log('Building release mode.');
|
||||
|
||||
execSync(`napi build --platform --no-js ${argsString}`, { stdio: 'inherit'});
|
||||
|
||||
} else {
|
||||
console.log('Building debug mode.');
|
||||
|
||||
execSync(`napi build --platform --no-js ${argsString}`, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, RUST_LOG: 'debug' }
|
||||
});
|
||||
}
|
||||
@@ -994,7 +994,7 @@ pub mod logging {
|
||||
};
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::{
|
||||
filter::{EnvFilter, LevelFilter},
|
||||
filter::EnvFilter,
|
||||
fmt::format::{DefaultVisitor, Writer},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
@@ -1082,9 +1082,17 @@ pub mod logging {
|
||||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<FnArgs<(LogLevel, String)>>) {
|
||||
let _ = JS_LOGGER.0.set(js_log_fn);
|
||||
|
||||
// the log level hierarchy is determined by:
|
||||
// - if RUST_LOG is detected at runtime
|
||||
// - if RUST_LOG is provided at compile time
|
||||
// - default to INFO
|
||||
let filter = EnvFilter::builder()
|
||||
// set the default log level to INFO.
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.with_default_directive(
|
||||
option_env!("RUST_LOG")
|
||||
.unwrap_or("info")
|
||||
.parse()
|
||||
.expect("should provide valid log level at compile time."),
|
||||
)
|
||||
// parse directives from the RUST_LOG environment variable,
|
||||
// overriding the default directive for matching targets.
|
||||
.from_env_lossy();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -203,8 +203,16 @@ const safeProviders: SafeProvider[] = [
|
||||
// We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid
|
||||
// the TokenService having to inject the PlatformUtilsService which introduces a
|
||||
// circular dependency on Desktop only.
|
||||
//
|
||||
// For Windows portable builds, we disable secure storage to ensure tokens are
|
||||
// stored on disk (in bitwarden-appdata) rather than in Windows Credential
|
||||
// Manager, making them portable across machines. This allows users to move the USB drive
|
||||
// between computers while maintaining authentication.
|
||||
//
|
||||
// Note: Portable mode does not use secure storage for read/write/clear operations,
|
||||
// preventing any collision with tokens from a regular desktop installation.
|
||||
provide: SUPPORTS_SECURE_STORAGE,
|
||||
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE,
|
||||
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE && !ipc.platform.isWindowsPortable,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DEFAULT_VAULT_TIMEOUT,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<bit-dialog #dialog dialogSize="large">
|
||||
<span bitDialogTitle>{{ "exportVault" | i18n }}</span>
|
||||
<span bitDialogTitle>{{ "export" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<tools-export
|
||||
(formLoading)="this.loading = $event"
|
||||
@@ -17,7 +17,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "exportVault" | i18n }}
|
||||
{{ "export" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<bit-dialog #dialog dialogSize="large" background="alt">
|
||||
<span bitDialogTitle>{{ "importData" | i18n }}</span>
|
||||
<span bitDialogTitle>{{ "import" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-relative">
|
||||
<tools-import
|
||||
@@ -27,7 +27,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "importData" | i18n }}
|
||||
{{ "import" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AccountService, Account } from "@bitwarden/common/auth/abstractions/acc
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -40,9 +41,10 @@ describe("Fido2CreateComponent", () => {
|
||||
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: "test-user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Account, UserId } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
|
||||
|
||||
@@ -30,9 +31,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
||||
beforeEach(() => {
|
||||
mockAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: mockUserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
mockFeatureFlagSubject = new BehaviorSubject<boolean>(true);
|
||||
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(
|
||||
|
||||
@@ -13,11 +13,13 @@ import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
makeSymmetricCryptoKey,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
FakeStateProvider,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../libs/common/spec";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { VAULT_TIMEOUT } from "../../../../libs/common/src/key-management/vault-timeout";
|
||||
|
||||
import { DesktopBiometricsService } from "./biometrics/desktop.biometrics.service";
|
||||
import { ElectronKeyService } from "./electron-key.service";
|
||||
@@ -40,11 +42,13 @@ describe("ElectronKeyService", () => {
|
||||
let accountService: FakeAccountService;
|
||||
let masterPasswordService: FakeMasterPasswordService;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId);
|
||||
|
||||
keyService = new ElectronKeyService(
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
@@ -79,38 +83,17 @@ describe("ElectronKeyService", () => {
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
describe("biometric unlock enabled", () => {
|
||||
beforeEach(() => {
|
||||
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
|
||||
});
|
||||
it("sets biometric key when biometric unlock enabled", async () => {
|
||||
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
|
||||
|
||||
it("sets null biometric client key half and biometric unlock key when require password on start disabled", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(false);
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
describe("require password on start enabled", () => {
|
||||
beforeEach(() => {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("sets biometric key", async () => {
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1198,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Follow us"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Sync vault"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Change master password"
|
||||
@@ -1775,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Export vault"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "File format"
|
||||
@@ -3492,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import data",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Import error"
|
||||
},
|
||||
|
||||
@@ -146,8 +146,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
|
||||
|
||||
private get syncVault(): MenuItemConstructorOptions {
|
||||
return {
|
||||
id: "syncVault",
|
||||
label: this.localize("syncVault"),
|
||||
id: "syncNow",
|
||||
label: this.localize("syncNow"),
|
||||
click: () => this.sendMessage("syncVault"),
|
||||
enabled: this.hasAuthenticatedAccounts,
|
||||
};
|
||||
@@ -155,8 +155,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
|
||||
|
||||
private get importVault(): MenuItemConstructorOptions {
|
||||
return {
|
||||
id: "importVault",
|
||||
label: this.localize("importData"),
|
||||
id: "import",
|
||||
label: this.localize("import"),
|
||||
click: () => this.sendMessage("importVault"),
|
||||
enabled: !this._isLocked,
|
||||
};
|
||||
@@ -164,8 +164,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
|
||||
|
||||
private get exportVault(): MenuItemConstructorOptions {
|
||||
return {
|
||||
id: "exportVault",
|
||||
label: this.localize("exportVault"),
|
||||
id: "export",
|
||||
label: this.localize("export"),
|
||||
click: () => this.sendMessage("exportVault"),
|
||||
enabled: !this._isLocked,
|
||||
};
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isFlatpak,
|
||||
isMacAppStore,
|
||||
isSnapStore,
|
||||
isWindowsPortable,
|
||||
isWindowsStore,
|
||||
} from "../utils";
|
||||
|
||||
@@ -133,6 +134,7 @@ export default {
|
||||
isDev: isDev(),
|
||||
isMacAppStore: isMacAppStore(),
|
||||
isWindowsStore: isWindowsStore(),
|
||||
isWindowsPortable: isWindowsPortable(),
|
||||
isFlatpak: isFlatpak(),
|
||||
isSnapStore: isSnapStore(),
|
||||
isAppImage: isAppImage(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NgZone } from "@angular/core";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, filter, firstValueFrom, of, take, timeout, timer } from "rxjs";
|
||||
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
@@ -10,7 +10,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService } from "@bitwarden/common/spec";
|
||||
import { mockAccountInfoWith, FakeAccountService } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@@ -23,17 +23,15 @@ import { BiometricMessageHandlerService } from "./biometric-message-handler.serv
|
||||
|
||||
const SomeUser = "SomeUser" as UserId;
|
||||
const AnotherUser = "SomeOtherUser" as UserId;
|
||||
const accounts: Record<UserId, AccountInfo> = {
|
||||
[SomeUser]: {
|
||||
const accounts = {
|
||||
[SomeUser]: mockAccountInfoWith({
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
[AnotherUser]: {
|
||||
}),
|
||||
[AnotherUser]: mockAccountInfoWith({
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
describe("BiometricMessageHandlerService", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -104,12 +104,12 @@
|
||||
*ngIf="organization.use2fa && organization.isOwner"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'importData' | i18n"
|
||||
[text]="'import' | i18n"
|
||||
route="settings/tools/import"
|
||||
*ngIf="organization.canAccessImport"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'exportVault' | i18n"
|
||||
[text]="'export' | i18n"
|
||||
route="settings/tools/export"
|
||||
*ngIf="canAccessExport$ | async"
|
||||
></bit-nav-item>
|
||||
|
||||
@@ -57,7 +57,7 @@ const routes: Routes = [
|
||||
),
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessImport)],
|
||||
data: {
|
||||
titleId: "importData",
|
||||
titleId: "import",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -68,7 +68,7 @@ const routes: Routes = [
|
||||
),
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessExport)],
|
||||
data: {
|
||||
titleId: "exportVault",
|
||||
titleId: "export",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DeleteRecoverRequest } from "@bitwarden/common/models/request/delete-recover.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-recover-delete",
|
||||
templateUrl: "recover-delete.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
JslibModule,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
I18nPipe,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class RecoverDeleteComponent {
|
||||
protected recoverDeleteForm = new FormGroup({
|
||||
@@ -29,7 +45,6 @@ export class RecoverDeleteComponent {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { Router, provideRouter } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
@@ -7,69 +7,49 @@ import {
|
||||
LoginSuccessHandlerService,
|
||||
PasswordLoginCredentials,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { RecoverTwoFactorComponent } from "./recover-two-factor.component";
|
||||
|
||||
describe("RecoverTwoFactorComponent", () => {
|
||||
let component: RecoverTwoFactorComponent;
|
||||
let fixture: ComponentFixture<RecoverTwoFactorComponent>;
|
||||
|
||||
// Mock Services
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockKeyService: MockProxy<KeyService>;
|
||||
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockValidationService: MockProxy<ValidationService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRouter = mock<Router>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
beforeEach(async () => {
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockKeyService = mock<KeyService>();
|
||||
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockLoginSuccessHandlerService = mock<LoginSuccessHandlerService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockValidationService = mock<ValidationService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [RecoverTwoFactorComponent],
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RecoverTwoFactorComponent],
|
||||
providers: [
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: PlatformUtilsService, mockPlatformUtilsService },
|
||||
provideRouter([]),
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: KeyService, useValue: mockKeyService },
|
||||
{ provide: LoginStrategyServiceAbstraction, useValue: mockLoginStrategyService },
|
||||
{ provide: ToastService, useValue: mockToastService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: ValidationService, useValue: mockValidationService },
|
||||
],
|
||||
imports: [I18nPipe],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
errorOnUnknownElements: false,
|
||||
});
|
||||
}).compileComponents();
|
||||
|
||||
mockRouter = TestBed.inject(Router) as MockProxy<Router>;
|
||||
jest.spyOn(mockRouter, "navigate");
|
||||
|
||||
fixture = TestBed.createComponent(RecoverTwoFactorComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, DestroyRef, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
PasswordLoginCredentials,
|
||||
@@ -14,14 +15,32 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-recover-two-factor",
|
||||
templateUrl: "recover-two-factor.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
JslibModule,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
I18nPipe,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class RecoverTwoFactorComponent implements OnInit {
|
||||
formGroup = new FormGroup({
|
||||
|
||||
@@ -10,7 +10,6 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
|
||||
import { VerifyEmailRequest } from "@bitwarden/common/models/request/verify-email.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -18,12 +17,10 @@ import { ToastService } from "@bitwarden/components";
|
||||
@Component({
|
||||
selector: "app-verify-email-token",
|
||||
templateUrl: "verify-email-token.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VerifyEmailTokenComponent implements OnInit {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormGroup } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VerifyDeleteRecoverRequest } from "@bitwarden/common/models/request/verify-delete-recover.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutComponent,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-verify-recover-delete",
|
||||
templateUrl: "verify-recover-delete.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
JslibModule,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutComponent,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class VerifyRecoverDeleteComponent implements OnInit {
|
||||
email: string;
|
||||
@@ -28,7 +42,6 @@ export class VerifyRecoverDeleteComponent implements OnInit {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private toastService: ToastService,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
PersonalSubscriptionPricingTierId,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogRef } from "@bitwarden/components";
|
||||
|
||||
@@ -63,9 +64,10 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultDialogData: UnifiedUpgradeDialogParams = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogRef, DialogService } from "@bitwarden/components";
|
||||
@@ -32,9 +33,10 @@ describe("UpgradeNavButtonComponent", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
@@ -46,11 +47,12 @@ describe("UpgradePaymentService", () => {
|
||||
|
||||
let sut: UpgradePaymentService;
|
||||
|
||||
const mockAccount = {
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const mockTokenizedPaymentMethod: TokenizedPaymentMethod = {
|
||||
@@ -151,9 +153,10 @@ describe("UpgradePaymentService", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const paidOrgData = {
|
||||
@@ -203,9 +206,10 @@ describe("UpgradePaymentService", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const paidOrgData = {
|
||||
@@ -255,9 +259,10 @@ describe("UpgradePaymentService", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
mockAccountService.activeAccount$ = of(mockAccount);
|
||||
@@ -289,9 +294,10 @@ describe("UpgradePaymentService", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
const expectedCredit = 25.5;
|
||||
|
||||
@@ -353,9 +359,10 @@ describe("UpgradePaymentService", () => {
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const paidOrgData = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BreachAccountResponse } from "@bitwarden/common/dirt/models/response/breach-account.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { BreachReportComponent } from "./breach-report.component";
|
||||
@@ -38,9 +39,10 @@ describe("BreachReportComponent", () => {
|
||||
let accountService: MockProxy<AccountService>;
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -286,9 +287,10 @@ describe("KeyRotationService", () => {
|
||||
|
||||
const mockUser = {
|
||||
id: "mockUserId" as UserId,
|
||||
email: "mockEmail",
|
||||
emailVerified: true,
|
||||
name: "mockName",
|
||||
...mockAccountInfoWith({
|
||||
email: "mockEmail",
|
||||
name: "mockName",
|
||||
}),
|
||||
};
|
||||
|
||||
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")];
|
||||
|
||||
@@ -75,11 +75,14 @@ class MockSyncService implements Partial<SyncService> {
|
||||
}
|
||||
|
||||
class MockAccountService implements Partial<AccountService> {
|
||||
// We can't use mockAccountInfoWith() here because we can't take a dependency on @bitwarden/common/spec.
|
||||
// This is because that package relies on jest dependencies that aren't available here.
|
||||
activeAccount$?: Observable<Account> = of({
|
||||
id: "test-user-id" as UserId,
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -75,11 +75,14 @@ class MockSyncService implements Partial<SyncService> {
|
||||
}
|
||||
|
||||
class MockAccountService implements Partial<AccountService> {
|
||||
// We can't use mockAccountInfoWith() here because we can't take a dependency on @bitwarden/common/spec.
|
||||
// This is because that package relies on jest dependencies that aren't available here.
|
||||
activeAccount$?: Observable<Account> = of({
|
||||
id: "test-user-id" as UserId,
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<bit-nav-item icon="bwi-send" [text]="'send' | i18n" route="sends"></bit-nav-item>
|
||||
<bit-nav-group icon="bwi-wrench" [text]="'tools' | i18n" route="tools">
|
||||
<bit-nav-item [text]="'generator' | i18n" route="tools/generator"></bit-nav-item>
|
||||
<bit-nav-item [text]="'importData' | i18n" route="tools/import"></bit-nav-item>
|
||||
<bit-nav-item [text]="'exportVault' | i18n" route="tools/export"></bit-nav-item>
|
||||
<bit-nav-item [text]="'import' | i18n" route="tools/import"></bit-nav-item>
|
||||
<bit-nav-item [text]="'export' | i18n" route="tools/export"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item icon="bwi-sliders" [text]="'reports' | i18n" route="reports"></bit-nav-item>
|
||||
<bit-nav-group icon="bwi-cog" [text]="'settings' | i18n" route="settings">
|
||||
|
||||
@@ -748,7 +748,7 @@ const routes: Routes = [
|
||||
loadComponent: () =>
|
||||
import("./tools/import/import-web.component").then((mod) => mod.ImportWebComponent),
|
||||
data: {
|
||||
titleId: "importData",
|
||||
titleId: "import",
|
||||
} satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
@@ -758,7 +758,7 @@ const routes: Routes = [
|
||||
(mod) => mod.ExportWebComponent,
|
||||
),
|
||||
data: {
|
||||
titleId: "exportVault",
|
||||
titleId: "export",
|
||||
} satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
|
||||
import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component";
|
||||
import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component";
|
||||
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
|
||||
@@ -18,20 +14,10 @@ import { SharedModule } from "./shared.module";
|
||||
@NgModule({
|
||||
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
|
||||
declarations: [
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
],
|
||||
exports: [
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
],
|
||||
exports: [SponsoredFamiliesComponent],
|
||||
})
|
||||
export class LooseComponentsModule {}
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "importData" | i18n }}
|
||||
{{ "import" | i18n }}
|
||||
</button>
|
||||
</bit-container>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "importData" | i18n }}
|
||||
{{ "import" | i18n }}
|
||||
</button>
|
||||
</bit-container>
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "confirmFormat" | i18n }}
|
||||
{{ "export" | i18n }}
|
||||
</button>
|
||||
</bit-container>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "confirmFormat" | i18n }}
|
||||
{{ "export" | i18n }}
|
||||
</button>
|
||||
</bit-container>
|
||||
|
||||
@@ -2,14 +2,18 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
@@ -27,8 +31,11 @@ describe("VaultBannersService", () => {
|
||||
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
const getEmailVerified = jest.fn().mockResolvedValue(true);
|
||||
const lastSync$ = new BehaviorSubject<Date | null>(null);
|
||||
const accounts$ = new BehaviorSubject<Record<UserId, AccountInfo>>({
|
||||
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
|
||||
const accounts$ = new BehaviorSubject({
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "test@bitwarden.com",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
const pendingAuthRequests$ = new BehaviorSubject<Array<AuthRequestResponse>>([]);
|
||||
|
||||
|
||||
@@ -1975,12 +1975,6 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Export vault"
|
||||
},
|
||||
"exportSecrets": {
|
||||
"message": "Export secrets"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "File format"
|
||||
},
|
||||
@@ -1993,9 +1987,6 @@
|
||||
"confirmMasterPassword": {
|
||||
"message": "Confirm master password"
|
||||
},
|
||||
"confirmFormat": {
|
||||
"message": "Confirm format"
|
||||
},
|
||||
"filePassword": {
|
||||
"message": "File password"
|
||||
},
|
||||
@@ -2306,6 +2297,9 @@
|
||||
"tools": {
|
||||
"message": "Tools"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import data"
|
||||
},
|
||||
@@ -8757,9 +8751,6 @@
|
||||
"server": {
|
||||
"message": "Server"
|
||||
},
|
||||
"exportData": {
|
||||
"message": "Export data"
|
||||
},
|
||||
"exportingOrganizationSecretDataTitle": {
|
||||
"message": "Exporting Organization Secret Data"
|
||||
},
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { OrganizationIntegrationServiceType } from "../organization-integration-service-type";
|
||||
import { OrgIntegrationConfiguration } from "../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../organization-integration-service-type";
|
||||
|
||||
export class DatadogConfiguration {
|
||||
export class DatadogConfiguration implements OrgIntegrationConfiguration {
|
||||
uri: string;
|
||||
apiKey: string;
|
||||
service: OrganizationIntegrationServiceType;
|
||||
service: OrganizationIntegrationServiceName;
|
||||
|
||||
constructor(uri: string, apiKey: string, service: string) {
|
||||
constructor(uri: string, apiKey: string, service: OrganizationIntegrationServiceName) {
|
||||
this.uri = uri;
|
||||
this.apiKey = apiKey;
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { OrganizationIntegrationServiceType } from "../organization-integration-service-type";
|
||||
import { OrgIntegrationConfiguration } from "../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../organization-integration-service-type";
|
||||
|
||||
export class HecConfiguration {
|
||||
export class HecConfiguration implements OrgIntegrationConfiguration {
|
||||
uri: string;
|
||||
scheme = "Bearer";
|
||||
token: string;
|
||||
service: OrganizationIntegrationServiceType;
|
||||
service: OrganizationIntegrationServiceName;
|
||||
|
||||
constructor(uri: string, token: string, service: string) {
|
||||
constructor(uri: string, token: string, service: OrganizationIntegrationServiceName) {
|
||||
this.uri = uri;
|
||||
this.token = token;
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { OrgIntegrationConfiguration } from "../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../organization-integration-service-type";
|
||||
|
||||
// Added to reflect how future webhook integrations could be structured within the OrganizationIntegration
|
||||
export class WebhookConfiguration {
|
||||
export class WebhookConfiguration implements OrgIntegrationConfiguration {
|
||||
propA: string;
|
||||
propB: string;
|
||||
service: OrganizationIntegrationServiceName;
|
||||
|
||||
constructor(propA: string, propB: string) {
|
||||
constructor(propA: string, propB: string, service: OrganizationIntegrationServiceName) {
|
||||
this.propA = propA;
|
||||
this.propB = propB;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { DatadogConfiguration } from "./configuration/datadog-configuration";
|
||||
import { HecConfiguration } from "./configuration/hec-configuration";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Defines the structure for organization integration configuration
|
||||
*/
|
||||
export interface OrgIntegrationConfiguration {
|
||||
service: OrganizationIntegrationServiceName;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the structure for organization integration template
|
||||
*/
|
||||
export interface OrgIntegrationTemplate {
|
||||
service: OrganizationIntegrationServiceName;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for creating organization integration configurations and templates
|
||||
*/
|
||||
export class OrgIntegrationBuilder {
|
||||
static buildHecConfiguration(
|
||||
uri: string,
|
||||
token: string,
|
||||
service: OrganizationIntegrationServiceName,
|
||||
): OrgIntegrationConfiguration {
|
||||
return new HecConfiguration(uri, token, service);
|
||||
}
|
||||
|
||||
static buildHecTemplate(
|
||||
index: string,
|
||||
service: OrganizationIntegrationServiceName,
|
||||
): OrgIntegrationTemplate {
|
||||
return new HecTemplate(index, service);
|
||||
}
|
||||
|
||||
static buildDataDogConfiguration(uri: string, apiKey: string): OrgIntegrationConfiguration {
|
||||
return new DatadogConfiguration(uri, apiKey, OrganizationIntegrationServiceName.Datadog);
|
||||
}
|
||||
|
||||
static buildDataDogTemplate(service: OrganizationIntegrationServiceName): OrgIntegrationTemplate {
|
||||
return new DatadogTemplate(service);
|
||||
}
|
||||
|
||||
static buildConfiguration(
|
||||
type: OrganizationIntegrationType,
|
||||
configuration: string,
|
||||
): OrgIntegrationConfiguration {
|
||||
switch (type) {
|
||||
case OrganizationIntegrationType.Hec: {
|
||||
const hecConfig = this.convertToJson<HecConfiguration>(configuration);
|
||||
return this.buildHecConfiguration(hecConfig.uri, hecConfig.token, hecConfig.service);
|
||||
}
|
||||
case OrganizationIntegrationType.Datadog: {
|
||||
const datadogConfig = this.convertToJson<DatadogConfiguration>(configuration);
|
||||
return this.buildDataDogConfiguration(datadogConfig.uri, datadogConfig.apiKey);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported integration type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
static buildTemplate(
|
||||
type: OrganizationIntegrationType,
|
||||
template: string,
|
||||
): OrgIntegrationTemplate {
|
||||
switch (type) {
|
||||
case OrganizationIntegrationType.Hec: {
|
||||
const hecTemplate = this.convertToJson<HecTemplate>(template);
|
||||
return this.buildHecTemplate(hecTemplate.index, hecTemplate.service);
|
||||
}
|
||||
case OrganizationIntegrationType.Datadog: {
|
||||
const datadogTemplate = this.convertToJson<DatadogTemplate>(template);
|
||||
return this.buildDataDogTemplate(datadogTemplate.service);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported integration type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static convertToJson<T>(jsonString?: string): T {
|
||||
try {
|
||||
return JSON.parse(jsonString || "{}") as T;
|
||||
} catch {
|
||||
throw new Error("Invalid integration configuration: JSON parse error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { OrganizationIntegrationServiceType } from "../../organization-integration-service-type";
|
||||
import { OrgIntegrationTemplate } from "../../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../../organization-integration-service-type";
|
||||
|
||||
export class DatadogTemplate {
|
||||
export class DatadogTemplate implements OrgIntegrationTemplate {
|
||||
source_type_name = "Bitwarden";
|
||||
title: string = "#Title#";
|
||||
text: string =
|
||||
"ActingUser: #ActingUserId#\nUser: #UserId#\nEvent: #Type#\nOrganization: #OrganizationId#\nPolicyId: #PolicyId#\nIpAddress: #IpAddress#\nDomainName: #DomainName#\nCipherId: #CipherId#\n";
|
||||
service: OrganizationIntegrationServiceType;
|
||||
service: OrganizationIntegrationServiceName;
|
||||
|
||||
constructor(service: string) {
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
constructor(service: OrganizationIntegrationServiceName) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { OrganizationIntegrationServiceType } from "../../organization-integration-service-type";
|
||||
import { OrgIntegrationTemplate } from "../../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../../organization-integration-service-type";
|
||||
|
||||
export class HecTemplate {
|
||||
export class HecTemplate implements OrgIntegrationTemplate {
|
||||
event = "#EventMessage#";
|
||||
source = "Bitwarden";
|
||||
index: string;
|
||||
service: OrganizationIntegrationServiceType;
|
||||
service: OrganizationIntegrationServiceName;
|
||||
|
||||
constructor(index: string, service: string) {
|
||||
constructor(index: string, service: OrganizationIntegrationServiceName) {
|
||||
this.index = index;
|
||||
this.service = service as OrganizationIntegrationServiceType;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { OrgIntegrationTemplate } from "../../integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "../../organization-integration-service-type";
|
||||
|
||||
// Added to reflect how future webhook integrations could be structured within the OrganizationIntegration
|
||||
export class WebhookTemplate {
|
||||
export class WebhookTemplate implements OrgIntegrationTemplate {
|
||||
service: OrganizationIntegrationServiceName;
|
||||
propA: string;
|
||||
propB: string;
|
||||
|
||||
constructor(propA: string, propB: string) {
|
||||
constructor(service: OrganizationIntegrationServiceName, propA: string, propB: string) {
|
||||
this.service = service;
|
||||
this.propA = propA;
|
||||
this.propB = propB;
|
||||
}
|
||||
|
||||
@@ -4,31 +4,25 @@ import {
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogTemplate } from "./integration-configuration-config/configuration-template/datadog-template";
|
||||
import { HecTemplate } from "./integration-configuration-config/configuration-template/hec-template";
|
||||
import { WebhookTemplate } from "./integration-configuration-config/configuration-template/webhook-template";
|
||||
import { WebhookIntegrationConfigurationConfig } from "./integration-configuration-config/webhook-integration-configuration-config";
|
||||
import { OrgIntegrationTemplate } from "./integration-builder";
|
||||
|
||||
export class OrganizationIntegrationConfiguration {
|
||||
id: OrganizationIntegrationConfigurationId;
|
||||
integrationId: OrganizationIntegrationId;
|
||||
eventType?: EventType | null;
|
||||
configuration?: WebhookIntegrationConfigurationConfig | null;
|
||||
filters?: string;
|
||||
template?: HecTemplate | WebhookTemplate | DatadogTemplate | null;
|
||||
template?: OrgIntegrationTemplate | null;
|
||||
|
||||
constructor(
|
||||
id: OrganizationIntegrationConfigurationId,
|
||||
integrationId: OrganizationIntegrationId,
|
||||
eventType?: EventType | null,
|
||||
configuration?: WebhookIntegrationConfigurationConfig | null,
|
||||
filters?: string,
|
||||
template?: HecTemplate | WebhookTemplate | DatadogTemplate | null,
|
||||
template?: OrgIntegrationTemplate | null,
|
||||
) {
|
||||
this.id = id;
|
||||
this.integrationId = integrationId;
|
||||
this.eventType = eventType;
|
||||
this.configuration = configuration;
|
||||
this.filters = filters;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const OrganizationIntegrationServiceType = Object.freeze({
|
||||
export const OrganizationIntegrationServiceName = Object.freeze({
|
||||
CrowdStrike: "CrowdStrike",
|
||||
Datadog: "Datadog",
|
||||
} as const);
|
||||
|
||||
export type OrganizationIntegrationServiceType =
|
||||
(typeof OrganizationIntegrationServiceType)[keyof typeof OrganizationIntegrationServiceType];
|
||||
export type OrganizationIntegrationServiceName =
|
||||
(typeof OrganizationIntegrationServiceName)[keyof typeof OrganizationIntegrationServiceName];
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { OrganizationIntegrationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogConfiguration } from "./configuration/datadog-configuration";
|
||||
import { HecConfiguration } from "./configuration/hec-configuration";
|
||||
import { WebhookConfiguration } from "./configuration/webhook-configuration";
|
||||
import { OrgIntegrationConfiguration } from "./integration-builder";
|
||||
import { OrganizationIntegrationConfiguration } from "./organization-integration-configuration";
|
||||
import { OrganizationIntegrationServiceType } from "./organization-integration-service-type";
|
||||
import { OrganizationIntegrationServiceName } from "./organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "./organization-integration-type";
|
||||
|
||||
export class OrganizationIntegration {
|
||||
id: OrganizationIntegrationId;
|
||||
type: OrganizationIntegrationType;
|
||||
serviceType: OrganizationIntegrationServiceType;
|
||||
configuration: HecConfiguration | WebhookConfiguration | DatadogConfiguration | null;
|
||||
serviceName: OrganizationIntegrationServiceName;
|
||||
configuration: OrgIntegrationConfiguration | null;
|
||||
integrationConfiguration: OrganizationIntegrationConfiguration[] = [];
|
||||
|
||||
constructor(
|
||||
id: OrganizationIntegrationId,
|
||||
type: OrganizationIntegrationType,
|
||||
serviceType: OrganizationIntegrationServiceType,
|
||||
configuration: HecConfiguration | WebhookConfiguration | DatadogConfiguration | null,
|
||||
serviceName: OrganizationIntegrationServiceName,
|
||||
configuration: OrgIntegrationConfiguration | null,
|
||||
integrationConfiguration: OrganizationIntegrationConfiguration[] = [],
|
||||
) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.serviceType = serviceType;
|
||||
this.serviceName = serviceName;
|
||||
this.configuration = configuration;
|
||||
this.integrationConfiguration = integrationConfiguration;
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
OrganizationIntegrationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogConfiguration } from "../models/configuration/datadog-configuration";
|
||||
import { DatadogTemplate } from "../models/integration-configuration-config/configuration-template/datadog-template";
|
||||
import { OrganizationIntegration } from "../models/organization-integration";
|
||||
import { OrganizationIntegrationConfiguration } from "../models/organization-integration-configuration";
|
||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
|
||||
import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "../models/organization-integration-type";
|
||||
|
||||
import { DatadogOrganizationIntegrationService } from "./datadog-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
|
||||
|
||||
describe("DatadogOrganizationIntegrationService", () => {
|
||||
let service: DatadogOrganizationIntegrationService;
|
||||
const mockIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
const mockIntegrationConfigurationApiService =
|
||||
mock<OrganizationIntegrationConfigurationApiService>();
|
||||
const organizationId = "org-1" as OrganizationId;
|
||||
const integrationId = "int-1" as OrganizationIntegrationId;
|
||||
const configId = "conf-1" as OrganizationIntegrationConfigurationId;
|
||||
const serviceType = OrganizationIntegrationServiceType.CrowdStrike;
|
||||
const url = "https://example.com";
|
||||
const apiKey = "token";
|
||||
|
||||
beforeEach(() => {
|
||||
service = new DatadogOrganizationIntegrationService(
|
||||
mockIntegrationApiService,
|
||||
mockIntegrationConfigurationApiService,
|
||||
);
|
||||
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should set organization integrations", (done) => {
|
||||
mockIntegrationApiService.getOrganizationIntegrations.mockResolvedValue([]);
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
const subscription = service.integrations$.subscribe((integrations) => {
|
||||
expect(integrations).toEqual([]);
|
||||
subscription.unsubscribe();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should save a new Datadog integration", async () => {
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
|
||||
const integrationResponse = {
|
||||
id: integrationId,
|
||||
type: OrganizationIntegrationType.Datadog,
|
||||
configuration: JSON.stringify({ url, apiKey, service: serviceType }),
|
||||
} as OrganizationIntegrationResponse;
|
||||
|
||||
const configResponse = {
|
||||
id: configId,
|
||||
template: JSON.stringify({ service: serviceType }),
|
||||
} as OrganizationIntegrationConfigurationResponse;
|
||||
|
||||
mockIntegrationApiService.createOrganizationIntegration.mockResolvedValue(integrationResponse);
|
||||
mockIntegrationConfigurationApiService.createOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
|
||||
await service.saveDatadog(organizationId, serviceType, url, apiKey);
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations.length).toBe(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
expect(integrations[0].serviceType).toBe(serviceType);
|
||||
});
|
||||
|
||||
it("should throw error on organization ID mismatch in saveDatadog", async () => {
|
||||
service.setOrganizationIntegrations("other-org" as OrganizationId);
|
||||
await expect(service.saveDatadog(organizationId, serviceType, url, apiKey)).rejects.toThrow(
|
||||
Error("Organization ID mismatch"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should update an existing Datadog integration", async () => {
|
||||
service.setOrganizationIntegrations(organizationId);
|
||||
|
||||
const integrationResponse = {
|
||||
id: integrationId,
|
||||
type: OrganizationIntegrationType.Datadog,
|
||||
configuration: JSON.stringify({ url, apiKey, service: serviceType }),
|
||||
} as OrganizationIntegrationResponse;
|
||||
|
||||
const configResponse = {
|
||||
id: configId,
|
||||
template: JSON.stringify({ service: serviceType }),
|
||||
} as OrganizationIntegrationConfigurationResponse;
|
||||
|
||||
mockIntegrationApiService.updateOrganizationIntegration.mockResolvedValue(integrationResponse);
|
||||
mockIntegrationConfigurationApiService.updateOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
|
||||
await service.updateDatadog(organizationId, integrationId, configId, serviceType, url, apiKey);
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations.length).toBe(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
});
|
||||
|
||||
it("should throw error on organization ID mismatch in updateDatadog", async () => {
|
||||
service.setOrganizationIntegrations("other-org" as OrganizationId);
|
||||
await expect(
|
||||
service.updateDatadog(organizationId, integrationId, configId, serviceType, url, apiKey),
|
||||
).rejects.toThrow(Error("Organization ID mismatch"));
|
||||
});
|
||||
|
||||
it("should get integration by id", async () => {
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
serviceType,
|
||||
{} as DatadogConfiguration,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
const integration = await service.getIntegrationById(integrationId);
|
||||
expect(integration).not.toBeNull();
|
||||
expect(integration!.id).toBe(integrationId);
|
||||
});
|
||||
|
||||
it("should get integration by service type", async () => {
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
serviceType,
|
||||
{} as DatadogConfiguration,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
const integration = await service.getIntegrationByServiceType(serviceType);
|
||||
expect(integration).not.toBeNull();
|
||||
expect(integration!.serviceType).toBe(serviceType);
|
||||
});
|
||||
|
||||
it("should get integration configurations", async () => {
|
||||
const config = new OrganizationIntegrationConfiguration(
|
||||
configId,
|
||||
integrationId,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
{} as DatadogTemplate,
|
||||
);
|
||||
|
||||
service["_integrations$"].next([
|
||||
new OrganizationIntegration(
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
serviceType,
|
||||
{} as DatadogConfiguration,
|
||||
[config],
|
||||
),
|
||||
]);
|
||||
const configs = await service.getIntegrationConfigurations(integrationId);
|
||||
expect(configs).not.toBeNull();
|
||||
expect(configs![0].id).toBe(configId);
|
||||
});
|
||||
|
||||
it("convertToJson should parse valid JSON", () => {
|
||||
const obj = service.convertToJson<{ a: number }>('{"a":1}');
|
||||
expect(obj).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it("convertToJson should return null for invalid JSON", () => {
|
||||
const obj = service.convertToJson<{ a: number }>("invalid");
|
||||
expect(obj).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,350 +0,0 @@
|
||||
import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DatadogConfiguration } from "../models/configuration/datadog-configuration";
|
||||
import { DatadogTemplate } from "../models/integration-configuration-config/configuration-template/datadog-template";
|
||||
import { OrganizationIntegration } from "../models/organization-integration";
|
||||
import { OrganizationIntegrationConfiguration } from "../models/organization-integration-configuration";
|
||||
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
|
||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
|
||||
import { OrganizationIntegrationResponse } from "../models/organization-integration-response";
|
||||
import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "../models/organization-integration-type";
|
||||
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
|
||||
|
||||
export type DatadogModificationFailureReason = {
|
||||
mustBeOwner: boolean;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export class DatadogOrganizationIntegrationService {
|
||||
private organizationId$ = new BehaviorSubject<OrganizationId | null>(null);
|
||||
private _integrations$ = new BehaviorSubject<OrganizationIntegration[]>([]);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
integrations$ = this._integrations$.asObservable();
|
||||
|
||||
private fetch$ = this.organizationId$
|
||||
.pipe(
|
||||
switchMap(async (orgId) => {
|
||||
if (orgId) {
|
||||
const data$ = await this.setIntegrations(orgId);
|
||||
return await firstValueFrom(data$);
|
||||
} else {
|
||||
return this._integrations$.getValue();
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe({
|
||||
next: (integrations) => {
|
||||
this._integrations$.next(integrations);
|
||||
},
|
||||
});
|
||||
|
||||
constructor(
|
||||
private integrationApiService: OrganizationIntegrationApiService,
|
||||
private integrationConfigurationApiService: OrganizationIntegrationConfigurationApiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets the organization Id and will trigger the retrieval of the
|
||||
* integrations for a given org.
|
||||
* @param orgId
|
||||
*/
|
||||
setOrganizationIntegrations(orgId: OrganizationId) {
|
||||
this.organizationId$.next(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new organization integration and updates the integrations$ observable
|
||||
* @param organizationId id of the organization
|
||||
* @param service service type of the integration
|
||||
* @param url url of the service
|
||||
* @param apiKey api token
|
||||
*/
|
||||
async saveDatadog(
|
||||
organizationId: OrganizationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
apiKey: string,
|
||||
): Promise<DatadogModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
const datadogConfig = new DatadogConfiguration(url, apiKey, service);
|
||||
const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration(
|
||||
organizationId,
|
||||
new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
datadogConfig.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
const newTemplate = new DatadogTemplate(service);
|
||||
const newIntegrationConfigResponse =
|
||||
await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
newIntegrationResponse.id,
|
||||
new OrganizationIntegrationConfigurationRequest(null, null, null, newTemplate.toString()),
|
||||
);
|
||||
|
||||
const newIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
newIntegrationResponse,
|
||||
newIntegrationConfigResponse,
|
||||
);
|
||||
if (newIntegration !== null) {
|
||||
this._integrations$.next([...this._integrations$.getValue(), newIntegration]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing organization integration and updates the integrations$ observable
|
||||
* @param organizationId id of the organization
|
||||
* @param OrganizationIntegrationId id of the organization integration
|
||||
* @param OrganizationIntegrationConfigurationId id of the organization integration configuration
|
||||
* @param service service type of the integration
|
||||
* @param url url of the service
|
||||
* @param apiKey api token
|
||||
*/
|
||||
async updateDatadog(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
apiKey: string,
|
||||
): Promise<DatadogModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
const datadogConfig = new DatadogConfiguration(url, apiKey, service);
|
||||
const updatedIntegrationResponse =
|
||||
await this.integrationApiService.updateOrganizationIntegration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Datadog,
|
||||
datadogConfig.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
const updatedTemplate = new DatadogTemplate(service);
|
||||
const updatedIntegrationConfigResponse =
|
||||
await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
new OrganizationIntegrationConfigurationRequest(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
updatedTemplate.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
const updatedIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
updatedIntegrationResponse,
|
||||
updatedIntegrationConfigResponse,
|
||||
);
|
||||
|
||||
if (updatedIntegration !== null) {
|
||||
this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDatadog(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
): Promise<DatadogModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
// delete the configuration first due to foreign key constraint
|
||||
await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
);
|
||||
|
||||
// delete the integration
|
||||
await this.integrationApiService.deleteOrganizationIntegration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
);
|
||||
|
||||
// update the local observable
|
||||
const updatedIntegrations = this._integrations$
|
||||
.getValue()
|
||||
.filter((i) => i.id !== OrganizationIntegrationId);
|
||||
this._integrations$.next(updatedIntegrations);
|
||||
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for an OrganizationIntegrationId
|
||||
* @param integrationId id of the integration
|
||||
* @returns OrganizationIntegration or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationById(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
): Promise<OrganizationIntegration | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => integrations.find((i) => i.id === integrationId) || null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for a service type
|
||||
* @param serviceType type of the service
|
||||
* @returns OrganizationIntegration or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationByServiceType(
|
||||
serviceType: OrganizationIntegrationServiceType,
|
||||
): Promise<OrganizationIntegration | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => integrations.find((i) => i.serviceType === serviceType) || null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegrationConfigurations for an integration ID
|
||||
* @param integrationId id of the integration
|
||||
* @returns OrganizationIntegration array or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationConfigurations(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
): Promise<OrganizationIntegrationConfiguration[] | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => {
|
||||
const integration = integrations.find((i) => i.id === integrationId);
|
||||
return integration ? integration.integrationConfiguration : null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Move to data models to be more explicit for future services
|
||||
private mapResponsesToOrganizationIntegration(
|
||||
integrationResponse: OrganizationIntegrationResponse,
|
||||
configurationResponse: OrganizationIntegrationConfigurationResponse,
|
||||
): OrganizationIntegration | null {
|
||||
const datadogConfig = this.convertToJson<DatadogConfiguration>(
|
||||
integrationResponse.configuration,
|
||||
);
|
||||
const template = this.convertToJson<DatadogTemplate>(configurationResponse.template);
|
||||
|
||||
if (!datadogConfig || !template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const integrationConfig = new OrganizationIntegrationConfiguration(
|
||||
configurationResponse.id,
|
||||
integrationResponse.id,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
template,
|
||||
);
|
||||
|
||||
return new OrganizationIntegration(
|
||||
integrationResponse.id,
|
||||
integrationResponse.type,
|
||||
datadogConfig.service,
|
||||
datadogConfig,
|
||||
[integrationConfig],
|
||||
);
|
||||
}
|
||||
|
||||
// Could possibly be moved to a base service. All services would then assume that the
|
||||
// integration configuration would always be an array and this datadog specific service
|
||||
// would just assume a single entry.
|
||||
private setIntegrations(orgId: OrganizationId) {
|
||||
const results$ = zip(this.integrationApiService.getOrganizationIntegrations(orgId)).pipe(
|
||||
switchMap(([responses]) => {
|
||||
const integrations: OrganizationIntegration[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
responses.forEach((integration) => {
|
||||
if (integration.type === OrganizationIntegrationType.Datadog) {
|
||||
const promise = this.integrationConfigurationApiService
|
||||
.getOrganizationIntegrationConfigurations(orgId, integration.id)
|
||||
.then((response) => {
|
||||
// datadog events will only have one OrganizationIntegrationConfiguration
|
||||
const config = response[0];
|
||||
|
||||
const orgIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
integration,
|
||||
config,
|
||||
);
|
||||
|
||||
if (orgIntegration !== null) {
|
||||
integrations.push(orgIntegration);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises).then(() => {
|
||||
return integrations;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return results$;
|
||||
}
|
||||
|
||||
// TODO: Move to base service when necessary
|
||||
convertToJson<T>(jsonString?: string): T | null {
|
||||
try {
|
||||
return JSON.parse(jsonString || "") as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,353 +0,0 @@
|
||||
import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { 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 type HecModificationFailureReason = {
|
||||
mustBeOwner: boolean;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
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 [] as OrganizationIntegration[];
|
||||
}
|
||||
}),
|
||||
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) {
|
||||
if (orgId == this.organizationId$.getValue()) {
|
||||
return;
|
||||
}
|
||||
this._integrations$.next([]);
|
||||
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,
|
||||
): Promise<HecModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
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]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing organization integration and updates the integrations$ observable
|
||||
* @param organizationId id of the organization
|
||||
* @param OrganizationIntegrationId id of the organization integration
|
||||
* @param OrganizationIntegrationConfigurationId id of the organization integration configuration
|
||||
* @param service service type of the integration
|
||||
* @param url url of the service
|
||||
* @param bearerToken api token
|
||||
* @param index index in service
|
||||
*/
|
||||
async updateHec(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
service: OrganizationIntegrationServiceType,
|
||||
url: string,
|
||||
bearerToken: string,
|
||||
index: string,
|
||||
): Promise<HecModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
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) {
|
||||
const unchangedIntegrations = this._integrations$
|
||||
.getValue()
|
||||
.filter((i) => i.id !== OrganizationIntegrationId);
|
||||
this._integrations$.next([...unchangedIntegrations, updatedIntegration]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteHec(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
): Promise<HecModificationFailureReason> {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
// delete the configuration first due to foreign key constraint
|
||||
await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
);
|
||||
|
||||
// delete the integration
|
||||
await this.integrationApiService.deleteOrganizationIntegration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
);
|
||||
|
||||
// update the local observable
|
||||
const updatedIntegrations = this._integrations$
|
||||
.getValue()
|
||||
.filter((i) => i.id !== OrganizationIntegrationId);
|
||||
this._integrations$.next(updatedIntegrations);
|
||||
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for an OrganizationIntegrationId
|
||||
* @param integrationId id of the integration
|
||||
* @returns OrganizationIntegration or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationById(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
): Promise<OrganizationIntegration | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => integrations.find((i) => i.id === integrationId) || null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for a service type
|
||||
* @param serviceType type of the service
|
||||
* @returns OrganizationIntegration or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationByServiceType(
|
||||
serviceType: OrganizationIntegrationServiceType,
|
||||
): Promise<OrganizationIntegration | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => integrations.find((i) => i.serviceType === serviceType) || null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegrationConfigurations for an integration ID
|
||||
* @param integrationId id of the integration
|
||||
* @returns OrganizationIntegration array or null
|
||||
*/
|
||||
// TODO: Move to base class when another service integration type is implemented
|
||||
async getIntegrationConfigurations(
|
||||
integrationId: OrganizationIntegrationId,
|
||||
): Promise<OrganizationIntegrationConfiguration[] | null> {
|
||||
return await firstValueFrom(
|
||||
this.integrations$.pipe(
|
||||
map((integrations) => {
|
||||
const integration = integrations.find((i) => i.id === integrationId);
|
||||
return integration ? integration.integrationConfiguration : null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Move to data models to be more explicit for future services
|
||||
private mapResponsesToOrganizationIntegration(
|
||||
integrationResponse: OrganizationIntegrationResponse,
|
||||
configurationResponse: OrganizationIntegrationConfigurationResponse,
|
||||
): OrganizationIntegration | null {
|
||||
const 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) => {
|
||||
if (integration.type === OrganizationIntegrationType.Hec) {
|
||||
const promise = this.integrationConfigurationApiService
|
||||
.getOrganizationIntegrationConfigurations(orgId, integration.id)
|
||||
.then((response) => {
|
||||
// Hec events will only have one OrganizationIntegrationConfiguration
|
||||
const config = response[0];
|
||||
|
||||
const orgIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
integration,
|
||||
config,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
|
||||
import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationServiceName } from "../models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "../models/organization-integration-type";
|
||||
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
@@ -56,7 +56,7 @@ describe("OrganizationIntegrationApiService", () => {
|
||||
it("should call apiService.send with correct parameters for createOrganizationIntegration", async () => {
|
||||
const request = new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Hec,
|
||||
`{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceType.CrowdStrike}' }`,
|
||||
`{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceName.CrowdStrike}' }`,
|
||||
);
|
||||
const orgId = "org1" as OrganizationId;
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("OrganizationIntegrationApiService", () => {
|
||||
it("should call apiService.send with the correct parameters for updateOrganizationIntegration", async () => {
|
||||
const request = new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Hec,
|
||||
`{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceType.CrowdStrike}' }`,
|
||||
`{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceName.CrowdStrike}' }`,
|
||||
);
|
||||
const orgId = "org1" as OrganizationId;
|
||||
const integrationId = "integration1" as OrganizationIntegrationId;
|
||||
|
||||
@@ -0,0 +1,633 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { OrgIntegrationBuilder } from "../models/integration-builder";
|
||||
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 { OrganizationIntegrationServiceName } 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";
|
||||
import { OrganizationIntegrationService } from "./organization-integration-service";
|
||||
|
||||
describe("OrganizationIntegrationService", () => {
|
||||
let service: OrganizationIntegrationService;
|
||||
let integrationApiService: MockProxy<OrganizationIntegrationApiService>;
|
||||
let integrationConfigurationApiService: MockProxy<OrganizationIntegrationConfigurationApiService>;
|
||||
|
||||
const orgId = "org-123" as OrganizationId;
|
||||
const integrationId = "integration-456" as OrganizationIntegrationId;
|
||||
const configurationId = "config-789" as OrganizationIntegrationConfigurationId;
|
||||
|
||||
const mockIntegrationResponse = new OrganizationIntegrationResponse({
|
||||
Id: integrationId,
|
||||
Type: OrganizationIntegrationType.Hec,
|
||||
Configuration: JSON.stringify({
|
||||
uri: "https://test.splunk.com",
|
||||
token: "test-token",
|
||||
service: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
}),
|
||||
});
|
||||
|
||||
const mockConfigurationResponse = new OrganizationIntegrationConfigurationResponse({
|
||||
Id: configurationId,
|
||||
Template: JSON.stringify({
|
||||
index: "main",
|
||||
service: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
integrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
integrationConfigurationApiService = mock<OrganizationIntegrationConfigurationApiService>();
|
||||
|
||||
service = new OrganizationIntegrationService(
|
||||
integrationApiService,
|
||||
integrationConfigurationApiService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should be created", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with empty integrations", async () => {
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setOrganizationId", () => {
|
||||
it("should fetch and set integrations for the organization", async () => {
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([mockIntegrationResponse]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReturnValue(
|
||||
Promise.resolve([mockConfigurationResponse]),
|
||||
);
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
|
||||
// Wait for the observable to emit
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
expect(integrations[0].type).toBe(OrganizationIntegrationType.Hec);
|
||||
expect(integrationApiService.getOrganizationIntegrations).toHaveBeenCalledWith(orgId);
|
||||
expect(
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations,
|
||||
).toHaveBeenCalledWith(orgId, integrationId);
|
||||
});
|
||||
|
||||
it("should skip fetching if organization ID is the same", async () => {
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(Promise.resolve([]));
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
integrationApiService.getOrganizationIntegrations.mockClear();
|
||||
|
||||
// Call again with the same org ID
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(integrationApiService.getOrganizationIntegrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should clear existing integrations when switching organizations", async () => {
|
||||
const orgId2 = "org-456" as OrganizationId;
|
||||
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([mockIntegrationResponse]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReturnValue(
|
||||
Promise.resolve([mockConfigurationResponse]),
|
||||
);
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
let integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(1);
|
||||
|
||||
// Switch to different org
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(Promise.resolve([]));
|
||||
service.setOrganizationId(orgId2).subscribe();
|
||||
|
||||
// Should immediately clear
|
||||
integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toEqual([]);
|
||||
});
|
||||
|
||||
it("should unsubscribe from previous fetch when setting new organization", async () => {
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(Promise.resolve([]));
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const orgId2 = "org-456" as OrganizationId;
|
||||
service.setOrganizationId(orgId2).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Should call the API for both organizations (no errors about duplicate subscriptions)
|
||||
// The exact call count may vary based on observable behavior
|
||||
expect(integrationApiService.getOrganizationIntegrations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle multiple integrations", async () => {
|
||||
const integration2Response = new OrganizationIntegrationResponse({
|
||||
Id: "integration-2" as OrganizationIntegrationId,
|
||||
Type: OrganizationIntegrationType.Datadog,
|
||||
Configuration: JSON.stringify({
|
||||
uri: "https://datadog.com",
|
||||
apiKey: "test-api-key",
|
||||
service: OrganizationIntegrationServiceName.Datadog,
|
||||
}),
|
||||
});
|
||||
|
||||
const configuration2Response = new OrganizationIntegrationConfigurationResponse({
|
||||
Id: "config-2" as OrganizationIntegrationConfigurationId,
|
||||
Template: JSON.stringify({
|
||||
service: OrganizationIntegrationServiceName.Datadog,
|
||||
}),
|
||||
});
|
||||
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([mockIntegrationResponse, integration2Response]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations
|
||||
.mockReturnValueOnce(Promise.resolve([mockConfigurationResponse]))
|
||||
.mockReturnValueOnce(Promise.resolve([configuration2Response]));
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
"https://test.splunk.com",
|
||||
"test-token",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"main",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
// Set the organization first
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(Promise.resolve([]));
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
});
|
||||
|
||||
it("should save a new integration successfully", async () => {
|
||||
integrationApiService.createOrganizationIntegration.mockResolvedValue(
|
||||
mockIntegrationResponse,
|
||||
);
|
||||
integrationConfigurationApiService.createOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
mockConfigurationResponse,
|
||||
);
|
||||
|
||||
const result = await service.save(orgId, OrganizationIntegrationType.Hec, config, template);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: false, success: true });
|
||||
expect(integrationApiService.createOrganizationIntegration).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
expect.any(OrganizationIntegrationRequest),
|
||||
);
|
||||
expect(
|
||||
integrationConfigurationApiService.createOrganizationIntegrationConfiguration,
|
||||
).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
integrationId,
|
||||
expect.any(OrganizationIntegrationConfigurationRequest),
|
||||
);
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
});
|
||||
|
||||
it("should throw error when organization ID mismatch", async () => {
|
||||
const differentOrgId = "different-org" as OrganizationId;
|
||||
|
||||
await expect(
|
||||
service.save(differentOrgId, OrganizationIntegrationType.Hec, config, template),
|
||||
).rejects.toThrow("Organization ID mismatch");
|
||||
});
|
||||
|
||||
it("should return mustBeOwner true when API returns 404", async () => {
|
||||
const error = new ErrorResponse({}, 404);
|
||||
integrationApiService.createOrganizationIntegration.mockRejectedValue(error);
|
||||
|
||||
const result = await service.save(orgId, OrganizationIntegrationType.Hec, config, template);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: true, success: false });
|
||||
});
|
||||
|
||||
it("should rethrow non-404 errors", async () => {
|
||||
const error = new Error("Server error");
|
||||
integrationApiService.createOrganizationIntegration.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
service.save(orgId, OrganizationIntegrationType.Hec, config, template),
|
||||
).rejects.toThrow("Server error");
|
||||
});
|
||||
|
||||
it("should handle configuration creation failure with 404", async () => {
|
||||
const error = new ErrorResponse({}, 404);
|
||||
integrationApiService.createOrganizationIntegration.mockResolvedValue(
|
||||
mockIntegrationResponse,
|
||||
);
|
||||
integrationConfigurationApiService.createOrganizationIntegrationConfiguration.mockRejectedValue(
|
||||
error,
|
||||
);
|
||||
|
||||
const result = await service.save(orgId, OrganizationIntegrationType.Hec, config, template);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: true, success: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
"https://updated.splunk.com",
|
||||
"updated-token",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"updated-index",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
// Set the organization and add an existing integration
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([mockIntegrationResponse]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReturnValue(
|
||||
Promise.resolve([mockConfigurationResponse]),
|
||||
);
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
});
|
||||
|
||||
it("should update an integration successfully", async () => {
|
||||
const updatedIntegrationResponse = new OrganizationIntegrationResponse({
|
||||
Id: integrationId,
|
||||
Type: OrganizationIntegrationType.Hec,
|
||||
Configuration: JSON.stringify({
|
||||
uri: "https://updated.splunk.com",
|
||||
token: "updated-token",
|
||||
service: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
}),
|
||||
});
|
||||
|
||||
const updatedConfigurationResponse = new OrganizationIntegrationConfigurationResponse({
|
||||
Id: configurationId,
|
||||
Template: JSON.stringify({
|
||||
index: "updated-index",
|
||||
service: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
}),
|
||||
});
|
||||
|
||||
integrationApiService.updateOrganizationIntegration.mockResolvedValue(
|
||||
updatedIntegrationResponse,
|
||||
);
|
||||
integrationConfigurationApiService.updateOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
updatedConfigurationResponse,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const result = await service.update(
|
||||
orgId,
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
configurationId,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: false, success: true });
|
||||
expect(integrationApiService.updateOrganizationIntegration).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
integrationId,
|
||||
expect.any(OrganizationIntegrationRequest),
|
||||
);
|
||||
expect(
|
||||
integrationConfigurationApiService.updateOrganizationIntegrationConfiguration,
|
||||
).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
integrationId,
|
||||
configurationId,
|
||||
expect.any(OrganizationIntegrationConfigurationRequest),
|
||||
);
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
});
|
||||
|
||||
it("should throw error when organization ID mismatch", async () => {
|
||||
const differentOrgId = "different-org" as OrganizationId;
|
||||
|
||||
await expect(
|
||||
service.update(
|
||||
differentOrgId,
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
configurationId,
|
||||
config,
|
||||
template,
|
||||
),
|
||||
).rejects.toThrow("Organization ID mismatch");
|
||||
});
|
||||
|
||||
it("should return mustBeOwner true when API returns 404", async () => {
|
||||
const error = new ErrorResponse({}, 404);
|
||||
integrationApiService.updateOrganizationIntegration.mockRejectedValue(error);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const result = await service.update(
|
||||
orgId,
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
configurationId,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: true, success: false });
|
||||
});
|
||||
|
||||
it("should rethrow non-404 errors", async () => {
|
||||
const error = new Error("Server error");
|
||||
integrationApiService.updateOrganizationIntegration.mockRejectedValue(error);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
await expect(
|
||||
service.update(
|
||||
orgId,
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
configurationId,
|
||||
config,
|
||||
template,
|
||||
),
|
||||
).rejects.toThrow("Server error");
|
||||
});
|
||||
|
||||
it("should replace old integration with updated one in the list", async () => {
|
||||
// Add multiple integrations first
|
||||
const integration2Response = new OrganizationIntegrationResponse({
|
||||
Id: "integration-2" as OrganizationIntegrationId,
|
||||
Type: OrganizationIntegrationType.Hec,
|
||||
Configuration: mockIntegrationResponse.configuration,
|
||||
});
|
||||
const configuration2Response = new OrganizationIntegrationConfigurationResponse({
|
||||
Id: "config-2" as OrganizationIntegrationConfigurationId,
|
||||
Template: mockConfigurationResponse.template,
|
||||
});
|
||||
|
||||
const orgId2 = "org-456" as OrganizationId;
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([mockIntegrationResponse, integration2Response]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations
|
||||
.mockReturnValue(Promise.resolve([mockConfigurationResponse]))
|
||||
.mockReturnValueOnce(Promise.resolve([mockConfigurationResponse]))
|
||||
.mockReturnValueOnce(Promise.resolve([configuration2Response]));
|
||||
|
||||
service.setOrganizationId(orgId2).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
let integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(2);
|
||||
|
||||
// Now update the first integration
|
||||
integrationApiService.updateOrganizationIntegration.mockResolvedValue(
|
||||
mockIntegrationResponse,
|
||||
);
|
||||
integrationConfigurationApiService.updateOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
mockConfigurationResponse,
|
||||
);
|
||||
|
||||
await service.update(
|
||||
orgId2,
|
||||
integrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
configurationId,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
|
||||
integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(2);
|
||||
expect(integrations.find((i) => i.id === integrationId)).toBeDefined();
|
||||
expect(integrations.find((i) => i.id === "integration-2")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
beforeEach(() => {
|
||||
// Set the organization and add an existing integration
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([mockIntegrationResponse]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReturnValue(
|
||||
Promise.resolve([mockConfigurationResponse]),
|
||||
);
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
});
|
||||
|
||||
it("should delete an integration successfully", async () => {
|
||||
integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
integrationApiService.deleteOrganizationIntegration.mockResolvedValue(undefined);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
let integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(1);
|
||||
|
||||
const result = await service.delete(orgId, integrationId, configurationId);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: false, success: true });
|
||||
expect(
|
||||
integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration,
|
||||
).toHaveBeenCalledWith(orgId, integrationId, configurationId);
|
||||
expect(integrationApiService.deleteOrganizationIntegration).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
integrationId,
|
||||
);
|
||||
|
||||
integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should delete configuration before integration", async () => {
|
||||
const callOrder: string[] = [];
|
||||
|
||||
integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration.mockImplementation(
|
||||
async () => {
|
||||
callOrder.push("configuration");
|
||||
},
|
||||
);
|
||||
integrationApiService.deleteOrganizationIntegration.mockImplementation(async () => {
|
||||
callOrder.push("integration");
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
await service.delete(orgId, integrationId, configurationId);
|
||||
|
||||
expect(callOrder).toEqual(["configuration", "integration"]);
|
||||
});
|
||||
|
||||
it("should throw error when organization ID mismatch", async () => {
|
||||
const differentOrgId = "different-org" as OrganizationId;
|
||||
|
||||
await expect(service.delete(differentOrgId, integrationId, configurationId)).rejects.toThrow(
|
||||
"Organization ID mismatch",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return mustBeOwner true when API returns 404", async () => {
|
||||
const error = new ErrorResponse({}, 404);
|
||||
integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration.mockRejectedValue(
|
||||
error,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const result = await service.delete(orgId, integrationId, configurationId);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: true, success: false });
|
||||
});
|
||||
|
||||
it("should rethrow non-404 errors", async () => {
|
||||
const error = new Error("Server error");
|
||||
integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration.mockRejectedValue(
|
||||
error,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
await expect(service.delete(orgId, integrationId, configurationId)).rejects.toThrow(
|
||||
"Server error",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle 404 error when deleting integration", async () => {
|
||||
const error = new ErrorResponse({}, 404);
|
||||
integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
integrationApiService.deleteOrganizationIntegration.mockRejectedValue(error);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const result = await service.delete(orgId, integrationId, configurationId);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: true, success: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapResponsesToOrganizationIntegration", () => {
|
||||
it("should return null if configuration cannot be built", () => {
|
||||
const invalidIntegrationResponse = new OrganizationIntegrationResponse({
|
||||
Id: integrationId,
|
||||
Type: 999 as OrganizationIntegrationType, // Invalid type
|
||||
Configuration: "invalid-json",
|
||||
});
|
||||
|
||||
// The buildConfiguration method throws for unsupported types
|
||||
// In production, this error is caught in the setIntegrations pipeline
|
||||
expect(() =>
|
||||
service["mapResponsesToOrganizationIntegration"](
|
||||
invalidIntegrationResponse,
|
||||
mockConfigurationResponse,
|
||||
),
|
||||
).toThrow("Unsupported integration type: 999");
|
||||
});
|
||||
|
||||
it("should handle template with invalid data", () => {
|
||||
const invalidConfigurationResponse = new OrganizationIntegrationConfigurationResponse({
|
||||
Id: configurationId,
|
||||
Template: "{}", // Empty template, will have undefined values but won't return null
|
||||
});
|
||||
|
||||
const result = service["mapResponsesToOrganizationIntegration"](
|
||||
mockIntegrationResponse,
|
||||
invalidConfigurationResponse,
|
||||
);
|
||||
|
||||
// The result won't be null, but will have a template with undefined/default values
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.integrationConfiguration[0].template).toBeDefined();
|
||||
});
|
||||
|
||||
it("should successfully map valid responses to OrganizationIntegration", () => {
|
||||
const result = service["mapResponsesToOrganizationIntegration"](
|
||||
mockIntegrationResponse,
|
||||
mockConfigurationResponse,
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.id).toBe(integrationId);
|
||||
expect(result?.type).toBe(OrganizationIntegrationType.Hec);
|
||||
expect(result?.integrationConfiguration).toHaveLength(1);
|
||||
expect(result?.integrationConfiguration[0].id).toBe(configurationId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty integration list from API", async () => {
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(Promise.resolve([]));
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle errors when fetching integrations", async () => {
|
||||
const validIntegration = mockIntegrationResponse;
|
||||
|
||||
integrationApiService.getOrganizationIntegrations.mockReturnValue(
|
||||
Promise.resolve([validIntegration]),
|
||||
);
|
||||
integrationConfigurationApiService.getOrganizationIntegrationConfigurations.mockReturnValue(
|
||||
Promise.resolve([mockConfigurationResponse]),
|
||||
);
|
||||
|
||||
service.setOrganizationId(orgId).subscribe();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const integrations = await firstValueFrom(service.integrations$);
|
||||
expect(integrations).toHaveLength(1);
|
||||
expect(integrations[0].id).toBe(integrationId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
import { BehaviorSubject, map, Observable, of, switchMap, tap, zip } from "rxjs";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import {
|
||||
OrganizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
OrgIntegrationBuilder,
|
||||
OrgIntegrationConfiguration,
|
||||
OrgIntegrationTemplate,
|
||||
} from "../models/integration-builder";
|
||||
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 { OrganizationIntegrationType } from "../models/organization-integration-type";
|
||||
|
||||
import { OrganizationIntegrationApiService } from "./organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service";
|
||||
/**
|
||||
* Common result type for integration modification operations (save, update, delete).
|
||||
* was the server side failure due to insufficient permissions (must be owner)?
|
||||
*/
|
||||
export type IntegrationModificationResult = {
|
||||
mustBeOwner: boolean;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides common functionality for managing integrations with different external services.
|
||||
*/
|
||||
export class OrganizationIntegrationService {
|
||||
private organizationId$ = new BehaviorSubject<OrganizationId | null>(null);
|
||||
private _integrations$ = new BehaviorSubject<OrganizationIntegration[]>([]);
|
||||
|
||||
integrations$: Observable<OrganizationIntegration[]> = this._integrations$.asObservable();
|
||||
|
||||
constructor(
|
||||
protected integrationApiService: OrganizationIntegrationApiService,
|
||||
protected integrationConfigurationApiService: OrganizationIntegrationConfigurationApiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets the organization Id and triggers the retrieval of integrations for the given organization.
|
||||
* The integrations will be available via the integrations$ observable.
|
||||
* If the organization ID is the same as the current one, no action is taken.
|
||||
* Use this method to kick off loading integrations for a specific organization.
|
||||
* Use integrations$ to subscribe to the loaded integrations.
|
||||
*
|
||||
* @param orgId - The organization ID to set
|
||||
* @returns Observable<void> that completes when the operation is done. Subscribe to trigger the load.
|
||||
*/
|
||||
setOrganizationId(orgId: OrganizationId): Observable<void> {
|
||||
if (orgId === this.organizationId$.getValue()) {
|
||||
return of(void 0);
|
||||
}
|
||||
this._integrations$.next([]);
|
||||
this.organizationId$.next(orgId);
|
||||
|
||||
// subscribe to load and set integrations
|
||||
// use integrations$ to get the loaded integrations
|
||||
return this.setIntegrations(orgId).pipe(
|
||||
tap((integrations) => {
|
||||
this._integrations$.next(integrations);
|
||||
}),
|
||||
map((): void => void 0),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new organization integration and updates the integrations$ observable.
|
||||
*
|
||||
* @param organizationId - ID of the organization
|
||||
* @param integrationType - Type of the organization integration
|
||||
* @param config - The configuration object for this integration
|
||||
* @param template - The template object for this integration
|
||||
* @returns Promise with the result indicating success or failure reason
|
||||
*/
|
||||
async save(
|
||||
organizationId: OrganizationId,
|
||||
integrationType: OrganizationIntegrationType,
|
||||
config: OrgIntegrationConfiguration,
|
||||
template: OrgIntegrationTemplate,
|
||||
): Promise<IntegrationModificationResult> {
|
||||
if (organizationId !== this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
const configString = config.toString();
|
||||
const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration(
|
||||
organizationId,
|
||||
new OrganizationIntegrationRequest(integrationType, configString),
|
||||
);
|
||||
|
||||
const templateString = template.toString();
|
||||
const newIntegrationConfigResponse =
|
||||
await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
newIntegrationResponse.id,
|
||||
new OrganizationIntegrationConfigurationRequest(null, null, null, templateString),
|
||||
);
|
||||
|
||||
const newIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
newIntegrationResponse,
|
||||
newIntegrationConfigResponse,
|
||||
);
|
||||
if (newIntegration !== null) {
|
||||
this._integrations$.next([...this._integrations$.getValue(), newIntegration]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing organization integration and updates the integrations$ observable.
|
||||
*
|
||||
* @param organizationId - ID of the organization
|
||||
* @param integrationId - ID of the organization integration
|
||||
* @param integrationType - Type of the organization integration
|
||||
* @param configurationId - ID of the organization integration configuration
|
||||
* @param config - The updated configuration object
|
||||
* @param template - The updated template object
|
||||
* @returns Promise with the result indicating success or failure reason
|
||||
*/
|
||||
async update(
|
||||
organizationId: OrganizationId,
|
||||
integrationId: OrganizationIntegrationId,
|
||||
integrationType: OrganizationIntegrationType,
|
||||
configurationId: OrganizationIntegrationConfigurationId,
|
||||
config: OrgIntegrationConfiguration,
|
||||
template: OrgIntegrationTemplate,
|
||||
): Promise<IntegrationModificationResult> {
|
||||
if (organizationId !== this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
const configString = config.toString();
|
||||
const updatedIntegrationResponse =
|
||||
await this.integrationApiService.updateOrganizationIntegration(
|
||||
organizationId,
|
||||
integrationId,
|
||||
new OrganizationIntegrationRequest(integrationType, configString),
|
||||
);
|
||||
|
||||
const templateString = template.toString();
|
||||
const updatedIntegrationConfigResponse =
|
||||
await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
integrationId,
|
||||
configurationId,
|
||||
new OrganizationIntegrationConfigurationRequest(null, null, null, templateString),
|
||||
);
|
||||
|
||||
const updatedIntegration = this.mapResponsesToOrganizationIntegration(
|
||||
updatedIntegrationResponse,
|
||||
updatedIntegrationConfigResponse,
|
||||
);
|
||||
|
||||
if (updatedIntegration !== null) {
|
||||
const integrations = this._integrations$.getValue();
|
||||
const index = integrations.findIndex((i) => i.id === integrationId);
|
||||
if (index !== -1) {
|
||||
integrations[index] = updatedIntegration;
|
||||
} else {
|
||||
integrations.push(updatedIntegration);
|
||||
}
|
||||
this._integrations$.next([...integrations]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an organization integration and updates the integrations$ observable.
|
||||
*
|
||||
* @param organizationId - ID of the organization
|
||||
* @param integrationId - ID of the organization integration
|
||||
* @param configurationId - ID of the organization integration configuration
|
||||
* @returns Promise with the result indicating success or failure reason
|
||||
*/
|
||||
async delete(
|
||||
organizationId: OrganizationId,
|
||||
integrationId: OrganizationIntegrationId,
|
||||
configurationId: OrganizationIntegrationConfigurationId,
|
||||
): Promise<IntegrationModificationResult> {
|
||||
if (organizationId !== this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
|
||||
try {
|
||||
// delete the configuration first due to foreign key constraint
|
||||
await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
integrationId,
|
||||
configurationId,
|
||||
);
|
||||
|
||||
// delete the integration
|
||||
await this.integrationApiService.deleteOrganizationIntegration(organizationId, integrationId);
|
||||
|
||||
// update the local observable
|
||||
const updatedIntegrations = this._integrations$
|
||||
.getValue()
|
||||
.filter((i) => i.id !== integrationId);
|
||||
this._integrations$.next(updatedIntegrations);
|
||||
|
||||
return { mustBeOwner: false, success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps API responses to an OrganizationIntegration domain model.
|
||||
*
|
||||
* @param integrationResponse - The integration response from the API
|
||||
* @param configurationResponse - The configuration response from the API
|
||||
* @returns OrganizationIntegration or null if mapping fails
|
||||
*/
|
||||
private mapResponsesToOrganizationIntegration(
|
||||
integrationResponse: OrganizationIntegrationResponse,
|
||||
configurationResponse: OrganizationIntegrationConfigurationResponse,
|
||||
): OrganizationIntegration | null {
|
||||
const integrationType = integrationResponse.type;
|
||||
const config = OrgIntegrationBuilder.buildConfiguration(
|
||||
integrationType,
|
||||
integrationResponse.configuration,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildTemplate(
|
||||
integrationType,
|
||||
configurationResponse.template ?? "{}",
|
||||
);
|
||||
|
||||
if (!config || !template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const integrationConfig = new OrganizationIntegrationConfiguration(
|
||||
configurationResponse.id,
|
||||
integrationResponse.id,
|
||||
null,
|
||||
"",
|
||||
template,
|
||||
);
|
||||
|
||||
return new OrganizationIntegration(
|
||||
integrationResponse.id,
|
||||
integrationResponse.type,
|
||||
config.service,
|
||||
config,
|
||||
[integrationConfig],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches integrations for the given organization from the API.
|
||||
*
|
||||
* @param orgId - Organization ID to fetch integrations for
|
||||
* @returns Observable of OrganizationIntegration array
|
||||
*/
|
||||
private setIntegrations(orgId: OrganizationId): Observable<OrganizationIntegration[]> {
|
||||
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) => {
|
||||
// Integration 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$;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
@@ -41,9 +42,10 @@ describe("Provider Permissions Guard", () => {
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
id: mockUserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
route = mock<ActivatedRouteSnapshot>({
|
||||
|
||||
@@ -143,16 +143,14 @@ export class AllApplicationsComponent implements OnInit {
|
||||
|
||||
onCheckboxChange = (applicationName: string, event: Event) => {
|
||||
const isChecked = (event.target as HTMLInputElement).checked;
|
||||
if (isChecked) {
|
||||
this.selectedUrls.update((selectedUrls) => {
|
||||
selectedUrls.add(applicationName);
|
||||
return selectedUrls;
|
||||
});
|
||||
} else {
|
||||
this.selectedUrls.update((selectedUrls) => {
|
||||
selectedUrls.delete(applicationName);
|
||||
return selectedUrls;
|
||||
});
|
||||
}
|
||||
this.selectedUrls.update((selectedUrls) => {
|
||||
const nextSelected = new Set(selectedUrls);
|
||||
if (isChecked) {
|
||||
nextSelected.add(applicationName);
|
||||
} else {
|
||||
nextSelected.delete(applicationName);
|
||||
}
|
||||
return nextSelected;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrgIntegrationBuilder } 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";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
@@ -29,8 +30,7 @@ describe("IntegrationCardComponent", () => {
|
||||
let fixture: ComponentFixture<IntegrationCardComponent>;
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const activatedRoute = mock<ActivatedRoute>();
|
||||
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||
const mockDatadogIntegrationService = mock<DatadogOrganizationIntegrationService>();
|
||||
const mockIntegrationService = mock<OrganizationIntegrationService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const toastService = mock<ToastService>();
|
||||
|
||||
@@ -54,8 +54,7 @@ describe("IntegrationCardComponent", () => {
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
|
||||
{ provide: OrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
],
|
||||
@@ -259,7 +258,7 @@ describe("IntegrationCardComponent", () => {
|
||||
configuration: {},
|
||||
integrationConfiguration: [{ id: "config-id" }],
|
||||
},
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
} as any;
|
||||
component.organizationId = "org-id" as any;
|
||||
jest.resetAllMocks();
|
||||
@@ -270,8 +269,8 @@ describe("IntegrationCardComponent", () => {
|
||||
closed: of({ success: false }),
|
||||
});
|
||||
await component.setupConnection();
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.update).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call updateHec if isUpdateAvailable is true", async () => {
|
||||
@@ -284,26 +283,35 @@ describe("IntegrationCardComponent", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
"test-url",
|
||||
"token",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"index",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.updateHec).toHaveBeenCalledWith(
|
||||
expect(mockIntegrationService.update).toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
"integration-id",
|
||||
OrganizationIntegrationType.Hec,
|
||||
"config-id",
|
||||
OrganizationIntegrationServiceType.CrowdStrike,
|
||||
"test-url",
|
||||
"token",
|
||||
"index",
|
||||
config,
|
||||
template,
|
||||
);
|
||||
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call saveHec if isUpdateAvailable is false", async () => {
|
||||
component.integrationSettings = {
|
||||
organizationIntegration: null,
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
} as any;
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
@@ -316,23 +324,32 @@ describe("IntegrationCardComponent", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
"test-url",
|
||||
"token",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"index",
|
||||
OrganizationIntegrationServiceName.CrowdStrike,
|
||||
);
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(false);
|
||||
|
||||
mockIntegrationService.saveHec.mockResolvedValue({ mustBeOwner: false, success: true });
|
||||
mockIntegrationService.save.mockResolvedValue({ mustBeOwner: false, success: true });
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.saveHec).toHaveBeenCalledWith(
|
||||
expect(mockIntegrationService.save).toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
OrganizationIntegrationServiceType.CrowdStrike,
|
||||
"test-url",
|
||||
"token",
|
||||
"index",
|
||||
OrganizationIntegrationType.Hec,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call deleteHec when a delete is requested", async () => {
|
||||
it("should call delete with Hec type when a delete is requested", async () => {
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
@@ -344,22 +361,22 @@ describe("IntegrationCardComponent", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true });
|
||||
mockIntegrationService.delete.mockResolvedValue({ mustBeOwner: false, success: true });
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).toHaveBeenCalledWith(
|
||||
expect(mockIntegrationService.delete).toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
"integration-id",
|
||||
"config-id",
|
||||
);
|
||||
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not call deleteHec if no existing configuration", async () => {
|
||||
it("should not call delete if no existing configuration", async () => {
|
||||
component.integrationSettings = {
|
||||
organizationIntegration: null,
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
} as any;
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
@@ -372,20 +389,16 @@ describe("IntegrationCardComponent", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true });
|
||||
mockIntegrationService.delete.mockResolvedValue({ mustBeOwner: false, success: true });
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).not.toHaveBeenCalledWith(
|
||||
expect(mockIntegrationService.delete).not.toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
"integration-id",
|
||||
"config-id",
|
||||
OrganizationIntegrationServiceType.CrowdStrike,
|
||||
"test-url",
|
||||
"token",
|
||||
"index",
|
||||
);
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
expect(mockIntegrationService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show toast on error while saving", async () => {
|
||||
@@ -399,11 +412,11 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.updateHec.mockRejectedValue(new Error("fail"));
|
||||
mockIntegrationService.update.mockRejectedValue(new Error("fail"));
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.updateHec).toHaveBeenCalled();
|
||||
expect(mockIntegrationService.update).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
@@ -422,11 +435,11 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404));
|
||||
mockIntegrationService.update.mockRejectedValue(new ErrorResponse("Not Found", 404));
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.updateHec).toHaveBeenCalled();
|
||||
expect(mockIntegrationService.update).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
@@ -445,11 +458,10 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404));
|
||||
|
||||
mockIntegrationService.update.mockRejectedValue(new ErrorResponse("Not Found", 404));
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.updateHec).toHaveBeenCalled();
|
||||
expect(mockIntegrationService.update).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
@@ -468,11 +480,11 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.deleteHec.mockRejectedValue(new Error("fail"));
|
||||
mockIntegrationService.delete.mockRejectedValue(new Error("fail"));
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
|
||||
expect(mockIntegrationService.delete).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
@@ -491,11 +503,10 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.deleteHec.mockRejectedValue(new ErrorResponse("Not Found", 404));
|
||||
|
||||
mockIntegrationService.delete.mockRejectedValue(new ErrorResponse("Not Found", 404));
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
|
||||
expect(mockIntegrationService.delete).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
|
||||
@@ -12,10 +12,10 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrgIntegrationBuilder } 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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
@@ -96,8 +96,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
private dialogService: DialogService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||
private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
|
||||
private organizationIntegrationService: OrganizationIntegrationService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
@@ -250,7 +249,18 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
async saveHec(result: HecConnectDialogResult) {
|
||||
let saveResponse = { mustBeOwner: false, success: false };
|
||||
let response = { mustBeOwner: false, success: false };
|
||||
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
result.index,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
|
||||
if (this.isUpdateAvailable) {
|
||||
// retrieve org integration and configuration ids
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
@@ -262,27 +272,25 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
// update existing integration and configuration
|
||||
saveResponse = await this.hecOrganizationIntegrationService.updateHec(
|
||||
response = await this.organizationIntegrationService.update(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
OrganizationIntegrationType.Hec,
|
||||
orgIntegrationConfigurationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
result.index,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
saveResponse = await this.hecOrganizationIntegrationService.saveHec(
|
||||
response = await this.organizationIntegrationService.save(
|
||||
this.organizationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
result.index,
|
||||
OrganizationIntegrationType.Hec,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (saveResponse.mustBeOwner) {
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
@@ -303,7 +311,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
const response = await this.hecOrganizationIntegrationService.deleteHec(
|
||||
const response = await this.organizationIntegrationService.delete(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
@@ -322,6 +330,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -333,23 +348,29 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
// update existing integration and configuration
|
||||
await this.datadogOrganizationIntegrationService.updateDatadog(
|
||||
response = await this.organizationIntegrationService.update(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
orgIntegrationConfigurationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.apiKey,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
await this.datadogOrganizationIntegrationService.saveDatadog(
|
||||
response = await this.organizationIntegrationService.save(
|
||||
this.organizationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.apiKey,
|
||||
OrganizationIntegrationType.Datadog,
|
||||
config,
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.mustBeOwner) {
|
||||
this.showMustBeOwnerToast();
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
@@ -366,7 +387,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
const response = await this.datadogOrganizationIntegrationService.deleteDatadog(
|
||||
const response = await this.organizationIntegrationService.delete(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
|
||||
@@ -6,8 +6,7 @@ import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
@@ -24,8 +23,7 @@ describe("IntegrationGridComponent", () => {
|
||||
let component: IntegrationGridComponent;
|
||||
let fixture: ComponentFixture<IntegrationGridComponent>;
|
||||
const mockActivatedRoute = mock<ActivatedRoute>();
|
||||
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
|
||||
const mockDatadogIntegrationService = mock<DatadogOrganizationIntegrationService>();
|
||||
const mockIntegrationService = mock<OrganizationIntegrationService>();
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
name: "Integration 1",
|
||||
@@ -71,8 +69,7 @@ describe("IntegrationGridComponent", () => {
|
||||
provide: ActivatedRoute,
|
||||
useValue: mockActivatedRoute,
|
||||
},
|
||||
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
|
||||
{ provide: OrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: mock<ToastService>(),
|
||||
|
||||
@@ -1,69 +1,78 @@
|
||||
<app-header> </app-header>
|
||||
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex" *ngIf="organization$ | async as organization">
|
||||
<bit-tab [label]="'singleSignOn' | i18n" *ngIf="organization.useSso">
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">{{ "singleSignOn" | i18n }}</h2>
|
||||
<p bitTypography="body1">
|
||||
{{ "ssoDescStart" | i18n }}
|
||||
<a bitLink routerLink="../settings/sso" class="tw-lowercase">{{ "singleSignOn" | i18n }}</a>
|
||||
{{ "ssoDescEnd" | i18n }}
|
||||
</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.SSO"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
@let organization = organization$ | async;
|
||||
|
||||
<bit-tab
|
||||
[label]="'userProvisioning' | i18n"
|
||||
*ngIf="organization.useScim || organization.useDirectory"
|
||||
>
|
||||
<section class="tw-mb-9" *ngIf="organization.useScim">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "scimIntegration" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">
|
||||
{{ "scimIntegrationDescStart" | i18n }}
|
||||
<a bitLink routerLink="../settings/scim">{{ "scimIntegration" | i18n }}</a>
|
||||
{{ "scimIntegrationDescEnd" | i18n }}
|
||||
</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.SCIM"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
<section class="tw-mb-9" *ngIf="organization.useDirectory">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "bwdc" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "bwdcDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.BWDC"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
@if (organization) {
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex">
|
||||
@if (organization?.useSso) {
|
||||
<bit-tab [label]="'singleSignOn' | i18n">
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">{{ "singleSignOn" | i18n }}</h2>
|
||||
<p bitTypography="body1">
|
||||
{{ "ssoDescStart" | i18n }}
|
||||
<a bitLink routerLink="../settings/sso" class="tw-lowercase">{{
|
||||
"singleSignOn" | i18n
|
||||
}}</a>
|
||||
{{ "ssoDescEnd" | i18n }}
|
||||
</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.SSO"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
}
|
||||
|
||||
<bit-tab [label]="'eventManagement' | i18n" *ngIf="organization.useEvents">
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "eventManagement" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "eventManagementDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.EVENT"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
@if (organization?.useScim || organization?.useDirectory) {
|
||||
<bit-tab [label]="'userProvisioning' | i18n">
|
||||
<section class="tw-mb-9" *ngIf="organization?.useScim">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "scimIntegration" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">
|
||||
{{ "scimIntegrationDescStart" | i18n }}
|
||||
<a bitLink routerLink="../settings/scim">{{ "scimIntegration" | i18n }}</a>
|
||||
{{ "scimIntegrationDescEnd" | i18n }}
|
||||
</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.SCIM"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
<section class="tw-mb-9" *ngIf="organization?.useDirectory">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "bwdc" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "bwdcDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.BWDC"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
}
|
||||
|
||||
<bit-tab [label]="'deviceManagement' | i18n">
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "deviceManagement" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "deviceManagementDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.DEVICE"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
@if (organization?.useEvents) {
|
||||
<bit-tab [label]="'eventManagement' | i18n">
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "eventManagement" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "eventManagementDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.EVENT"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
}
|
||||
|
||||
<bit-tab [label]="'deviceManagement' | i18n">
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "deviceManagement" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "deviceManagementDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrationsList | filterIntegrations: IntegrationType.DEVICE"
|
||||
></app-integration-grid>
|
||||
</section>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } from "rxjs";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { 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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -21,6 +20,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "./integrations.pipe";
|
||||
|
||||
// attempted, but because bit-tab-group is not OnPush, caused more issues than it solved
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@@ -236,10 +236,12 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
// Sets the organization ID which also loads the integrations$
|
||||
this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => {
|
||||
this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id);
|
||||
this.datadogOrganizationIntegrationService.setOrganizationIntegrations(org.id);
|
||||
});
|
||||
this.organization$
|
||||
.pipe(
|
||||
switchMap((org) => this.organizationIntegrationService.setOrganizationId(org.id)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -247,8 +249,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
|
||||
private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
|
||||
private organizationIntegrationService: OrganizationIntegrationService,
|
||||
) {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike)
|
||||
@@ -260,7 +261,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
// Add the new event based items to the list
|
||||
if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) {
|
||||
const crowdstrikeIntegration: Integration = {
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
linkURL: "https://bitwarden.com/help/crowdstrike-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
@@ -272,7 +273,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
this.integrationsList.push(crowdstrikeIntegration);
|
||||
|
||||
const datadogIntegration: Integration = {
|
||||
name: OrganizationIntegrationServiceType.Datadog,
|
||||
name: OrganizationIntegrationServiceName.Datadog,
|
||||
linkURL: "https://bitwarden.com/help/datadog-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-datadog-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
@@ -286,42 +287,23 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// For all existing event based configurations loop through and assign the
|
||||
// organizationIntegration for the correct services.
|
||||
this.hecOrganizationIntegrationService.integrations$
|
||||
this.organizationIntegrationService.integrations$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// reset all integrations to null first - in case one was deleted
|
||||
// reset all event based integrations to null first - in case one was deleted
|
||||
this.integrationsList.forEach((i) => {
|
||||
if (i.integrationType === OrganizationIntegrationType.Hec) {
|
||||
i.organizationIntegration = null;
|
||||
}
|
||||
i.organizationIntegration = null;
|
||||
});
|
||||
|
||||
integrations.map((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
|
||||
if (item) {
|
||||
item.organizationIntegration = integration;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.datadogOrganizationIntegrationService.integrations$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// reset all integrations to null first - in case one was deleted
|
||||
this.integrationsList.forEach((i) => {
|
||||
if (i.integrationType === OrganizationIntegrationType.Datadog) {
|
||||
i.organizationIntegration = null;
|
||||
}
|
||||
});
|
||||
|
||||
integrations.map((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
|
||||
integrations.forEach((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceName);
|
||||
if (item) {
|
||||
item.organizationIntegration = integration;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -14,13 +13,8 @@ import { OrganizationIntegrationsRoutingModule } from "./organization-integratio
|
||||
imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: DatadogOrganizationIntegrationService,
|
||||
useClass: DatadogOrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: HecOrganizationIntegrationService,
|
||||
useClass: HecOrganizationIntegrationService,
|
||||
provide: OrganizationIntegrationService,
|
||||
useClass: OrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -9,8 +9,7 @@ import {} from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
@@ -41,8 +40,7 @@ class MockNewMenuComponent {}
|
||||
|
||||
describe("IntegrationsComponent", () => {
|
||||
let fixture: ComponentFixture<IntegrationsComponent>;
|
||||
const hecOrgIntegrationSvc = mock<HecOrganizationIntegrationService>();
|
||||
const datadogOrgIntegrationSvc = mock<DatadogOrganizationIntegrationService>();
|
||||
const orgIntegrationSvc = mock<OrganizationIntegrationService>();
|
||||
|
||||
const activatedRouteMock = {
|
||||
snapshot: { paramMap: { get: jest.fn() } },
|
||||
@@ -60,8 +58,7 @@ describe("IntegrationsComponent", () => {
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc },
|
||||
{ provide: DatadogOrganizationIntegrationService, useValue: datadogOrgIntegrationSvc },
|
||||
{ provide: OrganizationIntegrationService, useValue: orgIntegrationSvc },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(IntegrationsComponent);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -23,13 +22,8 @@ import { IntegrationsComponent } from "./integrations.component";
|
||||
],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: DatadogOrganizationIntegrationService,
|
||||
useClass: DatadogOrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: HecOrganizationIntegrationService,
|
||||
useClass: HecOrganizationIntegrationService,
|
||||
provide: OrganizationIntegrationService,
|
||||
useClass: OrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -52,12 +52,12 @@
|
||||
[relativeTo]="route.parent"
|
||||
>
|
||||
<bit-nav-item
|
||||
[text]="'importData' | i18n"
|
||||
[text]="'import' | i18n"
|
||||
route="settings/import"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'exportData' | i18n"
|
||||
[text]="'export' | i18n"
|
||||
route="settings/export"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
@@ -37,9 +38,11 @@ describe("SecretService", () => {
|
||||
let accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
</bit-form-field>
|
||||
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
{{ "exportData" | i18n }}
|
||||
{{ "export" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -124,7 +124,7 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy {
|
||||
const ref = openUserVerificationPrompt(this.dialogService, {
|
||||
data: {
|
||||
confirmDescription: "exportSecretsWarningDesc",
|
||||
confirmButtonText: "exportSecrets",
|
||||
confirmButtonText: "export",
|
||||
modalTitle: "confirmSecretsExport",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
<bit-hint>{{ "acceptedFormats" | i18n }} Bitwarden (json)</bit-hint>
|
||||
</bit-form-field>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
{{ "importData" | i18n }}
|
||||
{{ "import" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
@@ -38,9 +39,11 @@ describe("SecretsManagerPortingApiService", () => {
|
||||
let accountService: MockProxy<AccountService>;
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const routes: Routes = [
|
||||
component: SecretsManagerImportComponent,
|
||||
canActivate: [organizationPermissionsGuard((org) => org.isAdmin)],
|
||||
data: {
|
||||
titleId: "importData",
|
||||
titleId: "import",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -20,7 +20,7 @@ const routes: Routes = [
|
||||
component: SecretsManagerExportComponent,
|
||||
canActivate: [organizationPermissionsGuard((org) => org.isAdmin)],
|
||||
data: {
|
||||
titleId: "exportData",
|
||||
titleId: "export",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user