1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 19:23:19 +00:00

Merge branch 'main' into autofill/pm-22789_v2

This commit is contained in:
Colton Hurst
2025-07-31 17:37:43 -04:00
committed by GitHub
57 changed files with 926 additions and 605 deletions

View File

@@ -69,7 +69,6 @@ jobs:
azure_login_client_key_name: ${{ steps.config.outputs.azure_login_client_key_name }}
azure_login_subscription_id_key_name: ${{ steps.config.outputs.azure_login_subscription_id_key_name }}
retrieve_secrets_keyvault: ${{ steps.config.outputs.retrieve_secrets_keyvault }}
sync_utility: ${{ steps.config.outputs.sync_utility }}
sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }}
slack_channel_name: ${{ steps.config.outputs.slack_channel_name }}
steps:
@@ -127,8 +126,6 @@ jobs:
echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT
;;
esac
# Set the sync utility to use for deployment to the environment (az-sync or azcopy)
echo "sync_utility=azcopy" >> $GITHUB_OUTPUT
- name: Environment Protection
env:
@@ -337,32 +334,6 @@ jobs:
description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}'
ref: ${{ needs.artifact-check.outputs.artifact_build_commit }}
- name: Login to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets[needs.setup.outputs.azure_login_subscription_id_key_name] }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets[needs.setup.outputs.azure_login_client_key_name] }}
- name: Retrieve Storage Account connection string for az sync
if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }}
id: retrieve-secrets-az-sync
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }}
secrets: "sa-bitwarden-web-vault-dev-key-temp"
- name: Retrieve Storage Account name and SPN credentials for azcopy
if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }}
id: retrieve-secrets-azcopy
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }}
secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}'
if: ${{ inputs.build-web-run-id }}
uses: bitwarden/gh-actions/download-artifacts@main
@@ -389,28 +360,32 @@ jobs:
working-directory: apps/web
run: unzip ${{ env._ENVIRONMENT_ARTIFACT }}
- name: Sync to Azure Storage Account using az storage blob sync
if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }}
working-directory: apps/web
run: |
az storage blob sync \
--source "./build" \
--container '$web' \
--connection-string "${{ steps.retrieve-secrets-az-sync.outputs.sa-bitwarden-web-vault-dev-key-temp }}" \
--delete-destination=${{ inputs.force-delete-destination }}
- name: Login to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets[needs.setup.outputs.azure_login_subscription_id_key_name] }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets[needs.setup.outputs.azure_login_client_key_name] }}
- name: Retrieve Storage Account name
id: retrieve-secrets-azcopy
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }}
secrets: "sa-bitwarden-web-vault-name"
- name: Sync to Azure Storage Account using azcopy
if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }}
working-directory: apps/web
env:
AZCOPY_AUTO_LOGIN_TYPE: SPN
AZCOPY_SPA_APPLICATION_ID: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-appid }}
AZCOPY_SPA_CLIENT_SECRET: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-password }}
AZCOPY_TENANT_ID: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-tenant }}
AZCOPY_AUTO_LOGIN_TYPE: AZCLI
AZCOPY_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
run: |
azcopy sync ./build 'https://${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }}.blob.core.windows.net/$web/' \
--delete-destination=${{ inputs.force-delete-destination }} --compare-hash="MD5"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Debug sync logs
if: ${{ inputs.debug }}
run: cat /home/runner/.azcopy/*.log

View File

@@ -43,6 +43,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
@@ -591,7 +592,7 @@ export default class MainBackground {
this.singleUserStateProvider,
);
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
new DefaultActiveUserAccessor(this.accountService),
this.singleUserStateProvider,
);
this.derivedStateProvider = new InlineDerivedStateProvider();

View File

@@ -44,6 +44,7 @@ import {
} from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
@@ -377,7 +378,7 @@ export class ServiceContainer {
);
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
new DefaultActiveUserAccessor(this.accountService),
this.singleUserStateProvider,
);

View File

@@ -9,6 +9,7 @@ import { Subject, firstValueFrom } from "rxjs";
import { SsoUrlService } from "@bitwarden/auth/common";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
import { ClientType } from "@bitwarden/common/enums";
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -168,7 +169,7 @@ export class Main {
);
const activeUserStateProvider = new DefaultActiveUserStateProvider(
accountService,
new DefaultActiveUserAccessor(accountService),
singleUserStateProvider,
);

View File

@@ -2,8 +2,10 @@
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Observable, Subject, switchMap, takeUntil } from "rxjs";
import { Observable, Subject, switchMap, takeUntil, scheduled, asyncScheduler } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
import {
getOrganizationById,
OrganizationService,
@@ -33,13 +35,192 @@ import { Integration } from "../shared/components/integrations/models";
],
})
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
integrationsList: Integration[] = [];
// integrationsList: Integration[] = [];
tabIndex: number;
organization$: Observable<Organization>;
isEventBasedIntegrationsEnabled: boolean = false;
private destroy$ = new Subject<void>();
// initialize the integrations list with default integrations
integrationsList: Integration[] = [
{
name: "AD FS",
linkURL: "https://bitwarden.com/help/saml-adfs/",
image: "../../../../../../../images/integrations/azure-active-directory.svg",
type: IntegrationType.SSO,
},
{
name: "Auth0",
linkURL: "https://bitwarden.com/help/saml-auth0/",
image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "AWS",
linkURL: "https://bitwarden.com/help/saml-aws/",
image: "../../../../../../../images/integrations/aws-color.svg",
imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/saml-azure/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.SSO,
},
{
name: "Duo",
linkURL: "https://bitwarden.com/help/saml-duo/",
image: "../../../../../../../images/integrations/logo-duo-color.svg",
type: IntegrationType.SSO,
},
{
name: "Google",
linkURL: "https://bitwarden.com/help/saml-google/",
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "JumpCloud",
linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "KeyCloak",
linkURL: "https://bitwarden.com/help/saml-keycloak/",
image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
type: IntegrationType.SSO,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/saml-okta/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/saml-onelogin/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "PingFederate",
linkURL: "https://bitwarden.com/help/saml-pingfederate/",
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.SCIM,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/okta-scim-integration/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "JumpCloud",
linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "Ping Identity",
linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
type: IntegrationType.SCIM,
},
{
name: "Active Directory",
linkURL: "https://bitwarden.com/help/ldap-directory/",
image: "../../../../../../../images/integrations/azure-active-directory.svg",
type: IntegrationType.BWDC,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.BWDC,
},
{
name: "Google Workspace",
linkURL: "https://bitwarden.com/help/workspace-directory/",
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
type: IntegrationType.BWDC,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/okta-directory/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.BWDC,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/onelogin-directory/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.BWDC,
},
{
name: "Splunk",
linkURL: "https://bitwarden.com/help/splunk-siem/",
image: "../../../../../../../images/integrations/logo-splunk-black.svg",
imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
type: IntegrationType.EVENT,
},
{
name: "Microsoft Sentinel",
linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Rapid7",
linkURL: "https://bitwarden.com/help/rapid7-siem/",
image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
type: IntegrationType.EVENT,
},
{
name: "Elastic",
linkURL: "https://bitwarden.com/help/elastic-siem/",
image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Panther",
linkURL: "https://bitwarden.com/help/panther-siem/",
image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Microsoft Intune",
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
type: IntegrationType.DEVICE,
},
];
ngOnInit(): void {
const orgId = this.route.snapshot.params.organizationId;
this.organization$ = this.route.params.pipe(
switchMap((params) =>
this.accountService.activeAccount$.pipe(
@@ -51,6 +232,25 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
),
),
);
scheduled(this.orgIntegrationApiService.getOrganizationIntegrations(orgId), asyncScheduler)
.pipe(takeUntil(this.destroy$))
.subscribe((integrations) => {
// Update the integrations list with the fetched integrations
if (integrations && integrations.length > 0) {
integrations.forEach((integration) => {
const configJson = JSON.parse(integration.configuration || "{}");
const serviceName = configJson.service ?? "";
const existingIntegration = this.integrationsList.find((i) => i.name === serviceName);
if (existingIntegration) {
// if a configuration exists, then it is connected
existingIntegration.isConnected = !!integration.configuration;
existingIntegration.configuration = integration.configuration || "";
}
});
}
});
}
constructor(
@@ -58,6 +258,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService,
private accountService: AccountService,
private configService: ConfigService,
private orgIntegrationApiService: OrganizationIntegrationApiService,
) {
this.configService
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
@@ -66,182 +267,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
this.isEventBasedIntegrationsEnabled = isEnabled;
});
this.integrationsList = [
{
name: "AD FS",
linkURL: "https://bitwarden.com/help/saml-adfs/",
image: "../../../../../../../images/integrations/azure-active-directory.svg",
type: IntegrationType.SSO,
},
{
name: "Auth0",
linkURL: "https://bitwarden.com/help/saml-auth0/",
image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "AWS",
linkURL: "https://bitwarden.com/help/saml-aws/",
image: "../../../../../../../images/integrations/aws-color.svg",
imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/saml-azure/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.SSO,
},
{
name: "Duo",
linkURL: "https://bitwarden.com/help/saml-duo/",
image: "../../../../../../../images/integrations/logo-duo-color.svg",
type: IntegrationType.SSO,
},
{
name: "Google",
linkURL: "https://bitwarden.com/help/saml-google/",
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "JumpCloud",
linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "KeyCloak",
linkURL: "https://bitwarden.com/help/saml-keycloak/",
image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
type: IntegrationType.SSO,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/saml-okta/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/saml-onelogin/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "PingFederate",
linkURL: "https://bitwarden.com/help/saml-pingfederate/",
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.SCIM,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/okta-scim-integration/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "JumpCloud",
linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "Ping Identity",
linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
type: IntegrationType.SCIM,
},
{
name: "Active Directory",
linkURL: "https://bitwarden.com/help/ldap-directory/",
image: "../../../../../../../images/integrations/azure-active-directory.svg",
type: IntegrationType.BWDC,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.BWDC,
},
{
name: "Google Workspace",
linkURL: "https://bitwarden.com/help/workspace-directory/",
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
type: IntegrationType.BWDC,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/okta-directory/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.BWDC,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/onelogin-directory/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.BWDC,
},
{
name: "Splunk",
linkURL: "https://bitwarden.com/help/splunk-siem/",
image: "../../../../../../../images/integrations/logo-splunk-black.svg",
imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
type: IntegrationType.EVENT,
},
{
name: "Microsoft Sentinel",
linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Rapid7",
linkURL: "https://bitwarden.com/help/rapid7-siem/",
image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
type: IntegrationType.EVENT,
},
{
name: "Elastic",
linkURL: "https://bitwarden.com/help/elastic-siem/",
image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Panther",
linkURL: "https://bitwarden.com/help/panther-siem/",
image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Microsoft Intune",
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
type: IntegrationType.DEVICE,
},
];
if (this.isEventBasedIntegrationsEnabled) {
this.integrationsList.push({
name: "Crowdstrike",

View File

@@ -33,7 +33,7 @@
<p class="tw-mb-0">{{ description }}</p>
@if (canSetupConnection) {
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection(name)">
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
</button>
}

View File

@@ -1,12 +1,15 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
// eslint-disable-next-line no-restricted-imports
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
import { 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";
// FIXME: remove `src` and fix import
import { ToastService } from "@bitwarden/components";
// eslint-disable-next-line no-restricted-imports
import { SharedModule } from "@bitwarden/components/src/shared";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -17,6 +20,8 @@ describe("IntegrationCardComponent", () => {
let component: IntegrationCardComponent;
let fixture: ComponentFixture<IntegrationCardComponent>;
const mockI18nService = mock<I18nService>();
const activatedRoute = mock<ActivatedRoute>();
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
@@ -24,26 +29,22 @@ describe("IntegrationCardComponent", () => {
beforeEach(async () => {
// reset system theme
systemTheme$.next(ThemeType.Light);
activatedRoute.snapshot = {
paramMap: {
get: jest.fn().mockReturnValue("test-organization-id"),
},
} as any;
await TestBed.configureTestingModule({
imports: [IntegrationCardComponent, SharedModule],
providers: [
{
provide: ThemeStateService,
useValue: { selectedTheme$: usersPreferenceTheme$ },
},
{
provide: SYSTEM_THEME_OBSERVABLE,
useValue: systemTheme$,
},
{
provide: I18nPipe,
useValue: mock<I18nPipe>(),
},
{
provide: I18nService,
useValue: mockI18nService,
},
{ provide: ThemeStateService, useValue: { selectedTheme$: usersPreferenceTheme$ } },
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: systemTheme$ },
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
{ provide: I18nService, useValue: mockI18nService },
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
{ provide: ToastService, useValue: mock<ToastService>() },
],
}).compileComponents();
});

View File

@@ -9,13 +9,26 @@ import {
OnDestroy,
ViewChild,
} from "@angular/core";
import { Observable, Subject, combineLatest, takeUntil } from "rxjs";
import { ActivatedRoute } from "@angular/router";
import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
// eslint-disable-next-line no-restricted-imports
import {
OrganizationIntegrationType,
OrganizationIntegrationRequest,
OrganizationIntegrationResponse,
OrganizationIntegrationApiService,
} from "@bitwarden/bit-common/dirt/integrations/index";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { SharedModule } from "../../../../../../shared/shared.module";
import { openHecConnectDialog } from "../integration-dialog/index";
import { Integration } from "../models";
@Component({
selector: "app-integration-card",
@@ -30,6 +43,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
@Input() image: string;
@Input() imageDarkMode?: string;
@Input() linkURL: string;
@Input() integrationSettings: Integration;
/** Adds relevant `rel` attribute to external links */
@Input() externalURL?: boolean;
@@ -49,6 +63,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
private themeStateService: ThemeStateService,
@Inject(SYSTEM_THEME_OBSERVABLE)
private systemTheme$: Observable<ThemeType>,
private dialogService: DialogService,
private activatedRoute: ActivatedRoute,
private apiService: OrganizationIntegrationApiService,
private toastService: ToastService,
private i18nService: I18nService,
) {}
ngAfterViewInit() {
@@ -101,9 +120,58 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
return this.isConnected !== undefined;
}
setupConnection(app: string) {
// This method can be used to handle the connection logic for the integration
// For example, it could open a modal or redirect to a setup page
this.isConnected = !this.isConnected; // Toggle connection state for demonstration
async setupConnection() {
// invoke the dialog to connect the integration
const dialog = openHecConnectDialog(this.dialogService, {
data: {
settings: this.integrationSettings,
},
});
const result = await lastValueFrom(dialog.closed);
// the dialog was cancelled
if (!result || !result.success) {
return;
}
// save the integration
try {
const dbResponse = await this.saveHecIntegration(result.configuration);
this.isConnected = !!dbResponse.id;
} catch {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("failedToSaveIntegration"),
});
return;
}
}
async saveHecIntegration(configuration: string): Promise<OrganizationIntegrationResponse> {
const organizationId = this.activatedRoute.snapshot.paramMap.get(
"organizationId",
) as OrganizationId;
const request = new OrganizationIntegrationRequest(
OrganizationIntegrationType.Hec,
configuration,
);
const integrations = await this.apiService.getOrganizationIntegrations(organizationId);
const existingIntegration = integrations.find(
(i) => i.type === OrganizationIntegrationType.Hec,
);
if (existingIntegration) {
return await this.apiService.updateOrganizationIntegration(
organizationId,
existingIntegration.id,
request,
);
} else {
return await this.apiService.createOrganizationIntegration(organizationId, request);
}
}
}

View File

@@ -1,58 +0,0 @@
import { importProvidersFrom } from "@angular/core";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
import { IntegrationCardComponent } from "./integration-card.component";
class MockThemeService implements Partial<ThemeStateService> {}
export default {
title: "Web/Integration Layout/Integration Card",
component: IntegrationCardComponent,
decorators: [
applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
}),
moduleMetadata({
providers: [
{
provide: ThemeStateService,
useClass: MockThemeService,
},
{
provide: SYSTEM_THEME_OBSERVABLE,
useValue: of(ThemeTypes.Light),
},
],
}),
],
args: {
integrations: [],
},
} as Meta;
type Story = StoryObj<IntegrationCardComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<app-integration-card
[name]="name"
[image]="image"
[linkURL]="linkURL"
></app-integration-card>
`,
}),
args: {
name: "Bitwarden",
image: "/integrations/bitwarden-vertical-blue.svg",
linkURL: "https://bitwarden.com",
},
};

View File

@@ -0,0 +1,38 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle>
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
</span>
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
@if (loading) {
<ng-container #spinner>
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
</ng-container>
}
@if (!loading) {
<ng-container>
<bit-form-field>
<bit-label>{{ "url" | i18n }}</bit-label>
<input bitInput formControlName="url" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "bearerToken" | i18n }}</bit-label>
<input bitInput formControlName="bearerToken" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "index" | i18n }}</bit-label>
<input bitInput formControlName="index" />
</bit-form-field>
</ng-container>
}
</div>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
{{ "save" | i18n }}
</button>
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,176 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { mock } from "jest-mock-extended";
import { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { Integration } from "../../models";
import {
ConnectHecDialogComponent,
HecConnectDialogParams,
HecConnectDialogResult,
openHecConnectDialog,
} from "./connect-dialog-hec.component";
beforeAll(() => {
// Mock element.animate for jsdom
// the animate function is not available in jsdom, so we provide a mock implementation
// This is necessary for tests that rely on animations
// This mock does not perform any actual animations, it just provides a structure that allows tests
// to run without throwing errors related to missing animate function
if (!HTMLElement.prototype.animate) {
HTMLElement.prototype.animate = function () {
return {
play: () => {},
pause: () => {},
finish: () => {},
cancel: () => {},
reverse: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
onfinish: null,
oncancel: null,
startTime: 0,
currentTime: 0,
playbackRate: 1,
playState: "idle",
replaceState: "active",
effect: null,
finished: Promise.resolve(),
id: "",
remove: () => {},
timeline: null,
ready: Promise.resolve(),
} as unknown as Animation;
};
}
});
describe("ConnectDialogHecComponent", () => {
let component: ConnectHecDialogComponent;
let fixture: ComponentFixture<ConnectHecDialogComponent>;
let dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
const mockI18nService = mock<I18nService>();
const integrationMock: Integration = {
name: "Test Integration",
image: "test-image.png",
linkURL: "https://example.com",
imageDarkMode: "test-image-dark.png",
newBadgeExpiration: "2024-12-31",
description: "Test Description",
isConnected: false,
canSetupConnection: true,
type: IntegrationType.EVENT,
} as Integration;
const connectInfo: HecConnectDialogParams = { settings: integrationMock };
beforeEach(async () => {
dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule],
providers: [
FormBuilder,
{ provide: DIALOG_DATA, useValue: connectInfo },
{ provide: DialogRef, useValue: dialogRefMock },
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
{ provide: I18nService, useValue: mockI18nService },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConnectHecDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
mockI18nService.t.mockImplementation((key) => key);
});
it("should create the component", () => {
expect(component).toBeTruthy();
});
it("should initialize form with empty values", () => {
expect(component.formGroup.value).toEqual({
url: "",
bearerToken: "",
index: "",
service: "Test Integration",
});
});
it("should have required validators for all fields", () => {
component.formGroup.setValue({ url: "", bearerToken: "", index: "", service: "" });
expect(component.formGroup.valid).toBeFalsy();
component.formGroup.setValue({
url: "https://test.com",
bearerToken: "token",
index: "1",
service: "Test Service",
});
expect(component.formGroup.valid).toBeTruthy();
});
it("should invalidate url if not matching pattern", () => {
component.formGroup.setValue({
url: "ftp://test.com",
bearerToken: "token",
index: "1",
service: "Test Service",
});
expect(component.formGroup.valid).toBeFalsy();
component.formGroup.setValue({
url: "https://test.com",
bearerToken: "token",
index: "1",
service: "Test Service",
});
expect(component.formGroup.valid).toBeTruthy();
});
it("should call dialogRef.close with correct result on submit", async () => {
component.formGroup.setValue({
url: "https://test.com",
bearerToken: "token",
index: "1",
service: "Test Service",
});
await component.submit();
expect(dialogRefMock.close).toHaveBeenCalledWith({
integrationSettings: integrationMock,
configuration: JSON.stringify({
url: "https://test.com",
bearerToken: "token",
index: "1",
service: "Test Service",
}),
success: true,
error: null,
});
});
});
describe("openCrowdstrikeConnectDialog", () => {
it("should call dialogService.open with correct params", () => {
const dialogServiceMock = mock<DialogService>();
const config: DialogConfig<HecConnectDialogParams, DialogRef<HecConnectDialogResult>> = {
data: { settings: { name: "Test" } as Integration },
} as any;
openHecConnectDialog(dialogServiceMock, config);
expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectHecDialogComponent, config);
});
});

View File

@@ -0,0 +1,81 @@
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { Integration } from "../../models";
export type HecConnectDialogParams = {
settings: Integration;
};
export interface HecConnectDialogResult {
integrationSettings: Integration;
configuration: string;
success: boolean;
error: string | null;
}
@Component({
templateUrl: "./connect-dialog-hec.component.html",
imports: [SharedModule],
})
export class ConnectHecDialogComponent implements OnInit {
loading = false;
formGroup = this.formBuilder.group({
url: ["", [Validators.required, Validators.pattern("https?://.+")]],
bearerToken: ["", Validators.required],
index: ["", Validators.required],
service: ["", Validators.required],
});
constructor(
@Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams,
protected formBuilder: FormBuilder,
private dialogRef: DialogRef<HecConnectDialogResult>,
) {}
ngOnInit(): void {
const settings = this.getSettingsAsJson(this.connectInfo.settings.configuration ?? "");
if (settings) {
this.formGroup.patchValue({
url: settings?.url || "",
bearerToken: settings?.bearerToken || "",
index: settings?.index || "",
service: this.connectInfo.settings.name,
});
}
}
getSettingsAsJson(configuration: string) {
try {
return JSON.parse(configuration);
} catch {
return {};
}
}
submit = async (): Promise<void> => {
const formJson = this.formGroup.getRawValue();
const result: HecConnectDialogResult = {
integrationSettings: this.connectInfo.settings,
configuration: JSON.stringify(formJson),
success: true,
error: null,
};
this.dialogRef.close(result);
return;
};
}
export function openHecConnectDialog(
dialogService: DialogService,
config: DialogConfig<HecConnectDialogParams, DialogRef<HecConnectDialogResult>>,
) {
return dialogService.open<HecConnectDialogResult>(ConnectHecDialogComponent, config);
}

View File

@@ -0,0 +1 @@
export * from "./connect-dialog/connect-dialog-hec.component";

View File

@@ -16,6 +16,7 @@
[description]="integration.description | i18n"
[isConnected]="integration.isConnected"
[canSetupConnection]="integration.canSetupConnection"
[integrationSettings]="integration"
></app-integration-card>
</li>
</ul>

View File

@@ -1,14 +1,20 @@
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 { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
// eslint-disable-next-line no-restricted-imports
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
import { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
// eslint-disable-next-line import/order
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
// FIXME: remove `src` and fix import
import { ToastService } from "@bitwarden/components";
// eslint-disable-next-line no-restricted-imports
import { SharedModule } from "@bitwarden/components/src/shared";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -21,6 +27,8 @@ import { IntegrationGridComponent } from "./integration-grid.component";
describe("IntegrationGridComponent", () => {
let component: IntegrationGridComponent;
let fixture: ComponentFixture<IntegrationGridComponent>;
const mockActivatedRoute = mock<ActivatedRoute>();
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
const integrations: Integration[] = [
{
name: "Integration 1",
@@ -37,6 +45,12 @@ describe("IntegrationGridComponent", () => {
];
beforeEach(() => {
mockActivatedRoute.snapshot = {
paramMap: {
get: jest.fn().mockReturnValue("test-organization-id"),
},
} as any;
TestBed.configureTestingModule({
imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule],
providers: [
@@ -56,6 +70,18 @@ describe("IntegrationGridComponent", () => {
provide: I18nService,
useValue: mock<I18nService>({ t: (key, p1) => key + " " + p1 }),
},
{
provide: ActivatedRoute,
useValue: mockActivatedRoute,
},
{
provide: OrganizationIntegrationApiService,
useValue: mockOrgIntegrationApiService,
},
{
provide: ToastService,
useValue: mock<ToastService>(),
},
],
});

View File

@@ -1,70 +0,0 @@
import { importProvidersFrom } from "@angular/core";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { IntegrationType } from "@bitwarden/common/enums";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
class MockThemeService implements Partial<ThemeStateService> {}
export default {
title: "Web/Integration Layout/Integration Grid",
component: IntegrationGridComponent,
decorators: [
applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
}),
moduleMetadata({
imports: [IntegrationCardComponent],
providers: [
{
provide: ThemeStateService,
useClass: MockThemeService,
},
{
provide: SYSTEM_THEME_OBSERVABLE,
useValue: of(ThemeTypes.Dark),
},
],
}),
],
} as Meta;
type Story = StoryObj<IntegrationGridComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<app-integration-grid [integrations]="integrations"></app-integration-grid>
`,
}),
args: {
integrations: [
{
name: "Card 1",
linkURL: "https://bitwarden.com",
image: "/integrations/bitwarden-vertical-blue.svg",
type: IntegrationType.SSO,
},
{
name: "Card 2",
linkURL: "https://bitwarden.com",
image: "/integrations/bitwarden-vertical-blue.svg",
type: IntegrationType.SDK,
},
{
name: "Card 3",
linkURL: "https://bitwarden.com",
image: "/integrations/bitwarden-vertical-blue.svg",
type: IntegrationType.SCIM,
},
],
},
};

View File

@@ -20,4 +20,5 @@ export type Integration = {
description?: string;
isConnected?: boolean;
canSetupConnection?: boolean;
configuration?: string;
};

View File

@@ -304,6 +304,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
this.fetchingTaxAmount = true;
if (!this.taxInfoComponent.validate()) {
this.fetchingTaxAmount = false;
return 0;
}
@@ -326,7 +327,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
const response = await this.taxService.previewTaxAmountForOrganizationTrial(request);
this.fetchingTaxAmount = false;
return response.taxAmount;
return response;
};
get price() {

View File

@@ -12,6 +12,7 @@
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[loading]="submitting"
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
[primaryButtonText]="{ key: 'createAccount' }"
@@ -23,6 +24,7 @@
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[loading]="submitting"
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
[primaryButtonText]="{ key: 'createAccount' }"

View File

@@ -41,6 +41,8 @@ import {
InternalUserDecryptionOptionsServiceAbstraction,
LoginEmailService,
} from "@bitwarden/auth/common";
// eslint-disable-next-line no-restricted-imports
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
@@ -392,6 +394,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultDeviceManagementComponentService,
deps: [],
}),
safeProvider({
provide: OrganizationIntegrationApiService,
useClass: OrganizationIntegrationApiService,
deps: [ApiService],
}),
];
@NgModule({

View File

@@ -9481,6 +9481,9 @@
"crowdstrikeEventIntegrationDesc": {
"message": "Send event data to your Logscale instance"
},
"failedToSaveIntegration": {
"message": "Failed to save integration. Please try again later."
},
"deviceIdMissing": {
"message": "Device ID is missing"
},
@@ -9562,6 +9565,15 @@
"createNewClientToManageAsProvider": {
"message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle."
},
"url": {
"message": "URL"
},
"bearerToken": {
"message": "Bearer Token"
},
"index": {
"message": "Index"
},
"selectAPlan": {
"message": "Select a plan"
},

View File

@@ -1 +1,6 @@
export * from "./services";
export * from "./models/organization-integration-type";
export * from "./models/organization-integration-request";
export * from "./models/organization-integration-response";
export * from "./models/organization-integration-configuration-request";
export * from "./models/organization-integration-configuration-response";

View File

@@ -5,11 +5,13 @@ import { OrganizationIntegrationType } from "./organization-integration-type";
export class OrganizationIntegrationResponse extends BaseResponse {
id: OrganizationIntegrationId;
organizationIntegrationType: OrganizationIntegrationType;
type: OrganizationIntegrationType;
configuration: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.organizationIntegrationType = this.getResponseProperty("Type");
this.type = this.getResponseProperty("Type");
this.configuration = this.getResponseProperty("Configuration");
}
}

View File

@@ -11,17 +11,17 @@ import { OrganizationIntegrationApiService } from "./organization-integration-ap
export const mockIntegrationResponse: any = {
id: "1" as OrganizationIntegrationId,
organizationIntegrationType: OrganizationIntegrationType.Hec,
type: OrganizationIntegrationType.Hec,
};
export const mockIntegrationResponses: any[] = [
{
id: "1" as OrganizationIntegrationId,
OrganizationIntegrationType: OrganizationIntegrationType.Hec,
type: OrganizationIntegrationType.Hec,
},
{
id: "2" as OrganizationIntegrationId,
OrganizationIntegrationType: OrganizationIntegrationType.Webhook,
type: OrganizationIntegrationType.Webhook,
},
];
@@ -46,7 +46,7 @@ describe("OrganizationIntegrationApiService", () => {
expect(result).toEqual(mockIntegrationResponses);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`organizations/${orgId}/integrations`,
`/organizations/${orgId}/integrations`,
null,
true,
true,
@@ -63,12 +63,10 @@ describe("OrganizationIntegrationApiService", () => {
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse));
const result = await service.createOrganizationIntegration(orgId, request);
expect(result.organizationIntegrationType).toEqual(
mockIntegrationResponse.organizationIntegrationType,
);
expect(result.type).toEqual(mockIntegrationResponse.type);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`organizations/${orgId.toString()}/integrations`,
`/organizations/${orgId.toString()}/integrations`,
request,
true,
true,
@@ -86,12 +84,10 @@ describe("OrganizationIntegrationApiService", () => {
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse));
const result = await service.updateOrganizationIntegration(orgId, integrationId, request);
expect(result.organizationIntegrationType).toEqual(
mockIntegrationResponse.organizationIntegrationType,
);
expect(result.type).toEqual(mockIntegrationResponse.type);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`organizations/${orgId}/integrations/${integrationId}`,
`/organizations/${orgId}/integrations/${integrationId}`,
request,
true,
true,
@@ -106,7 +102,7 @@ describe("OrganizationIntegrationApiService", () => {
expect(apiService.send).toHaveBeenCalledWith(
"DELETE",
`organizations/${orgId}/integrations/${integrationId}`,
`/organizations/${orgId}/integrations/${integrationId}`,
null,
true,
false,

View File

@@ -15,7 +15,7 @@ export class OrganizationIntegrationApiService {
): Promise<OrganizationIntegrationResponse[]> {
const response = await this.apiService.send(
"GET",
`organizations/${orgId}/integrations`,
`/organizations/${orgId}/integrations`,
null,
true,
true,
@@ -29,7 +29,7 @@ export class OrganizationIntegrationApiService {
): Promise<OrganizationIntegrationResponse> {
const response = await this.apiService.send(
"POST",
`organizations/${orgId}/integrations`,
`/organizations/${orgId}/integrations`,
request,
true,
true,
@@ -44,7 +44,7 @@ export class OrganizationIntegrationApiService {
): Promise<OrganizationIntegrationResponse> {
const response = await this.apiService.send(
"PUT",
`organizations/${orgId}/integrations/${integrationId}`,
`/organizations/${orgId}/integrations/${integrationId}`,
request,
true,
true,
@@ -58,7 +58,7 @@ export class OrganizationIntegrationApiService {
): Promise<any> {
await this.apiService.send(
"DELETE",
`organizations/${orgId}/integrations/${integrationId}`,
`/organizations/${orgId}/integrations/${integrationId}`,
null,
true,
false,

View File

@@ -1,6 +1,7 @@
import { Component } from "@angular/core";
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 { of } from "rxjs";
@@ -8,9 +9,12 @@ import {} from "@bitwarden/web-vault/app/shared";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { ToastService } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component";
import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component";
@@ -33,23 +37,25 @@ class MockNewMenuComponent {}
describe("IntegrationsComponent", () => {
let fixture: ComponentFixture<IntegrationsComponent>;
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
const activatedRouteMock = {
snapshot: { paramMap: { get: jest.fn() } },
};
const mockI18nService = mock<I18nService>();
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent],
imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent],
providers: [
{
provide: I18nService,
useValue: mock<I18nService>(),
},
{
provide: ThemeStateService,
useValue: mock<ThemeStateService>(),
},
{
provide: SYSTEM_THEME_OBSERVABLE,
useValue: of(ThemeType.Light),
},
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: ThemeStateService, useValue: mock<ThemeStateService>() },
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) },
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
{ provide: I18nService, useValue: mockI18nService },
],
}).compileComponents();
fixture = TestBed.createComponent(IntegrationsComponent);

View File

@@ -110,6 +110,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
@@ -232,6 +233,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import {
ActiveUserAccessor,
ActiveUserStateProvider,
DerivedStateProvider,
GlobalStateProvider,
@@ -1271,10 +1273,15 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultGlobalStateProvider,
deps: [StorageServiceProvider, LogService],
}),
safeProvider({
provide: ActiveUserAccessor,
useClass: DefaultActiveUserAccessor,
deps: [AccountServiceAbstraction],
}),
safeProvider({
provide: ActiveUserStateProvider,
useClass: DefaultActiveUserStateProvider,
deps: [AccountServiceAbstraction, SingleUserStateProvider],
deps: [ActiveUserAccessor, SingleUserStateProvider],
}),
safeProvider({
provide: SingleUserStateProvider,

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended";
import { Observable, map, of, switchMap, take } from "rxjs";
import { BehaviorSubject, map, Observable, of, switchMap, take } from "rxjs";
import {
GlobalState,
@@ -16,11 +16,11 @@ import {
DeriveDefinition,
DerivedStateProvider,
UserKeyDefinition,
ActiveUserAccessor,
} from "../src/platform/state";
import { UserId } from "../src/types/guid";
import { DerivedStateDependencies } from "../src/types/state";
import { FakeAccountService } from "./fake-account-service";
import {
FakeActiveUserState,
FakeDerivedState,
@@ -28,6 +28,35 @@ import {
FakeSingleUserState,
} from "./fake-state";
export interface MinimalAccountService {
activeUserId: UserId | null;
activeAccount$: Observable<{ id: UserId } | null>;
}
export class FakeActiveUserAccessor implements MinimalAccountService, ActiveUserAccessor {
private _subject: BehaviorSubject<UserId | null>;
constructor(startingUser: UserId | null) {
this._subject = new BehaviorSubject(startingUser);
this.activeAccount$ = this._subject
.asObservable()
.pipe(map((id) => (id != null ? { id } : null)));
this.activeUserId$ = this._subject.asObservable();
}
get activeUserId(): UserId {
return this._subject.value;
}
activeUserId$: Observable<UserId>;
activeAccount$: Observable<{ id: UserId }>;
switch(user: UserId | null) {
this._subject.next(user);
}
}
export class FakeGlobalStateProvider implements GlobalStateProvider {
mock = mock<GlobalStateProvider>();
establishedMocks: Map<string, FakeGlobalState<unknown>> = new Map();
@@ -138,18 +167,18 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
}
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId>;
activeUserId$: Observable<UserId | null>;
states: Map<string, FakeActiveUserState<unknown>> = new Map();
constructor(
public accountService: FakeAccountService,
public accountServiceAccessor: MinimalAccountService,
readonly updateSyncCallback?: (
key: UserKeyDefinition<unknown>,
userId: UserId,
newValue: unknown,
) => Promise<void>,
) {
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
this.activeUserId$ = accountServiceAccessor.activeAccount$.pipe(map((a) => a?.id));
}
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
@@ -182,9 +211,13 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
}
private buildFakeState<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T) {
const state = new FakeActiveUserState<T>(this.accountService, initialValue, async (...args) => {
await this.updateSyncCallback?.(userKeyDefinition, ...args);
});
const state = new FakeActiveUserState<T>(
this.accountServiceAccessor,
initialValue,
async (...args) => {
await this.updateSyncCallback?.(userKeyDefinition, ...args);
},
);
state.keyDefinition = userKeyDefinition;
return state;
}
@@ -256,14 +289,14 @@ export class FakeStateProvider implements StateProvider {
return this.derived.get(parentState$, deriveDefinition, dependencies);
}
constructor(public accountService: FakeAccountService) {}
constructor(private activeAccountAccessor: MinimalAccountService) {}
private distributeSingleUserUpdate(
key: UserKeyDefinition<unknown>,
userId: UserId,
newState: unknown,
) {
if (this.activeUser.accountService.activeUserId === userId) {
if (this.activeUser.accountServiceAccessor.activeUserId === userId) {
const state = this.activeUser.getFake(key, { allowInit: false });
state?.nextState(newState, { syncValue: false });
}
@@ -284,7 +317,7 @@ export class FakeStateProvider implements StateProvider {
this.distributeSingleUserUpdate.bind(this),
);
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(
this.accountService,
this.activeAccountAccessor,
this.distributeActiveUserUpdate.bind(this),
);
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();

View File

@@ -18,7 +18,7 @@ import { CombinedState, activeMarker } from "../src/platform/state/user-state";
import { UserId } from "../src/types/guid";
import { DerivedStateDependencies } from "../src/types/state";
import { FakeAccountService } from "./fake-account-service";
import { MinimalAccountService } from "./fake-state-provider";
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
shouldUpdate: () => true,
@@ -177,7 +177,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
combinedState$: Observable<CombinedState<T>>;
constructor(
private accountService: FakeAccountService,
private activeAccountAccessor: MinimalAccountService,
initialValue?: T,
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
) {
@@ -194,14 +194,10 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
}
get userId() {
return this.accountService.activeUserId;
}
nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
this.stateSubject.next({
syncValue,
combinedState: [this.userId, state],
combinedState: [this.activeAccountAccessor.activeUserId, state],
});
}
@@ -216,12 +212,12 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
: null;
if (!options.shouldUpdate(current, combinedDependencies)) {
return [this.userId, current];
return [this.activeAccountAccessor.activeUserId, current];
}
const newState = configureState(current, combinedDependencies);
this.nextState(newState);
this.nextMock([this.userId, newState]);
return [this.userId, newState];
this.nextMock([this.activeAccountAccessor.activeUserId, newState]);
return [this.activeAccountAccessor.activeUserId, newState];
}
/** Tracks update values resolved by `FakeState.update` */

View File

@@ -0,0 +1,19 @@
import { map, Observable } from "rxjs";
import { UserId } from "@bitwarden/user-core";
import { ActiveUserAccessor } from "../../platform/state";
import { AccountService } from "../abstractions/account.service";
/**
* Implementation for Platform so they can avoid a direct dependency on AccountService. Not for general consumption.
*/
export class DefaultActiveUserAccessor implements ActiveUserAccessor {
constructor(private readonly accountService: AccountService) {
this.activeUserId$ = this.accountService.activeAccount$.pipe(
map((a) => (a != null ? a.id : null)),
);
}
activeUserId$: Observable<UserId | null>;
}

View File

@@ -3,7 +3,6 @@ import { PreviewIndividualInvoiceRequest } from "../models/request/preview-indiv
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax";
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
import { PreviewTaxAmountResponse } from "../models/response/tax";
export abstract class TaxServiceAbstraction {
abstract getCountries(): CountryListItem[];
@@ -20,5 +19,5 @@ export abstract class TaxServiceAbstraction {
abstract previewTaxAmountForOrganizationTrial: (
request: PreviewTaxAmountForOrganizationTrialRequest,
) => Promise<PreviewTaxAmountResponse>;
) => Promise<number>;
}

View File

@@ -1,5 +1,4 @@
import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax";
import { PreviewTaxAmountResponse } from "@bitwarden/common/billing/models/response/tax";
import { ApiService } from "../../abstractions/api.service";
import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction";
@@ -306,13 +305,14 @@ export class TaxService implements TaxServiceAbstraction {
async previewTaxAmountForOrganizationTrial(
request: PreviewTaxAmountForOrganizationTrialRequest,
): Promise<PreviewTaxAmountResponse> {
return await this.apiService.send(
): Promise<number> {
const response = await this.apiService.send(
"POST",
"/tax/preview-amount/organization-trial",
request,
true,
true,
);
return response as number;
}
}

View File

@@ -1,6 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify, Opaque } from "type-fest";
import { Jsonify } from "type-fest";
import { EncString as SdkEncString } from "@bitwarden/sdk-internal";
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../platform/enums";
import { Encrypted } from "../../../platform/interfaces/encrypted";
@@ -10,7 +12,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
export const DECRYPT_ERROR = "[error: cannot decrypt]";
export class EncString implements Encrypted {
encryptedString?: EncryptedString;
encryptedString?: SdkEncString;
encryptionType?: EncryptionType;
decryptedValue?: string;
data?: string;
@@ -42,7 +44,11 @@ export class EncString implements Encrypted {
return this.data == null ? null : Utils.fromB64ToArray(this.data);
}
toJSON() {
toSdk(): SdkEncString {
return this.encryptedString;
}
toJSON(): string {
return this.encryptedString as string;
}
@@ -56,14 +62,14 @@ export class EncString implements Encrypted {
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
if (iv != null) {
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
this.encryptedString = (encType + "." + iv + "|" + data) as SdkEncString;
} else {
this.encryptedString = (encType + "." + data) as EncryptedString;
this.encryptedString = (encType + "." + data) as SdkEncString;
}
// mac
if (mac != null) {
this.encryptedString = (this.encryptedString + "|" + mac) as EncryptedString;
this.encryptedString = (this.encryptedString + "|" + mac) as SdkEncString;
}
this.encryptionType = encType;
@@ -73,7 +79,7 @@ export class EncString implements Encrypted {
}
private initFromEncryptedString(encryptedString: string) {
this.encryptedString = encryptedString as EncryptedString;
this.encryptedString = encryptedString as SdkEncString;
if (!this.encryptedString) {
return;
}
@@ -191,4 +197,8 @@ export class EncString implements Encrypted {
}
}
export type EncryptedString = Opaque<string, "EncString">;
/**
* Temporary type mapping until consumers are moved over.
* @deprecated - Use SdkEncString directly
*/
export type EncryptedString = SdkEncString;

View File

@@ -22,6 +22,7 @@ import {
ClientSettings,
DeviceType as SdkDeviceType,
TokenProvider,
UnsignedSharedKey,
} from "@bitwarden/sdk-internal";
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
@@ -237,7 +238,7 @@ export class DefaultSdkService implements SdkService {
organizationKeys: new Map(
Object.entries(orgKeys ?? {})
.filter(([_, v]) => v.type === "organization")
.map(([k, v]) => [k, v.key]),
.map(([k, v]) => [k, v.key as UnsignedSharedKey]),
),
});
}

View File

@@ -0,0 +1,11 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/user-core";
export abstract class ActiveUserAccessor {
/**
* Returns a stream of the current active user for the application. The stream either emits the user id for that account
* or returns null if there is no current active user.
*/
abstract activeUserId$: Observable<UserId | null>;
}

View File

@@ -1,37 +0,0 @@
import { mock } from "jest-mock-extended";
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
import { UserId } from "../../../types/guid";
import { SingleUserStateProvider } from "../user-state.provider";
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
describe("DefaultActiveUserStateProvider", () => {
const singleUserStateProvider = mock<SingleUserStateProvider>();
const userId = "userId" as UserId;
const accountInfo = {
id: userId,
name: "name",
email: "email",
emailVerified: false,
};
const accountService = mockAccountServiceWith(userId, accountInfo);
let sut: DefaultActiveUserStateProvider;
beforeEach(() => {
sut = new DefaultActiveUserStateProvider(accountService, singleUserStateProvider);
});
afterEach(() => {
jest.resetAllMocks();
});
it("should track the active User id from account service", () => {
const emissions = trackEmissions(sut.activeUserId$);
accountService.activeAccountSubject.next(undefined);
accountService.activeAccountSubject.next(accountInfo);
expect(emissions).toEqual([userId, undefined, userId]);
});
});

View File

@@ -1,9 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, distinctUntilChanged, map } from "rxjs";
import { Observable, distinctUntilChanged } from "rxjs";
import { AccountService } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { ActiveUserAccessor } from "../active-user.accessor";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState } from "../user-state";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
@@ -14,11 +14,10 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId | undefined>;
constructor(
private readonly accountService: AccountService,
private readonly activeAccountAccessor: ActiveUserAccessor,
private readonly singleUserStateProvider: SingleUserStateProvider,
) {
this.activeUserId$ = this.accountService.activeAccount$.pipe(
map((account) => account?.id),
this.activeUserId$ = this.activeAccountAccessor.activeUserId$.pipe(
// To avoid going to storage when we don't need to, only get updates when there is a true change.
distinctUntilChanged((a, b) => (a == null || b == null ? a == b : a === b)), // Treat null and undefined as equal
);

View File

@@ -3,14 +3,13 @@
* @jest-environment ../shared/test.environment.ts
*/
import { any, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, map, of, timeout } from "rxjs";
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs";
import { Jsonify } from "type-fest";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { awaitAsync, trackEmissions } from "../../../../spec";
import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { Account } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { LogService } from "../../abstractions/log.service";
import { StateDefinition } from "../state-definition";
@@ -48,7 +47,7 @@ describe("DefaultActiveUserState", () => {
const storageServiceProvider = mock<StorageServiceProvider>();
const stateEventRegistrarService = mock<StateEventRegistrarService>();
const logService = mock<LogService>();
let activeAccountSubject: BehaviorSubject<Account | null>;
let activeAccountSubject: BehaviorSubject<UserId | null>;
let singleUserStateProvider: DefaultSingleUserStateProvider;
@@ -64,11 +63,11 @@ describe("DefaultActiveUserState", () => {
logService,
);
activeAccountSubject = new BehaviorSubject<Account | null>(null);
activeAccountSubject = new BehaviorSubject<UserId | null>(null);
userState = new DefaultActiveUserState(
testKeyDefinition,
activeAccountSubject.asObservable().pipe(map((a) => a?.id)),
activeAccountSubject.asObservable(),
singleUserStateProvider,
);
});
@@ -83,12 +82,7 @@ describe("DefaultActiveUserState", () => {
const changeActiveUser = async (id: string) => {
const userId = makeUserId(id);
activeAccountSubject.next({
id: userId,
email: `test${id}@example.com`,
emailVerified: false,
name: `Test User ${id}`,
});
activeAccountSubject.next(userId);
await awaitAsync();
};
@@ -588,7 +582,7 @@ describe("DefaultActiveUserState", () => {
});
it("does not await updates if the active user changes", async () => {
const initialUserId = (await firstValueFrom(activeAccountSubject)).id;
const initialUserId = activeAccountSubject.value;
expect(initialUserId).toBe(userId);
trackEmissions(userState.state$);
await awaitAsync(); // storage updates are behind a promise

View File

@@ -4,16 +4,16 @@
*/
import { Observable, of } from "rxjs";
import { UserId } from "@bitwarden/user-core";
import { awaitAsync, trackEmissions } from "../../../../spec";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
import {
FakeActiveUserAccessor,
FakeActiveUserStateProvider,
FakeDerivedStateProvider,
FakeGlobalStateProvider,
FakeSingleUserStateProvider,
} from "../../../../spec/fake-state-provider";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { UserId } from "../../../types/guid";
import { DeriveDefinition } from "../derive-definition";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
@@ -27,12 +27,12 @@ describe("DefaultStateProvider", () => {
let singleUserStateProvider: FakeSingleUserStateProvider;
let globalStateProvider: FakeGlobalStateProvider;
let derivedStateProvider: FakeDerivedStateProvider;
let accountService: FakeAccountService;
let activeAccountAccessor: FakeActiveUserAccessor;
const userId = "fakeUserId" as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
activeAccountAccessor = new FakeActiveUserAccessor(userId);
activeUserStateProvider = new FakeActiveUserStateProvider(activeAccountAccessor);
singleUserStateProvider = new FakeSingleUserStateProvider();
globalStateProvider = new FakeGlobalStateProvider();
derivedStateProvider = new FakeDerivedStateProvider();
@@ -70,12 +70,6 @@ describe("DefaultStateProvider", () => {
userId?: UserId,
) => Observable<string>,
) => {
const accountInfo = {
email: "email",
emailVerified: false,
name: "name",
status: AuthenticationStatus.LoggedOut,
};
const keyDefinition = new UserKeyDefinition<string>(
new StateDefinition("test", "disk"),
"test",
@@ -97,7 +91,7 @@ describe("DefaultStateProvider", () => {
});
it("should follow the current active user if no userId is provided", async () => {
accountService.activeAccountSubject.next({ id: userId, ...accountInfo });
activeAccountAccessor.switch(userId);
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(methodUnderTest(keyDefinition));
@@ -113,7 +107,7 @@ describe("DefaultStateProvider", () => {
state.nextState("value");
const emissions = trackEmissions(methodUnderTest(keyDefinition));
accountService.activeAccountSubject.next({ id: "newUserId" as UserId, ...accountInfo });
activeAccountAccessor.switch("newUserId" as UserId);
const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition));
state.nextState("value2");
state.nextState("value3");
@@ -125,12 +119,6 @@ describe("DefaultStateProvider", () => {
);
describe("getUserState$", () => {
const accountInfo = {
email: "email",
emailVerified: false,
name: "name",
status: AuthenticationStatus.LoggedOut,
};
const keyDefinition = new UserKeyDefinition<string>(
new StateDefinition("test", "disk"),
"test",
@@ -141,7 +129,7 @@ describe("DefaultStateProvider", () => {
);
it("should not emit any values until a truthy user id is supplied", async () => {
accountService.activeAccountSubject.next(null);
activeAccountAccessor.switch(null);
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
@@ -151,7 +139,7 @@ describe("DefaultStateProvider", () => {
expect(emissions).toHaveLength(0);
accountService.activeAccountSubject.next({ id: userId, ...accountInfo });
activeAccountAccessor.switch(userId);
await awaitAsync();
@@ -170,7 +158,7 @@ describe("DefaultStateProvider", () => {
);
it("should emit default value if no userId supplied and first active user id emission in falsy", async () => {
accountService.activeAccountSubject.next(null);
activeAccountAccessor.switch(null);
const emissions = trackEmissions(
sut.getUserStateOrDefault$(keyDefinition, {

View File

@@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
import { FakeActiveUserAccessor } from "../../../../spec";
import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { UserId } from "../../../types/guid";
import { LogService } from "../../abstractions/log.service";
@@ -39,7 +39,7 @@ describe("Specific State Providers", () => {
stateEventRegistrarService,
logService,
);
activeSut = new DefaultActiveUserStateProvider(mockAccountServiceWith(null), singleSut);
activeSut = new DefaultActiveUserStateProvider(new FakeActiveUserAccessor(null), singleSut);
globalSut = new DefaultGlobalStateProvider(storageServiceProvider, logService);
});

View File

@@ -10,5 +10,6 @@ export { KeyDefinition, KeyDefinitionOptions } from "./key-definition";
export { StateUpdateOptions } from "./state-update-options";
export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition";
export { StateEventRunnerService } from "./state-event-runner.service";
export { ActiveUserAccessor } from "./active-user.accessor";
export * from "./state-definitions";

View File

@@ -128,8 +128,8 @@ export class Attachment extends Domain {
url: this.url,
size: this.size,
sizeName: this.sizeName,
fileName: this.fileName?.toJSON(),
key: this.key?.toJSON(),
fileName: this.fileName?.toSdk(),
key: this.key?.toSdk(),
};
}

View File

@@ -95,12 +95,12 @@ export class Card extends Domain {
*/
toSdkCard(): SdkCard {
return {
cardholderName: this.cardholderName?.toJSON(),
brand: this.brand?.toJSON(),
number: this.number?.toJSON(),
expMonth: this.expMonth?.toJSON(),
expYear: this.expYear?.toJSON(),
code: this.code?.toJSON(),
cardholderName: this.cardholderName?.toSdk(),
brand: this.brand?.toSdk(),
number: this.number?.toSdk(),
expMonth: this.expMonth?.toSdk(),
expYear: this.expYear?.toSdk(),
code: this.code?.toSdk(),
};
}

View File

@@ -11,6 +11,7 @@ import {
CipherRepromptType as SdkCipherRepromptType,
LoginLinkedIdType,
Cipher as SdkCipher,
EncString as SdkEncString,
} from "@bitwarden/sdk-internal";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
@@ -1010,22 +1011,22 @@ describe("Cipher DTO", () => {
organizationId: "orgId",
folderId: "folderId",
collectionIds: [],
key: "EncryptedString",
name: "EncryptedString",
notes: "EncryptedString",
key: "EncryptedString" as SdkEncString,
name: "EncryptedString" as SdkEncString,
notes: "EncryptedString" as SdkEncString,
type: SdkCipherType.Login,
login: {
username: "EncryptedString",
password: "EncryptedString",
username: "EncryptedString" as SdkEncString,
password: "EncryptedString" as SdkEncString,
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
uris: [
{
uri: "EncryptedString",
uriChecksum: "EncryptedString",
uri: "EncryptedString" as SdkEncString,
uriChecksum: "EncryptedString" as SdkEncString,
match: UriMatchType.Domain,
},
],
totp: "EncryptedString",
totp: "EncryptedString" as SdkEncString,
autofillOnPageLoad: false,
fido2Credentials: undefined,
},
@@ -1049,35 +1050,35 @@ describe("Cipher DTO", () => {
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
fileName: "file" as SdkEncString,
key: "EncKey" as SdkEncString,
},
{
id: "a2",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
fileName: "file" as SdkEncString,
key: "EncKey" as SdkEncString,
},
],
fields: [
{
name: "EncryptedString",
value: "EncryptedString",
name: "EncryptedString" as SdkEncString,
value: "EncryptedString" as SdkEncString,
type: FieldType.Linked,
linkedId: LoginLinkedIdType.Username,
},
{
name: "EncryptedString",
value: "EncryptedString",
name: "EncryptedString" as SdkEncString,
value: "EncryptedString" as SdkEncString,
type: FieldType.Linked,
linkedId: LoginLinkedIdType.Password,
},
],
passwordHistory: [
{
password: "EncryptedString",
password: "EncryptedString" as SdkEncString,
lastUsedDate: "2022-01-31T12:00:00.000Z",
},
],

View File

@@ -348,9 +348,9 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
organizationId: this.organizationId ?? undefined,
folderId: this.folderId ?? undefined,
collectionIds: this.collectionIds ?? [],
key: this.key?.toJSON(),
name: this.name.toJSON(),
notes: this.notes?.toJSON(),
key: this.key?.toSdk(),
name: this.name.toSdk(),
notes: this.notes?.toSdk(),
type: this.type,
favorite: this.favorite ?? false,
organizationUseTotp: this.organizationUseTotp ?? false,

View File

@@ -158,18 +158,18 @@ export class Fido2Credential extends Domain {
*/
toSdkFido2Credential(): SdkFido2Credential {
return {
credentialId: this.credentialId?.toJSON(),
keyType: this.keyType.toJSON(),
keyAlgorithm: this.keyAlgorithm.toJSON(),
keyCurve: this.keyCurve.toJSON(),
keyValue: this.keyValue.toJSON(),
rpId: this.rpId.toJSON(),
userHandle: this.userHandle?.toJSON(),
userName: this.userName?.toJSON(),
counter: this.counter.toJSON(),
rpName: this.rpName?.toJSON(),
userDisplayName: this.userDisplayName?.toJSON(),
discoverable: this.discoverable?.toJSON(),
credentialId: this.credentialId?.toSdk(),
keyType: this.keyType.toSdk(),
keyAlgorithm: this.keyAlgorithm.toSdk(),
keyCurve: this.keyCurve.toSdk(),
keyValue: this.keyValue.toSdk(),
rpId: this.rpId.toSdk(),
userHandle: this.userHandle?.toSdk(),
userName: this.userName?.toSdk(),
counter: this.counter.toSdk(),
rpName: this.rpName?.toSdk(),
userDisplayName: this.userDisplayName?.toSdk(),
discoverable: this.discoverable?.toSdk(),
creationDate: this.creationDate.toISOString(),
};
}

View File

@@ -83,8 +83,8 @@ export class Field extends Domain {
*/
toSdkField(): SdkField {
return {
name: this.name?.toJSON(),
value: this.value?.toJSON(),
name: this.name?.toSdk(),
value: this.value?.toSdk(),
type: this.type,
// Safe type cast: client and SDK LinkedIdType enums have identical values
linkedId: this.linkedId as unknown as SdkLinkedIdType,

View File

@@ -175,24 +175,24 @@ export class Identity extends Domain {
*/
toSdkIdentity(): SdkIdentity {
return {
title: this.title?.toJSON(),
firstName: this.firstName?.toJSON(),
middleName: this.middleName?.toJSON(),
lastName: this.lastName?.toJSON(),
address1: this.address1?.toJSON(),
address2: this.address2?.toJSON(),
address3: this.address3?.toJSON(),
city: this.city?.toJSON(),
state: this.state?.toJSON(),
postalCode: this.postalCode?.toJSON(),
country: this.country?.toJSON(),
company: this.company?.toJSON(),
email: this.email?.toJSON(),
phone: this.phone?.toJSON(),
ssn: this.ssn?.toJSON(),
username: this.username?.toJSON(),
passportNumber: this.passportNumber?.toJSON(),
licenseNumber: this.licenseNumber?.toJSON(),
title: this.title?.toSdk(),
firstName: this.firstName?.toSdk(),
middleName: this.middleName?.toSdk(),
lastName: this.lastName?.toSdk(),
address1: this.address1?.toSdk(),
address2: this.address2?.toSdk(),
address3: this.address3?.toSdk(),
city: this.city?.toSdk(),
state: this.state?.toSdk(),
postalCode: this.postalCode?.toSdk(),
country: this.country?.toSdk(),
company: this.company?.toSdk(),
email: this.email?.toSdk(),
phone: this.phone?.toSdk(),
ssn: this.ssn?.toSdk(),
username: this.username?.toSdk(),
passportNumber: this.passportNumber?.toSdk(),
licenseNumber: this.licenseNumber?.toSdk(),
};
}

View File

@@ -97,8 +97,8 @@ export class LoginUri extends Domain {
*/
toSdkLoginUri(): SdkLoginUri {
return {
uri: this.uri?.toJSON(),
uriChecksum: this.uriChecksum?.toJSON(),
uri: this.uri?.toSdk(),
uriChecksum: this.uriChecksum?.toSdk(),
match: this.match,
};
}

View File

@@ -155,10 +155,10 @@ export class Login extends Domain {
toSdkLogin(): SdkLogin {
return {
uris: this.uris?.map((u) => u.toSdkLoginUri()),
username: this.username?.toJSON(),
password: this.password?.toJSON(),
username: this.username?.toSdk(),
password: this.password?.toSdk(),
passwordRevisionDate: this.passwordRevisionDate?.toISOString(),
totp: this.totp?.toJSON(),
totp: this.totp?.toSdk(),
autofillOnPageLoad: this.autofillOnPageLoad ?? undefined,
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
};

View File

@@ -67,7 +67,7 @@ export class Password extends Domain {
*/
toSdkPasswordHistory(): PasswordHistory {
return {
password: this.password.toJSON(),
password: this.password.toSdk(),
lastUsedDate: this.lastUsedDate.toISOString(),
};
}

View File

@@ -80,9 +80,9 @@ export class SshKey extends Domain {
*/
toSdkSshKey(): SdkSshKey {
return {
privateKey: this.privateKey.toJSON(),
publicKey: this.publicKey.toJSON(),
fingerprint: this.keyFingerprint.toJSON(),
privateKey: this.privateKey.toSdk(),
publicKey: this.publicKey.toSdk(),
fingerprint: this.keyFingerprint.toSdk(),
};
}

View File

@@ -69,7 +69,7 @@ export class AttachmentView implements View {
size: this.size,
sizeName: this.sizeName,
fileName: this.fileName,
key: this.encryptedKey?.toJSON(),
key: this.encryptedKey?.toSdk(),
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
decryptedKey: this.key ? this.key.toBase64() : null,
};

View File

@@ -331,7 +331,7 @@ export class CipherView implements View, InitializerMetadata {
creationDate: (this.creationDate ?? new Date()).toISOString(),
deletedDate: this.deletedDate?.toISOString(),
reprompt: this.reprompt ?? CipherRepromptType.None,
key: this.key?.toJSON(),
key: this.key?.toSdk(),
// Cipher type specific properties are set in the switch statement below
// CipherView initializes each with default constructors (undefined values)
// The SDK does not expect those undefined values and will throw exceptions

View File

@@ -15,7 +15,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal";
import { VerifyAsymmetricKeysResponse, EncString as SdkEncString } from "@bitwarden/sdk-internal";
import { KeyService } from "../../abstractions/key.service";
import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service";
@@ -28,7 +28,7 @@ function setupVerificationResponse(
) {
const mockKeyPairResponse = {
userPublicKey: "userPublicKey",
userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey",
userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey" as SdkEncString,
};
sdkService.client.crypto

16
package-lock.json generated
View File

@@ -23,7 +23,7 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
"@bitwarden/sdk-internal": "0.2.0-main.237",
"@bitwarden/sdk-internal": "0.2.0-main.242",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -128,7 +128,7 @@
"base64-loader": "1.0.0",
"browserslist": "4.23.2",
"chromatic": "13.1.2",
"concurrently": "9.1.2",
"concurrently": "9.2.0",
"copy-webpack-plugin": "13.0.0",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
@@ -4622,9 +4622,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
"version": "0.2.0-main.237",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.237.tgz",
"integrity": "sha512-1psCagsmUo2QeIw/xFW/OCfSInl6Gu+LYldbdLuv1z26FurrgmAv8BejDaPRx006BRn0z0hn6TlZtteaZS762w==",
"version": "0.2.0-main.242",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.242.tgz",
"integrity": "sha512-LFPNAAq9ORVGdvcB3PBhlM3GQZUMf3MhIuYbZxmhAG5SVlvem+sbaolgK3Fnf/8ajVx1IDMNEhfgQkA4mU9uAg==",
"license": "GPL-3.0",
"dependencies": {
"type-fest": "^4.41.0"
@@ -17206,9 +17206,9 @@
}
},
"node_modules/concurrently": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
"integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
"integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -91,7 +91,7 @@
"base64-loader": "1.0.0",
"browserslist": "4.23.2",
"chromatic": "13.1.2",
"concurrently": "9.1.2",
"concurrently": "9.2.0",
"copy-webpack-plugin": "13.0.0",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
@@ -158,7 +158,7 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
"@bitwarden/sdk-internal": "0.2.0-main.237",
"@bitwarden/sdk-internal": "0.2.0-main.242",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",