1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 03:13:55 +00:00

[PM-16694] ac integration page background fill missing (#18508)

* Fixing some tech debt before implementing actual fix of implementation

* Adding new components to handle the different routes for the integrations page to make use of bit-tab-nav-bar to follow background-fill UI spec

* Implement organization integrations page with routing and state management

- Added routing for organization integrations including device management, event management, single sign-on, and user provisioning.
- Created OrganizationIntegrationsState to manage integrations and organization data.
- Introduced OrganizationIntegrationsResolver for preloading organization and integration data.
- Updated components to utilize the new state management and resolver.
- Refactored integration routes to follow updated naming conventions.

* Refactor organization integrations components to use signals and observables; enhance async handling in templates and add debug logging

* Enhance organization integrations module with routing updates and state management improvements

- Added OrganizationIntegrationsState for better state management.
- Updated routing to redirect to single sign-on by default.
- Integrated OrganizationIntegrationsResolver for preloading data.
- Refactored components to utilize new state management and improved async handling.

* Refactor SingleSignOnComponent to remove OnInit lifecycle and debug logging

- Simplified SingleSignOnComponent by removing the OnInit implementation.
- Eliminated debug logging for integrations in ngOnInit.
- Cleaned up imports for better readability.

* Refactor WebHeaderComponent to simplify background handling

- Removed the useAltBackground input signal from WebHeaderComponent.
- Updated the HTML template to conditionally apply styles based solely on the child element count of the tabs container.

* Refactor organization integrations components for improved readability and performance

- Updated HTML templates to remove optional chaining for organization properties.
- Removed unnecessary debug logging and comments in the OrganizationIntegrationsResolver.
- Simplified DeviceManagementComponent by eliminating the OnInit lifecycle hook.

* Refactor organization integrations components to use direct state properties

- Updated components to access organization and integrations directly from state instead of using observables.
- Simplified HTML templates by removing async pipes and using direct function calls for better readability.
- Ensured consistent naming conventions for organization and integrations variables across components.

* Enhance WebHeaderComponent by adding bitTypography attribute to the title element for improved styling consistency

* Refactor organization state to use 'undefined' instead of 'null' for organization signal and remove OnInit lifecycle hook from UserProvisioningComponent for cleaner code.

* Refactor EventManagementComponent to remove OnInit lifecycle hook for cleaner code and improved readability.

* Update organization state to set organization value to 'undefined' when null is provided, enhancing state management consistency.

* Update WebHeaderComponent to allow optional title and icon inputs, enhancing flexibility in header configuration.

* Update WebHeaderComponent to allow account property to be nullable, improving type safety and handling of user data.
This commit is contained in:
Jared
2026-02-05 10:58:42 -05:00
committed by jaasen-livefront
parent 076cc24fc5
commit 26087b846c
18 changed files with 528 additions and 418 deletions

View File

@@ -79,7 +79,7 @@
<bit-nav-item
icon="bwi-msp"
[text]="'integrations' | i18n"
route="integrations"
route="integrations/single-sign-on"
*ngIf="integrationPageEnabled$ | async"
></bit-nav-item>
<bit-nav-group

View File

@@ -13,11 +13,11 @@
bitTypography="h1"
noMargin
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
[title]="title || (routeData.titleId | i18n)"
[title]="title() || (routeData.titleId | i18n)"
>
<div class="tw-truncate">
<i *ngIf="icon" class="bwi {{ icon }}" aria-hidden="true"></i>
{{ title || (routeData.titleId | i18n) }}
<i *ngIf="icon" class="bwi {{ icon() }}" aria-hidden="true"></i>
{{ title() || (routeData.titleId | i18n) }}
</div>
<div><ng-content select="[slot=title-suffix]"></ng-content></div>
</h1>

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
import { Component, input, InputSignal } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { map, Observable } from "rxjs";
@@ -25,19 +23,15 @@ export class WebHeaderComponent {
/**
* Custom title that overrides the route data `titleId`
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() title: string;
readonly title: InputSignal<string | undefined> = input();
/**
* Icon to show before the title
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() icon: string;
readonly icon: InputSignal<string | undefined> = input();
protected routeData$: Observable<{ titleId: string }>;
protected account$: Observable<User & { id: UserId }>;
protected account$: Observable<(User & { id: UserId }) | null>;
protected canLock$: Observable<boolean>;
protected selfHosted: boolean;
protected hostname = location.hostname;

View File

@@ -0,0 +1,11 @@
@let integrationsList = integrations();
<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>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "../integrations.pipe";
import { OrganizationIntegrationsState } from "../organization-integrations.state";
// 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: "device-management",
templateUrl: "device-management.component.html",
imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
})
export class DeviceManagementComponent {
integrations = this.state.integrations;
constructor(private state: OrganizationIntegrationsState) {}
get IntegrationType(): typeof IntegrationType {
return IntegrationType;
}
}

View File

@@ -0,0 +1,11 @@
@let integrationsList = integrations();
<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>

View File

@@ -0,0 +1,24 @@
import { Component } from "@angular/core";
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "../integrations.pipe";
import { OrganizationIntegrationsState } from "../organization-integrations.state";
// 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: "event-management",
templateUrl: "event-management.component.html",
imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
})
export class EventManagementComponent {
integrations = this.state.integrations;
constructor(private state: OrganizationIntegrationsState) {}
get IntegrationType(): typeof IntegrationType {
return IntegrationType;
}
}

View File

@@ -1,82 +1,18 @@
<app-header> </app-header>
@let org = organization();
@let organization = organization$ | async;
<app-header>
@if (org) {
<bit-tab-nav-bar slot="tabs">
<bit-tab-link route="single-sign-on">{{ "singleSignOn" | i18n }}</bit-tab-link>
@if (org.useScim || org.useDirectory) {
<bit-tab-link route="user-provisioning">{{ "userProvisioning" | i18n }}</bit-tab-link>
}
@if (org.useEvents) {
<bit-tab-link route="event-management">{{ "eventManagement" | i18n }}</bit-tab-link>
}
<bit-tab-link route="device-management">{{ "deviceManagement" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>
}
</app-header>
@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>
}
@if (organization?.useScim || organization?.useDirectory) {
<bit-tab [label]="'userProvisioning' | i18n">
@if (organization?.useScim) {
<section class="tw-mb-9">
<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>
}
@if (organization?.useDirectory) {
<section class="tw-mb-9">
<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?.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>
}
<router-outlet></router-outlet>

View File

@@ -1,336 +1,22 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } from "rxjs";
import { Component } from "@angular/core";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { IntegrationType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { getById } from "@bitwarden/common/platform/misc";
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "./integrations.pipe";
import { OrganizationIntegrationsState } from "./organization-integrations.state";
// 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({
selector: "ac-integrations",
templateUrl: "./integrations.component.html",
imports: [SharedModule, IntegrationGridComponent, HeaderModule, FilterIntegrationsPipe],
imports: [SharedModule, HeaderModule],
})
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
tabIndex: number = 0;
organization$: Observable<Organization> = new Observable<Organization>();
isEventManagementForDataDogAndCrowdStrikeEnabled: boolean = false;
isEventManagementForHuntressEnabled: boolean = false;
private destroy$ = new Subject<void>();
export class AdminConsoleIntegrationsComponent {
organization = this.state.organization;
// 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: "Sumo Logic",
linkURL: "https://bitwarden.com/help/sumo-logic-siem/",
image: "../../../../../../../images/integrations/logo-sumo-logic-siem.svg",
imageDarkMode: "../../../../../../../images/integrations/logo-sumo-logic-siem-darkmode.svg",
type: IntegrationType.EVENT,
newBadgeExpiration: "2025-12-31",
},
{
name: "Microsoft Intune",
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
type: IntegrationType.DEVICE,
},
];
async ngOnInit() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (!userId) {
throw new Error("User ID not found");
}
this.organization$ = this.route.params.pipe(
switchMap((params) =>
this.organizationService.organizations$(userId).pipe(
getById(params.organizationId),
// Filter out undefined values
takeWhile((org: Organization | undefined) => !!org),
),
),
);
// Sets the organization ID which also loads the integrations$
this.organization$
.pipe(
switchMap((org) => this.organizationIntegrationService.setOrganizationId(org.id)),
takeUntil(this.destroy$),
)
.subscribe();
}
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private accountService: AccountService,
private configService: ConfigService,
private organizationIntegrationService: OrganizationIntegrationService,
) {
this.configService
.getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike)
.pipe(takeUntil(this.destroy$))
.subscribe((isEnabled) => {
this.isEventManagementForDataDogAndCrowdStrikeEnabled = isEnabled;
});
this.configService
.getFeatureFlag$(FeatureFlag.EventManagementForHuntress)
.pipe(takeUntil(this.destroy$))
.subscribe((isEnabled) => {
this.isEventManagementForHuntressEnabled = isEnabled;
});
// Add the new event based items to the list
if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) {
const crowdstrikeIntegration: Integration = {
name: OrganizationIntegrationServiceName.CrowdStrike,
linkURL: "https://bitwarden.com/help/crowdstrike-siem/",
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
type: IntegrationType.EVENT,
description: "crowdstrikeEventIntegrationDesc",
canSetupConnection: true,
integrationType: OrganizationIntegrationType.Hec,
};
this.integrationsList.push(crowdstrikeIntegration);
const datadogIntegration: Integration = {
name: OrganizationIntegrationServiceName.Datadog,
linkURL: "https://bitwarden.com/help/datadog-siem/",
image: "../../../../../../../images/integrations/logo-datadog-color.svg",
type: IntegrationType.EVENT,
description: "datadogEventIntegrationDesc",
canSetupConnection: true,
integrationType: OrganizationIntegrationType.Datadog,
};
this.integrationsList.push(datadogIntegration);
}
// Add Huntress SIEM integration (separate feature flag)
if (this.isEventManagementForHuntressEnabled) {
const huntressIntegration: Integration = {
name: OrganizationIntegrationServiceName.Huntress,
linkURL: "https://bitwarden.com/help/huntress-siem/",
image: "../../../../../../../images/integrations/logo-huntress-siem.svg",
type: IntegrationType.EVENT,
description: "huntressEventIntegrationDesc",
canSetupConnection: true,
integrationType: OrganizationIntegrationType.Hec,
};
this.integrationsList.push(huntressIntegration);
}
// For all existing event based configurations loop through and assign the
// organizationIntegration for the correct services.
this.organizationIntegrationService.integrations$
.pipe(takeUntil(this.destroy$))
.subscribe((integrations) => {
// reset all event based integrations to null first - in case one was deleted
this.integrationsList.forEach((i) => {
i.organizationIntegration = null;
});
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();
}
constructor(private state: OrganizationIntegrationsState) {}
// use in the view
get IntegrationType(): typeof IntegrationType {

View File

@@ -7,7 +7,10 @@ import { IntegrationType } from "@bitwarden/common/enums";
name: "filterIntegrations",
})
export class FilterIntegrationsPipe implements PipeTransform {
transform(integrations: Integration[], type: IntegrationType): Integration[] {
transform(integrations: Integration[] | null | undefined, type: IntegrationType): Integration[] {
if (!integrations) {
return [];
}
return integrations.filter((integration) => integration.type === type);
}
}

View File

@@ -3,16 +3,31 @@ import { RouterModule, Routes } from "@angular/router";
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
import { DeviceManagementComponent } from "./device-management/device-management.component";
import { EventManagementComponent } from "./event-management/event-management.component";
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver";
import { OrganizationIntegrationsState } from "./organization-integrations.state";
import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component";
import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component";
const routes: Routes = [
{
path: "",
canActivate: [organizationPermissionsGuard((org) => org.canAccessIntegrations)],
component: AdminConsoleIntegrationsComponent,
data: {
titleId: "integrations",
},
component: AdminConsoleIntegrationsComponent,
providers: [OrganizationIntegrationsState, OrganizationIntegrationsResolver],
resolve: { integrations: OrganizationIntegrationsResolver },
children: [
{ path: "", redirectTo: "single-sign-on", pathMatch: "full" },
{ path: "single-sign-on", component: SingleSignOnComponent },
{ path: "user-provisioning", component: UserProvisioningComponent },
{ path: "event-management", component: EventManagementComponent },
{ path: "device-management", component: DeviceManagementComponent },
],
},
];

View File

@@ -1,17 +1,30 @@
import { NgModule } from "@angular/core";
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
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";
import { EventManagementComponent } from "./event-management/event-management.component";
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
import { OrganizationIntegrationsRoutingModule } from "./organization-integrations-routing.module";
import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver";
import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component";
import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component";
@NgModule({
imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
imports: [
AdminConsoleIntegrationsComponent,
OrganizationIntegrationsRoutingModule,
SingleSignOnComponent,
UserProvisioningComponent,
DeviceManagementComponent,
EventManagementComponent,
],
providers: [
OrganizationIntegrationsResolver,
safeProvider({
provide: OrganizationIntegrationService,
useClass: OrganizationIntegrationService,

View File

@@ -0,0 +1,285 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Resolve } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { take, takeWhile } from "rxjs/operators";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { IntegrationType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { getById } from "@bitwarden/common/platform/misc";
import { OrganizationIntegrationsState } from "./organization-integrations.state";
@Injectable()
export class OrganizationIntegrationsResolver implements Resolve<boolean> {
constructor(
private organizationService: OrganizationService,
private accountService: AccountService,
private configService: ConfigService,
private organizationIntegrationService: OrganizationIntegrationService,
private state: OrganizationIntegrationsState,
) {}
async resolve(route: ActivatedRouteSnapshot): Promise<boolean> {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (!userId) {
throw new Error("User ID not found");
}
const orgId = route.paramMap.get("organizationId")!;
const org = await firstValueFrom(
this.organizationService.organizations$(userId).pipe(getById(orgId), takeWhile(Boolean)),
);
this.state.setOrganization(org);
await firstValueFrom(this.organizationIntegrationService.setOrganizationId(org.id));
const integrations: 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: "Sumo Logic",
linkURL: "https://bitwarden.com/help/sumo-logic-siem/",
image: "../../../../../../../images/integrations/logo-sumo-logic-siem.svg",
imageDarkMode: "../../../../../../../images/integrations/logo-sumo-logic-siem-darkmode.svg",
type: IntegrationType.EVENT,
newBadgeExpiration: "2025-12-31",
},
{
name: "Microsoft Intune",
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
type: IntegrationType.DEVICE,
},
];
const featureEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike),
);
if (featureEnabled) {
integrations.push(
{
name: OrganizationIntegrationServiceName.CrowdStrike,
linkURL: "https://bitwarden.com/help/crowdstrike-siem/",
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
type: IntegrationType.EVENT,
canSetupConnection: true,
integrationType: OrganizationIntegrationType.Hec,
},
{
name: OrganizationIntegrationServiceName.Datadog,
linkURL: "https://bitwarden.com/help/datadog-siem/",
image: "../../../../../../../images/integrations/logo-datadog-color.svg",
type: IntegrationType.EVENT,
canSetupConnection: true,
integrationType: OrganizationIntegrationType.Datadog,
},
);
}
// Add Huntress SIEM integration (separate feature flag)
const huntressFeatureEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.EventManagementForHuntress),
);
if (huntressFeatureEnabled) {
integrations.push({
name: OrganizationIntegrationServiceName.Huntress,
linkURL: "https://bitwarden.com/help/huntress-siem/",
image: "../../../../../../../images/integrations/logo-huntress-siem.svg",
type: IntegrationType.EVENT,
description: "huntressEventIntegrationDesc",
canSetupConnection: true,
integrationType: OrganizationIntegrationType.Hec,
});
}
const orgIntegrations = await firstValueFrom(
this.organizationIntegrationService.integrations$.pipe(take(1)),
);
const merged = integrations.map((i) => ({
...i,
organizationIntegration: orgIntegrations.find((o) => o.serviceName === i.name) ?? null,
}));
this.state.setIntegrations(merged);
return true;
}
}

View File

@@ -0,0 +1,22 @@
import { Injectable, signal } from "@angular/core";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@Injectable()
export class OrganizationIntegrationsState {
private readonly _integrations = signal<Integration[]>([]);
private readonly _organization = signal<Organization | undefined>(undefined);
// Signals
integrations = this._integrations.asReadonly();
organization = this._organization.asReadonly();
setOrganization(val: Organization | null) {
this._organization.set(val ?? undefined);
}
setIntegrations(val: Integration[]) {
this._integrations.set(val);
}
}

View File

@@ -0,0 +1,12 @@
@let integrationsList = integrations();
<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>

View File

@@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "../integrations.pipe";
import { OrganizationIntegrationsState } from "../organization-integrations.state";
// 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: "single-sign-on",
templateUrl: "single-sign-on.component.html",
imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
})
export class SingleSignOnComponent {
integrations = this.state.integrations;
IntegrationType = IntegrationType;
constructor(private state: OrganizationIntegrationsState) {}
}

View File

@@ -0,0 +1,25 @@
@let org = organization();
@let integrationsList = integrations();
<section class="tw-mb-9" *ngIf="org?.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="org?.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>

View File

@@ -0,0 +1,26 @@
import { Component } from "@angular/core";
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "../integrations.pipe";
import { OrganizationIntegrationsState } from "../organization-integrations.state";
// 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: "user-provisioning",
templateUrl: "user-provisioning.component.html",
imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
})
export class UserProvisioningComponent {
organization = this.state.organization;
integrations = this.state.integrations;
constructor(private state: OrganizationIntegrationsState) {}
get IntegrationType(): typeof IntegrationType {
return IntegrationType;
}
}